mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Updated the OpenGroup polling to run on a non-main thread Updated the TSGroupModel to store moderatorIds as well as the adminIds (new endpoint is only going to give diffs) Updated the BatchRequest to support json, base64 encoded strings and raw bytes for it's body Replaced the 'lastMessageServerID' methods with 'OpenGroupSequenceNumber' methods (since we have swapped the property over) Added an alert when banning fails (previously it would fail silently) Fixed a bug where sent blinded messages were appearing as incoming messages Fixed a bug where the OpenGroup infoUpdates wasn't getting decoded correctly Fixed an issue where the ConversationVC wouldn't become the first responder again after the ban alerts disappeared Fixed an issue where I'd incorrectly used the message 'seqNo' in place of the message server id Fixed an issue where open group messages were setting their `sentTimestamp` to seconds instead of milliseconds for incoming messages
606 lines
30 KiB
Swift
606 lines
30 KiB
Swift
|
|
// See https://github.com/yapstudios/YapDatabase/wiki/LongLivedReadTransactions and
|
|
// https://github.com/yapstudios/YapDatabase/wiki/YapDatabaseModifiedNotification for
|
|
// more information on database handling.
|
|
final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConversationButtonSetDelegate, SeedReminderViewDelegate {
|
|
private var threads: YapDatabaseViewMappings!
|
|
private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel
|
|
private var tableViewTopConstraint: NSLayoutConstraint!
|
|
|
|
private var messageRequestCount: UInt {
|
|
threads.numberOfItems(inGroup: TSMessageRequestGroup)
|
|
}
|
|
|
|
private var threadCount: UInt {
|
|
threads.numberOfItems(inGroup: TSInboxGroup)
|
|
}
|
|
|
|
private lazy var dbConnection: YapDatabaseConnection = {
|
|
let result = OWSPrimaryStorage.shared().newDatabaseConnection()
|
|
result.objectCacheLimit = 500
|
|
return result
|
|
}()
|
|
|
|
// MARK: UI Components
|
|
private lazy var seedReminderView: SeedReminderView = {
|
|
let result = SeedReminderView(hasContinueButton: true)
|
|
let title = "You're almost finished! 80%"
|
|
let attributedTitle = NSMutableAttributedString(string: title)
|
|
attributedTitle.addAttribute(.foregroundColor, value: Colors.accent, range: (title as NSString).range(of: "80%"))
|
|
result.title = attributedTitle
|
|
result.subtitle = NSLocalizedString("view_seed_reminder_subtitle_1", comment: "")
|
|
result.setProgress(0.8, animated: false)
|
|
result.delegate = self
|
|
return result
|
|
}()
|
|
|
|
private lazy var tableView: UITableView = {
|
|
let result = UITableView()
|
|
result.backgroundColor = .clear
|
|
result.separatorStyle = .none
|
|
result.register(MessageRequestsCell.self, forCellReuseIdentifier: MessageRequestsCell.reuseIdentifier)
|
|
result.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
|
|
let bottomInset = Values.newConversationButtonBottomOffset + NewConversationButtonSet.expandedButtonSize + Values.largeSpacing + NewConversationButtonSet.collapsedButtonSize
|
|
result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0)
|
|
result.showsVerticalScrollIndicator = false
|
|
return result
|
|
}()
|
|
|
|
private lazy var newConversationButtonSet: NewConversationButtonSet = {
|
|
let result = NewConversationButtonSet()
|
|
result.delegate = self
|
|
return result
|
|
}()
|
|
|
|
private lazy var fadeView: UIView = {
|
|
let result = UIView()
|
|
let gradient = Gradients.homeVCFade
|
|
result.setGradient(gradient)
|
|
result.isUserInteractionEnabled = false
|
|
return result
|
|
}()
|
|
|
|
private lazy var emptyStateView: UIView = {
|
|
let explanationLabel = UILabel()
|
|
explanationLabel.textColor = Colors.text
|
|
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
|
explanationLabel.numberOfLines = 0
|
|
explanationLabel.lineBreakMode = .byWordWrapping
|
|
explanationLabel.textAlignment = .center
|
|
explanationLabel.text = NSLocalizedString("vc_home_empty_state_message", comment: "")
|
|
let createNewPrivateChatButton = Button(style: .prominentOutline, size: .large)
|
|
createNewPrivateChatButton.setTitle(NSLocalizedString("vc_home_empty_state_button_title", comment: ""), for: UIControl.State.normal)
|
|
createNewPrivateChatButton.addTarget(self, action: #selector(createNewDM), for: UIControl.Event.touchUpInside)
|
|
createNewPrivateChatButton.set(.width, to: 196)
|
|
let result = UIStackView(arrangedSubviews: [ explanationLabel, createNewPrivateChatButton ])
|
|
result.axis = .vertical
|
|
result.spacing = Values.mediumSpacing
|
|
result.alignment = .center
|
|
result.isHidden = true
|
|
return result
|
|
}()
|
|
|
|
// MARK: Lifecycle
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
// Threads (part 1)
|
|
dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to)
|
|
// Preparation
|
|
SignalApp.shared().homeViewController = self
|
|
// Gradient & nav bar
|
|
setUpGradientBackground()
|
|
if navigationController?.navigationBar != nil {
|
|
setUpNavBarStyle()
|
|
}
|
|
updateNavBarButtons()
|
|
setUpNavBarSessionHeading()
|
|
// Recovery phrase reminder
|
|
let hasViewedSeed = UserDefaults.standard[.hasViewedSeed]
|
|
if !hasViewedSeed {
|
|
view.addSubview(seedReminderView)
|
|
seedReminderView.pin(.leading, to: .leading, of: view)
|
|
seedReminderView.pin(.top, to: .top, of: view)
|
|
seedReminderView.pin(.trailing, to: .trailing, of: view)
|
|
}
|
|
// Table view
|
|
tableView.dataSource = self
|
|
tableView.delegate = self
|
|
view.addSubview(tableView)
|
|
tableView.pin(.leading, to: .leading, of: view)
|
|
if !hasViewedSeed {
|
|
tableViewTopConstraint = tableView.pin(.top, to: .bottom, of: seedReminderView)
|
|
} else {
|
|
tableViewTopConstraint = tableView.pin(.top, to: .top, of: view, withInset: Values.smallSpacing)
|
|
}
|
|
tableView.pin(.trailing, to: .trailing, of: view)
|
|
tableView.pin(.bottom, to: .bottom, of: view)
|
|
view.addSubview(fadeView)
|
|
fadeView.pin(.leading, to: .leading, of: view)
|
|
let topInset = 0.15 * view.height()
|
|
fadeView.pin(.top, to: .top, of: view, withInset: topInset)
|
|
fadeView.pin(.trailing, to: .trailing, of: view)
|
|
fadeView.pin(.bottom, to: .bottom, of: view)
|
|
// Empty state view
|
|
view.addSubview(emptyStateView)
|
|
emptyStateView.center(.horizontal, in: view)
|
|
let verticalCenteringConstraint = emptyStateView.center(.vertical, in: view)
|
|
verticalCenteringConstraint.constant = -16 // Makes things appear centered visually
|
|
// New conversation button set
|
|
view.addSubview(newConversationButtonSet)
|
|
newConversationButtonSet.center(.horizontal, in: view)
|
|
newConversationButtonSet.pin(.bottom, to: .bottom, of: view, withInset: -Values.newConversationButtonBottomOffset) // Negative due to how the constraint is set up
|
|
// Notifications
|
|
let notificationCenter = NotificationCenter.default
|
|
notificationCenter.addObserver(self, selector: #selector(handleYapDatabaseModifiedNotification(_:)), name: .YapDatabaseModified, object: OWSPrimaryStorage.shared().dbNotificationObject)
|
|
notificationCenter.addObserver(self, selector: #selector(handleProfileDidChangeNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil)
|
|
notificationCenter.addObserver(self, selector: #selector(handleLocalProfileDidChangeNotification(_:)), name: Notification.Name(kNSNotificationName_LocalProfileDidChange), object: nil)
|
|
notificationCenter.addObserver(self, selector: #selector(handleSeedViewedNotification(_:)), name: .seedViewed, object: nil)
|
|
notificationCenter.addObserver(self, selector: #selector(handleBlockedContactsUpdatedNotification(_:)), name: .blockedContactsUpdated, object: nil)
|
|
// Threads (part 2)
|
|
threads = YapDatabaseViewMappings(groups: [ TSMessageRequestGroup, TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point
|
|
threads.setIsReversed(true, forGroup: TSInboxGroup)
|
|
dbConnection.read { transaction in
|
|
self.threads.update(with: transaction) // Perform the initial update
|
|
}
|
|
// Start polling if needed (i.e. if the user just created or restored their Session ID)
|
|
if OWSIdentityManager.shared().identityKeyPair() != nil {
|
|
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
|
appDelegate.startPollerIfNeeded()
|
|
appDelegate.startClosedGroupPoller()
|
|
appDelegate.startOpenGroupPollersIfNeeded()
|
|
// Do this only if we created a new Session ID, or if we already received the initial configuration message
|
|
if UserDefaults.standard[.hasSyncedInitialConfiguration] {
|
|
appDelegate.syncConfigurationIfNeeded()
|
|
}
|
|
}
|
|
// Re-populate snode pool if needed
|
|
SnodeAPI.getSnodePool().retainUntilComplete()
|
|
// Onion request path countries cache
|
|
DispatchQueue.global(qos: .utility).sync {
|
|
let _ = IP2Country.shared.populateCacheIfNeeded()
|
|
}
|
|
// Get default open group rooms if needed
|
|
OpenGroupManager.getDefaultRoomsIfNeeded()
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
reload()
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
// MARK: - UITableViewDataSource
|
|
|
|
func numberOfSections(in tableView: UITableView) -> Int {
|
|
return 2
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
switch section {
|
|
case 0:
|
|
if messageRequestCount > 0 && !UserDefaults.standard[.hasHiddenMessageRequests] {
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
|
|
case 1: return Int(threadCount)
|
|
default: return 0
|
|
}
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
switch indexPath.section {
|
|
case 0:
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: MessageRequestsCell.reuseIdentifier) as! MessageRequestsCell
|
|
cell.update(with: Int(messageRequestCount))
|
|
return cell
|
|
|
|
default:
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
|
|
cell.threadViewModel = threadViewModel(at: indexPath.row)
|
|
return cell
|
|
}
|
|
}
|
|
|
|
// MARK: Updating
|
|
|
|
private func reload() {
|
|
AssertIsOnMainThread()
|
|
dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit
|
|
dbConnection.read { transaction in
|
|
self.threads.update(with: transaction)
|
|
}
|
|
threadViewModelCache.removeAll()
|
|
tableView.reloadData()
|
|
emptyStateView.isHidden = (threadCount != 0)
|
|
}
|
|
|
|
@objc private func handleYapDatabaseModifiedNotification(_ yapDatabase: YapDatabase) {
|
|
// NOTE: This code is very finicky and crashes easily. Modify with care.
|
|
AssertIsOnMainThread()
|
|
// If we don't capture `threads` here, a race condition can occur where the
|
|
// `thread.snapshotOfLastUpdate != firstSnapshot - 1` check below evaluates to
|
|
// `false`, but `threads` then changes between that check and the
|
|
// `ext.getSectionChanges(§ionChanges, rowChanges: &rowChanges, for: notifications, with: threads)`
|
|
// line. This causes `tableView.endUpdates()` to crash with an `NSInternalInconsistencyException`.
|
|
let threads = threads!
|
|
// Create a stable state for the connection and jump to the latest commit
|
|
let notifications = dbConnection.beginLongLivedReadTransaction()
|
|
guard !notifications.isEmpty else { return }
|
|
let ext = dbConnection.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewConnection
|
|
let hasChanges = (
|
|
ext.hasChanges(forGroup: TSMessageRequestGroup, in: notifications) ||
|
|
ext.hasChanges(forGroup: TSInboxGroup, in: notifications)
|
|
)
|
|
|
|
guard hasChanges else { return }
|
|
|
|
if let firstChangeSet = notifications[0].userInfo {
|
|
let firstSnapshot = firstChangeSet[YapDatabaseSnapshotKey] as! UInt64
|
|
|
|
// The 'getSectionChanges' code below will crash if we try to process multiple commits at once
|
|
// so just force a full reload
|
|
if threads.snapshotOfLastUpdate != firstSnapshot - 1 {
|
|
// Check if we inserted a new message request (if so then unhide the message request banner)
|
|
if
|
|
let extensions: [String: Any] = firstChangeSet[YapDatabaseExtensionsKey] as? [String: Any],
|
|
let viewExtensions: [String: Any] = extensions[TSThreadDatabaseViewExtensionName] as? [String: Any]
|
|
{
|
|
// Note: We do a 'flatMap' here rather than explicitly grab the desired key because
|
|
// the key we need is 'changeset_key_changes' in 'YapDatabaseViewPrivate.h' so could
|
|
// change due to an update and silently break this - this approach is a bit safer
|
|
let allChanges: [Any] = Array(viewExtensions.values).compactMap { $0 as? [Any] }.flatMap { $0 }
|
|
let messageRequestInserts = allChanges
|
|
.compactMap { $0 as? YapDatabaseViewRowChange }
|
|
.filter { $0.finalGroup == TSMessageRequestGroup && $0.type == .insert }
|
|
|
|
if !messageRequestInserts.isEmpty && UserDefaults.standard[.hasHiddenMessageRequests] {
|
|
UserDefaults.standard[.hasHiddenMessageRequests] = false
|
|
}
|
|
}
|
|
|
|
return reload()
|
|
}
|
|
}
|
|
|
|
var sectionChanges = NSArray()
|
|
var rowChanges = NSArray()
|
|
ext.getSectionChanges(§ionChanges, rowChanges: &rowChanges, for: notifications, with: threads)
|
|
|
|
// Separate out the changes for new message requests and the inbox (so we can avoid updating for
|
|
// new messages within an existing message request)
|
|
let messageRequestInserts = rowChanges
|
|
.compactMap { $0 as? YapDatabaseViewRowChange }
|
|
.filter { $0.finalGroup == TSMessageRequestGroup && $0.type == .insert }
|
|
let inboxRowChanges = rowChanges
|
|
.filter { ($0 as? YapDatabaseViewRowChange)?.finalGroup != TSMessageRequestGroup }
|
|
|
|
guard sectionChanges.count > 0 || inboxRowChanges.count > 0 || messageRequestInserts.count > 0 else { return }
|
|
|
|
tableView.beginUpdates()
|
|
|
|
// If we need to unhide the message request row and then re-insert it
|
|
if !messageRequestInserts.isEmpty && UserDefaults.standard[.hasHiddenMessageRequests] {
|
|
UserDefaults.standard[.hasHiddenMessageRequests] = false
|
|
tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
|
|
}
|
|
|
|
// TODO: Crash due to change from Message Requests getting approved?
|
|
inboxRowChanges.forEach { rowChange in
|
|
let rowChange = rowChange as! YapDatabaseViewRowChange
|
|
let key = rowChange.collectionKey.key
|
|
threadViewModelCache[key] = nil
|
|
|
|
switch rowChange.type {
|
|
case .delete:
|
|
tableView.deleteRows(at: [ rowChange.indexPath! ], with: .automatic)
|
|
|
|
case .insert:
|
|
tableView.insertRows(at: [ rowChange.newIndexPath! ], with: .automatic)
|
|
|
|
case .update:
|
|
tableView.reloadRows(at: [ rowChange.indexPath! ], with: .automatic)
|
|
|
|
case .move:
|
|
// Note: We need to handle the move from the message requests section to the inbox (since
|
|
// we are only showing a single row for message requests we need to custom handle this as
|
|
// an insert as the change won't be defined correctly)
|
|
if rowChange.originalGroup == TSMessageRequestGroup && rowChange.finalGroup == TSInboxGroup {
|
|
tableView.insertRows(at: [ rowChange.newIndexPath! ], with: .automatic)
|
|
|
|
// If that was the last message request then we need to also remove the message request
|
|
// row to prevent a crash
|
|
if messageRequestCount == 0 {
|
|
tableView.deleteRows(at: [ IndexPath(row: 0, section: 0) ], with: .automatic)
|
|
}
|
|
}
|
|
|
|
default: break
|
|
}
|
|
}
|
|
tableView.endUpdates()
|
|
// HACK: Moves can have conflicts with the other 3 types of change.
|
|
// Just batch perform all the moves separately to prevent crashing.
|
|
// Since all the changes are from the original state to the final state,
|
|
// it will still be correct if we pick the moves out.
|
|
tableView.beginUpdates()
|
|
rowChanges.forEach { rowChange in
|
|
let rowChange = rowChange as! YapDatabaseViewRowChange
|
|
let key = rowChange.collectionKey.key
|
|
threadViewModelCache[key] = nil
|
|
|
|
switch rowChange.type {
|
|
case .move:
|
|
// Since we are custom handling this specific movement in the above 'updates' call we need
|
|
// to avoid trying to handle it here
|
|
if rowChange.originalGroup == TSMessageRequestGroup && rowChange.finalGroup == TSInboxGroup {
|
|
return
|
|
}
|
|
|
|
tableView.moveRow(at: rowChange.indexPath!, to: rowChange.newIndexPath!)
|
|
|
|
default: break
|
|
}
|
|
}
|
|
tableView.endUpdates()
|
|
emptyStateView.isHidden = (threadCount != 0)
|
|
}
|
|
|
|
@objc private func handleProfileDidChangeNotification(_ notification: Notification) {
|
|
tableView.reloadData() // TODO: Just reload the affected cell
|
|
}
|
|
|
|
@objc private func handleLocalProfileDidChangeNotification(_ notification: Notification) {
|
|
updateNavBarButtons()
|
|
}
|
|
|
|
@objc private func handleSeedViewedNotification(_ notification: Notification) {
|
|
tableViewTopConstraint.isActive = false
|
|
tableViewTopConstraint = tableView.pin(.top, to: .top, of: view, withInset: Values.smallSpacing)
|
|
seedReminderView.removeFromSuperview()
|
|
}
|
|
|
|
@objc private func handleBlockedContactsUpdatedNotification(_ notification: Notification) {
|
|
self.tableView.reloadData() // TODO: Just reload the affected cell
|
|
}
|
|
|
|
private func updateNavBarButtons() {
|
|
// Profile picture view
|
|
let profilePictureSize = Values.verySmallProfilePictureSize
|
|
let profilePictureView = ProfilePictureView()
|
|
profilePictureView.accessibilityLabel = "Settings button"
|
|
profilePictureView.size = profilePictureSize
|
|
profilePictureView.publicKey = getUserHexEncodedPublicKey()
|
|
profilePictureView.update()
|
|
profilePictureView.set(.width, to: profilePictureSize)
|
|
profilePictureView.set(.height, to: profilePictureSize)
|
|
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
|
|
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
|
|
// Path status indicator
|
|
let pathStatusView = PathStatusView()
|
|
pathStatusView.accessibilityLabel = "Current onion routing path indicator"
|
|
pathStatusView.set(.width, to: PathStatusView.size)
|
|
pathStatusView.set(.height, to: PathStatusView.size)
|
|
// Container view
|
|
let profilePictureViewContainer = UIView()
|
|
profilePictureViewContainer.accessibilityLabel = "Settings button"
|
|
profilePictureViewContainer.addSubview(profilePictureView)
|
|
profilePictureView.autoPinEdgesToSuperviewEdges()
|
|
profilePictureViewContainer.addSubview(pathStatusView)
|
|
pathStatusView.pin(.trailing, to: .trailing, of: profilePictureViewContainer)
|
|
pathStatusView.pin(.bottom, to: .bottom, of: profilePictureViewContainer)
|
|
// Left bar button item
|
|
let leftBarButtonItem = UIBarButtonItem(customView: profilePictureViewContainer)
|
|
leftBarButtonItem.accessibilityLabel = "Settings button"
|
|
leftBarButtonItem.isAccessibilityElement = true
|
|
navigationItem.leftBarButtonItem = leftBarButtonItem
|
|
// Right bar button item - search button
|
|
let rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .search, target: self, action: #selector(showSearchUI))
|
|
rightBarButtonItem.accessibilityLabel = "Search button"
|
|
rightBarButtonItem.isAccessibilityElement = true
|
|
navigationItem.rightBarButtonItem = rightBarButtonItem
|
|
}
|
|
|
|
@objc override internal func handleAppModeChangedNotification(_ notification: Notification) {
|
|
super.handleAppModeChangedNotification(notification)
|
|
let gradient = Gradients.homeVCFade
|
|
fadeView.setGradient(gradient) // Re-do the gradient
|
|
tableView.reloadData()
|
|
}
|
|
|
|
// MARK: - UITableViewDelegate
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
tableView.deselectRow(at: indexPath, animated: true)
|
|
|
|
switch indexPath.section {
|
|
case 0:
|
|
let viewController: MessageRequestsViewController = MessageRequestsViewController()
|
|
self.navigationController?.pushViewController(viewController, animated: true)
|
|
return
|
|
|
|
default:
|
|
guard let thread = self.thread(at: indexPath.row) else { return }
|
|
show(thread, with: ConversationViewAction.none, highlightedMessageID: nil, animated: true)
|
|
}
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
|
return true
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
|
switch indexPath.section {
|
|
case 0:
|
|
let hide = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_HIDE_TITLE", comment: "")) { [weak self] _, _ in
|
|
UserDefaults.standard[.hasHiddenMessageRequests] = true
|
|
|
|
// Animate the row removal
|
|
self?.tableView.beginUpdates()
|
|
self?.tableView.deleteRows(at: [indexPath], with: .automatic)
|
|
self?.tableView.endUpdates()
|
|
}
|
|
hide.backgroundColor = Colors.destructive
|
|
|
|
return [hide]
|
|
|
|
default:
|
|
guard let thread = self.thread(at: indexPath.row) else { return [] }
|
|
let delete = UITableViewRowAction(style: .destructive, title: NSLocalizedString("TXT_DELETE_TITLE", comment: "")) { [weak self] _, _ in
|
|
var message = NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE", comment: "")
|
|
if let thread = thread as? TSGroupThread, thread.isClosedGroup, thread.groupModel.groupAdminIds.contains(getUserHexEncodedPublicKey()) {
|
|
message = NSLocalizedString("admin_group_leave_warning", comment: "")
|
|
}
|
|
let alert = UIAlertController(title: NSLocalizedString("CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE", comment: ""), message: message, preferredStyle: .alert)
|
|
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_DELETE_TITLE", comment: ""), style: .destructive) { [weak self] _ in
|
|
self?.delete(thread)
|
|
})
|
|
alert.addAction(UIAlertAction(title: NSLocalizedString("TXT_CANCEL_TITLE", comment: ""), style: .default) { _ in })
|
|
guard let self = self else { return }
|
|
self.present(alert, animated: true, completion: nil)
|
|
}
|
|
delete.backgroundColor = Colors.destructive
|
|
|
|
let isPinned = thread.isPinned
|
|
let pin = UITableViewRowAction(style: .normal, title: NSLocalizedString("PIN_BUTTON_TEXT", comment: "")) { [weak self] _, _ in
|
|
thread.isPinned = true
|
|
thread.save()
|
|
self?.threadViewModelCache.removeValue(forKey: thread.uniqueId!)
|
|
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
|
}
|
|
pin.backgroundColor = Colors.pathsBuilding
|
|
let unpin = UITableViewRowAction(style: .normal, title: NSLocalizedString("UNPIN_BUTTON_TEXT", comment: "")) { [weak self] _, _ in
|
|
thread.isPinned = false
|
|
thread.save()
|
|
self?.threadViewModelCache.removeValue(forKey: thread.uniqueId!)
|
|
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
|
}
|
|
unpin.backgroundColor = Colors.pathsBuilding
|
|
|
|
if let thread = thread as? TSContactThread {
|
|
let publicKey = thread.contactSessionID()
|
|
let blockingManager = SSKEnvironment.shared.blockingManager
|
|
let isBlocked = blockingManager.isRecipientIdBlocked(publicKey)
|
|
let block = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_BLOCK_BUTTON", comment: "")) { _, _ in
|
|
blockingManager.addBlockedPhoneNumber(publicKey)
|
|
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
|
}
|
|
block.backgroundColor = Colors.unimportant
|
|
let unblock = UITableViewRowAction(style: .normal, title: NSLocalizedString("BLOCK_LIST_UNBLOCK_BUTTON", comment: "")) { _, _ in
|
|
blockingManager.removeBlockedPhoneNumber(publicKey)
|
|
tableView.reloadRows(at: [ indexPath ], with: UITableView.RowAnimation.fade)
|
|
}
|
|
unblock.backgroundColor = Colors.unimportant
|
|
return [ delete, (isBlocked ? unblock : block), (isPinned ? unpin : pin) ]
|
|
} else {
|
|
return [ delete, (isPinned ? unpin : pin) ]
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Interaction
|
|
|
|
func handleContinueButtonTapped(from seedReminderView: SeedReminderView) {
|
|
let seedVC = SeedVC()
|
|
let navigationController = OWSNavigationController(rootViewController: seedVC)
|
|
present(navigationController, animated: true, completion: nil)
|
|
}
|
|
|
|
@objc func show(_ thread: TSThread, with action: ConversationViewAction, highlightedMessageID: String?, animated: Bool) {
|
|
DispatchMainThreadSafe {
|
|
if let presentedVC = self.presentedViewController {
|
|
presentedVC.dismiss(animated: false, completion: nil)
|
|
}
|
|
let conversationVC = ConversationVC(thread: thread)
|
|
self.navigationController?.setViewControllers([ self, conversationVC ], animated: true)
|
|
}
|
|
}
|
|
|
|
private func delete(_ thread: TSThread) {
|
|
let openGroup = Storage.shared.getOpenGroup(for: thread.uniqueId!)
|
|
Storage.write { transaction in
|
|
Storage.shared.cancelPendingMessageSendJobs(for: thread.uniqueId!, using: transaction)
|
|
if let openGroup = openGroup {
|
|
OpenGroupManager.shared.delete(openGroup, associatedWith: thread, using: transaction)
|
|
} else if let thread = thread as? TSGroupThread, thread.isClosedGroup == true {
|
|
let groupID = thread.groupModel.groupId
|
|
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
|
|
MessageSender.leave(groupPublicKey, using: transaction).retainUntilComplete()
|
|
thread.removeAllThreadInteractions(with: transaction)
|
|
thread.remove(with: transaction)
|
|
} else {
|
|
thread.removeAllThreadInteractions(with: transaction)
|
|
thread.remove(with: transaction)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc private func openSettings() {
|
|
let settingsVC = SettingsVC()
|
|
let navigationController = OWSNavigationController(rootViewController: settingsVC)
|
|
present(navigationController, animated: true, completion: nil)
|
|
}
|
|
|
|
@objc private func showSearchUI() {
|
|
if let presentedVC = self.presentedViewController {
|
|
presentedVC.dismiss(animated: false, completion: nil)
|
|
}
|
|
let searchController = GlobalSearchViewController()
|
|
self.navigationController?.setViewControllers([ self, searchController ], animated: true)
|
|
}
|
|
|
|
@objc func joinOpenGroup() {
|
|
let joinOpenGroupVC = JoinOpenGroupVC()
|
|
let navigationController = OWSNavigationController(rootViewController: joinOpenGroupVC)
|
|
present(navigationController, animated: true, completion: nil)
|
|
}
|
|
|
|
@objc func createNewDM() {
|
|
let newDMVC = NewDMVC()
|
|
let navigationController = OWSNavigationController(rootViewController: newDMVC)
|
|
present(navigationController, animated: true, completion: nil)
|
|
}
|
|
|
|
@objc(createNewDMFromDeepLink:)
|
|
func createNewDMFromDeepLink(sessionID: String) {
|
|
let newDMVC = NewDMVC(sessionID: sessionID)
|
|
let navigationController = OWSNavigationController(rootViewController: newDMVC)
|
|
present(navigationController, animated: true, completion: nil)
|
|
}
|
|
|
|
@objc func createClosedGroup() {
|
|
let newClosedGroupVC = NewClosedGroupVC()
|
|
let navigationController = OWSNavigationController(rootViewController: newClosedGroupVC)
|
|
present(navigationController, animated: true, completion: nil)
|
|
}
|
|
|
|
// MARK: Convenience
|
|
private func thread(at index: Int) -> TSThread? {
|
|
var thread: TSThread? = nil
|
|
dbConnection.read { transaction in
|
|
// Note: Section needs to be '1' as we now have 'TSMessageRequests' as the 0th section
|
|
let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction
|
|
thread = ext.object(atRow: UInt(index), inSection: 1, with: self.threads) as? TSThread
|
|
}
|
|
return thread
|
|
}
|
|
|
|
private func threadViewModel(at index: Int) -> ThreadViewModel? {
|
|
guard let thread = thread(at: index) else { return nil }
|
|
if let cachedThreadViewModel = threadViewModelCache[thread.uniqueId!] {
|
|
return cachedThreadViewModel
|
|
} else {
|
|
var threadViewModel: ThreadViewModel? = nil
|
|
dbConnection.read { transaction in
|
|
threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
|
|
}
|
|
threadViewModelCache[thread.uniqueId!] = threadViewModel
|
|
return threadViewModel
|
|
}
|
|
}
|
|
}
|