mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Fixed an issue where fileIds weren't correctly getting sent along with open group messages Fixed an issue where the screens could miss updates if the device was locked with the app in the foreground and then later unlocked after receiving notifications Added an optimisation to prevent attempting to send a message after it has been deleted Added logic to report fake calls if the code goes down an invalid code path when handling a call (to prevent Apple blocking the app) Delayed the core which clears notifications to increase the time the app has to handle interactions (just in case it was a race condition)
746 lines
30 KiB
Swift
746 lines
30 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import GRDB
|
|
import DifferenceKit
|
|
import SessionMessagingKit
|
|
import SessionUtilitiesKit
|
|
import SignalUtilitiesKit
|
|
|
|
final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConversationButtonSetDelegate, SeedReminderViewDelegate {
|
|
private static let loadingHeaderHeight: CGFloat = 20
|
|
|
|
private let viewModel: HomeViewModel = HomeViewModel()
|
|
private var dataChangeObservable: DatabaseCancellable?
|
|
private var hasLoadedInitialStateData: Bool = false
|
|
private var hasLoadedInitialThreadData: Bool = false
|
|
private var isLoadingMore: Bool = false
|
|
private var isAutoLoadingNextPage: Bool = false
|
|
private var viewHasAppeared: Bool = false
|
|
|
|
// MARK: - Intialization
|
|
|
|
init() {
|
|
Storage.shared.addObserver(viewModel.pagedDataObserver)
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
preconditionFailure("Use init() instead.")
|
|
}
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
// MARK: - UI
|
|
|
|
private var tableViewTopConstraint: NSLayoutConstraint!
|
|
|
|
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
|
|
result.isHidden = !self.viewModel.state.showViewedSeedBanner
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var loadingConversationsLabel: UILabel = {
|
|
let result: UILabel = UILabel()
|
|
result.font = UIFont.systemFont(ofSize: Values.smallFontSize)
|
|
result.text = "LOADING_CONVERSATIONS".localized()
|
|
result.textColor = Colors.text
|
|
result.textAlignment = .center
|
|
result.numberOfLines = 0
|
|
|
|
return result
|
|
}()
|
|
|
|
private lazy var tableView: UITableView = {
|
|
let result = UITableView()
|
|
result.backgroundColor = .clear
|
|
result.separatorStyle = .none
|
|
result.contentInset = UIEdgeInsets(
|
|
top: 0,
|
|
left: 0,
|
|
bottom: (
|
|
Values.newConversationButtonBottomOffset +
|
|
NewConversationButtonSet.expandedButtonSize +
|
|
Values.largeSpacing +
|
|
NewConversationButtonSet.collapsedButtonSize
|
|
),
|
|
right: 0
|
|
)
|
|
result.showsVerticalScrollIndicator = false
|
|
result.register(view: MessageRequestsCell.self)
|
|
result.register(view: FullConversationCell.self)
|
|
result.dataSource = self
|
|
result.delegate = self
|
|
|
|
if #available(iOS 15.0, *) {
|
|
result.sectionHeaderTopPadding = 0
|
|
}
|
|
|
|
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: Values.iPadButtonWidth)
|
|
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()
|
|
|
|
// Note: This is a hack to ensure `isRTL` is initially gets run on the main thread so the value
|
|
// is cached (it gets called on background threads and if it hasn't cached the value then it can
|
|
// cause odd performance issues since it accesses UIKit)
|
|
_ = CurrentAppContext().isRTL
|
|
|
|
// Preparation
|
|
SessionApp.homeViewController.mutate { $0 = self }
|
|
|
|
// Gradient & nav bar
|
|
setUpGradientBackground()
|
|
if navigationController?.navigationBar != nil {
|
|
setUpNavBarStyle()
|
|
}
|
|
updateNavBarButtons()
|
|
setUpNavBarSessionHeading()
|
|
|
|
// Recovery phrase reminder
|
|
view.addSubview(seedReminderView)
|
|
seedReminderView.pin(.leading, to: .leading, of: view)
|
|
seedReminderView.pin(.top, to: .top, of: view)
|
|
seedReminderView.pin(.trailing, to: .trailing, of: view)
|
|
|
|
// Loading conversations label
|
|
view.addSubview(loadingConversationsLabel)
|
|
|
|
loadingConversationsLabel.pin(.top, to: .top, of: view, withInset: Values.veryLargeSpacing)
|
|
loadingConversationsLabel.pin(.leading, to: .leading, of: view, withInset: 50)
|
|
loadingConversationsLabel.pin(.trailing, to: .trailing, of: view, withInset: -50)
|
|
|
|
// Table view
|
|
view.addSubview(tableView)
|
|
tableView.pin(.leading, to: .leading, of: view)
|
|
if self.viewModel.state.showViewedSeedBanner {
|
|
tableViewTopConstraint = tableView.pin(.top, to: .bottom, of: seedReminderView)
|
|
}
|
|
else {
|
|
tableViewTopConstraint = tableView.pin(.top, to: .top, of: view)
|
|
}
|
|
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
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(applicationDidBecomeActive(_:)),
|
|
name: UIApplication.didBecomeActiveNotification,
|
|
object: nil
|
|
)
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(applicationDidResignActive(_:)),
|
|
name: UIApplication.didEnterBackgroundNotification, object: nil
|
|
)
|
|
|
|
// Start polling if needed (i.e. if the user just created or restored their Session ID)
|
|
if Identity.userExists(), let appDelegate: AppDelegate = UIApplication.shared.delegate as? AppDelegate {
|
|
appDelegate.startPollersIfNeeded()
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
|
|
// Onion request path countries cache
|
|
DispatchQueue.global(qos: .utility).sync {
|
|
let _ = IP2Country.shared.populateCacheIfNeeded()
|
|
}
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
startObservingChanges()
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
self.viewHasAppeared = true
|
|
self.autoLoadNextPageIfNeeded()
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
stopObservingChanges()
|
|
}
|
|
|
|
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
|
startObservingChanges(didReturnFromBackground: true)
|
|
}
|
|
|
|
@objc func applicationDidResignActive(_ notification: Notification) {
|
|
stopObservingChanges()
|
|
}
|
|
|
|
// MARK: - Updating
|
|
|
|
private func startObservingChanges(didReturnFromBackground: Bool = false) {
|
|
// Start observing for data changes
|
|
dataChangeObservable = Storage.shared.start(
|
|
viewModel.observableState,
|
|
// If we haven't done the initial load the trigger it immediately (blocking the main
|
|
// thread so we remain on the launch screen until it completes to be consistent with
|
|
// the old behaviour)
|
|
scheduling: (hasLoadedInitialStateData ?
|
|
.async(onQueue: .main) :
|
|
.immediate
|
|
),
|
|
onError: { _ in },
|
|
onChange: { [weak self] state in
|
|
// The default scheduler emits changes on the main thread
|
|
self?.handleUpdates(state)
|
|
}
|
|
)
|
|
|
|
self.viewModel.onThreadChange = { [weak self] updatedThreadData in
|
|
self?.handleThreadUpdates(updatedThreadData)
|
|
}
|
|
|
|
// Note: When returning from the background we could have received notifications but the
|
|
// PagedDatabaseObserver won't have them so we need to force a re-fetch of the current
|
|
// data to ensure everything is up to date
|
|
if didReturnFromBackground {
|
|
self.viewModel.pagedDataObserver?.reload()
|
|
}
|
|
}
|
|
|
|
private func stopObservingChanges() {
|
|
// Stop observing database changes
|
|
dataChangeObservable?.cancel()
|
|
self.viewModel.onThreadChange = nil
|
|
}
|
|
|
|
private func handleUpdates(_ updatedState: HomeViewModel.State, initialLoad: Bool = false) {
|
|
// Ensure the first load runs without animations (if we don't do this the cells will animate
|
|
// in from a frame of CGRect.zero)
|
|
guard hasLoadedInitialStateData else {
|
|
hasLoadedInitialStateData = true
|
|
UIView.performWithoutAnimation { handleUpdates(updatedState, initialLoad: true) }
|
|
return
|
|
}
|
|
|
|
if updatedState.userProfile != self.viewModel.state.userProfile {
|
|
updateNavBarButtons()
|
|
}
|
|
|
|
// Update the 'view seed' UI
|
|
if updatedState.showViewedSeedBanner != self.viewModel.state.showViewedSeedBanner {
|
|
tableViewTopConstraint.isActive = false
|
|
seedReminderView.isHidden = !updatedState.showViewedSeedBanner
|
|
|
|
if updatedState.showViewedSeedBanner {
|
|
tableViewTopConstraint = tableView.pin(.top, to: .bottom, of: seedReminderView)
|
|
}
|
|
else {
|
|
tableViewTopConstraint = tableView.pin(.top, to: .top, of: view, withInset: Values.smallSpacing)
|
|
}
|
|
}
|
|
|
|
self.viewModel.updateState(updatedState)
|
|
}
|
|
|
|
private func handleThreadUpdates(_ updatedData: [HomeViewModel.SectionModel], initialLoad: Bool = false) {
|
|
// Ensure the first load runs without animations (if we don't do this the cells will animate
|
|
// in from a frame of CGRect.zero)
|
|
guard hasLoadedInitialThreadData else {
|
|
hasLoadedInitialThreadData = true
|
|
UIView.performWithoutAnimation { handleThreadUpdates(updatedData, initialLoad: true) }
|
|
return
|
|
}
|
|
|
|
// Hide the 'loading conversations' label (now that we have received conversation data)
|
|
loadingConversationsLabel.isHidden = true
|
|
|
|
// Show the empty state if there is no data
|
|
emptyStateView.isHidden = (
|
|
!updatedData.isEmpty &&
|
|
updatedData.contains(where: { !$0.elements.isEmpty })
|
|
)
|
|
|
|
CATransaction.begin()
|
|
CATransaction.setCompletionBlock { [weak self] in
|
|
// Complete page loading
|
|
self?.isLoadingMore = false
|
|
self?.autoLoadNextPageIfNeeded()
|
|
}
|
|
|
|
// Reload the table content (animate changes after the first load)
|
|
tableView.reload(
|
|
using: StagedChangeset(source: viewModel.threadData, target: updatedData),
|
|
deleteSectionsAnimation: .none,
|
|
insertSectionsAnimation: .none,
|
|
reloadSectionsAnimation: .none,
|
|
deleteRowsAnimation: .bottom,
|
|
insertRowsAnimation: .none,
|
|
reloadRowsAnimation: .none,
|
|
interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues
|
|
) { [weak self] updatedData in
|
|
self?.viewModel.updateThreadData(updatedData)
|
|
}
|
|
|
|
CATransaction.commit()
|
|
}
|
|
|
|
private func autoLoadNextPageIfNeeded() {
|
|
guard !self.isAutoLoadingNextPage && !self.isLoadingMore else { return }
|
|
|
|
self.isAutoLoadingNextPage = true
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + PagedData.autoLoadNextPageDelay) { [weak self] in
|
|
self?.isAutoLoadingNextPage = false
|
|
|
|
// Note: We sort the headers as we want to prioritise loading newer pages over older ones
|
|
let sections: [(HomeViewModel.Section, CGRect)] = (self?.viewModel.threadData
|
|
.enumerated()
|
|
.map { index, section in (section.model, (self?.tableView.rectForHeader(inSection: index) ?? .zero)) })
|
|
.defaulting(to: [])
|
|
let shouldLoadMore: Bool = sections
|
|
.contains { section, headerRect in
|
|
section == .loadMore &&
|
|
headerRect != .zero &&
|
|
(self?.tableView.bounds.contains(headerRect) == true)
|
|
}
|
|
|
|
guard shouldLoadMore else { return }
|
|
|
|
self?.isLoadingMore = true
|
|
|
|
DispatchQueue.global(qos: .default).async { [weak self] in
|
|
self?.viewModel.pagedDataObserver?.load(.pageAfter)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func updateNavBarButtons() {
|
|
// Profile picture view
|
|
let profilePictureSize = Values.verySmallProfilePictureSize
|
|
let profilePictureView = ProfilePictureView()
|
|
profilePictureView.accessibilityLabel = "Settings button"
|
|
profilePictureView.size = profilePictureSize
|
|
profilePictureView.update(
|
|
publicKey: getUserHexEncodedPublicKey(),
|
|
profile: Profile.fetchOrCreateCurrentUser(),
|
|
threadVariant: .contact
|
|
)
|
|
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: - UITableViewDataSource
|
|
|
|
func numberOfSections(in tableView: UITableView) -> Int {
|
|
return viewModel.threadData.count
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
let section: HomeViewModel.SectionModel = viewModel.threadData[section]
|
|
|
|
return section.elements.count
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
let section: HomeViewModel.SectionModel = viewModel.threadData[indexPath.section]
|
|
|
|
switch section.model {
|
|
case .messageRequests:
|
|
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
|
let cell: MessageRequestsCell = tableView.dequeue(type: MessageRequestsCell.self, for: indexPath)
|
|
cell.update(with: Int(threadViewModel.threadUnreadCount ?? 0))
|
|
return cell
|
|
|
|
case .threads:
|
|
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
|
let cell: FullConversationCell = tableView.dequeue(type: FullConversationCell.self, for: indexPath)
|
|
cell.update(with: threadViewModel)
|
|
return cell
|
|
|
|
default: preconditionFailure("Other sections should have no content")
|
|
}
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
|
let section: HomeViewModel.SectionModel = viewModel.threadData[section]
|
|
|
|
switch section.model {
|
|
case .loadMore:
|
|
let loadingIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium)
|
|
loadingIndicator.tintColor = Colors.text
|
|
loadingIndicator.alpha = 0.5
|
|
loadingIndicator.startAnimating()
|
|
|
|
let view: UIView = UIView()
|
|
view.addSubview(loadingIndicator)
|
|
loadingIndicator.center(in: view)
|
|
|
|
return view
|
|
|
|
default: return nil
|
|
}
|
|
}
|
|
|
|
// MARK: - UITableViewDelegate
|
|
|
|
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
|
let section: HomeViewModel.SectionModel = viewModel.threadData[section]
|
|
|
|
switch section.model {
|
|
case .loadMore: return HomeVC.loadingHeaderHeight
|
|
default: return 0
|
|
}
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
|
|
guard self.hasLoadedInitialThreadData && self.viewHasAppeared && !self.isLoadingMore else { return }
|
|
|
|
let section: HomeViewModel.SectionModel = self.viewModel.threadData[section]
|
|
|
|
switch section.model {
|
|
case .loadMore:
|
|
self.isLoadingMore = true
|
|
|
|
DispatchQueue.global(qos: .default).async { [weak self] in
|
|
self?.viewModel.pagedDataObserver?.load(.pageAfter)
|
|
}
|
|
|
|
default: break
|
|
}
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
tableView.deselectRow(at: indexPath, animated: true)
|
|
|
|
let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section]
|
|
|
|
switch section.model {
|
|
case .messageRequests:
|
|
let viewController: MessageRequestsViewController = MessageRequestsViewController()
|
|
self.navigationController?.pushViewController(viewController, animated: true)
|
|
|
|
case .threads:
|
|
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
|
show(
|
|
threadViewModel.threadId,
|
|
variant: threadViewModel.threadVariant,
|
|
isMessageRequest: (threadViewModel.threadIsMessageRequest == true),
|
|
with: .none,
|
|
focusedInteractionId: nil,
|
|
animated: true
|
|
)
|
|
|
|
default: break
|
|
}
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
|
return true
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
|
|
let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section]
|
|
|
|
switch section.model {
|
|
case .messageRequests:
|
|
let hide = UITableViewRowAction(style: .destructive, title: "TXT_HIDE_TITLE".localized()) { _, _ in
|
|
Storage.shared.write { db in db[.hasHiddenMessageRequests] = true }
|
|
}
|
|
hide.backgroundColor = Colors.destructive
|
|
|
|
return [hide]
|
|
|
|
case .threads:
|
|
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
|
let delete: UITableViewRowAction = UITableViewRowAction(
|
|
style: .destructive,
|
|
title: "TXT_DELETE_TITLE".localized()
|
|
) { [weak self] _, _ in
|
|
let message = (threadViewModel.currentUserIsClosedGroupAdmin == true ?
|
|
"admin_group_leave_warning".localized() :
|
|
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE".localized()
|
|
)
|
|
|
|
let alert = UIAlertController(
|
|
title: "CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE".localized(),
|
|
message: message,
|
|
preferredStyle: .alert
|
|
)
|
|
alert.addAction(UIAlertAction(
|
|
title: "TXT_DELETE_TITLE".localized(),
|
|
style: .destructive
|
|
) { _ in
|
|
Storage.shared.writeAsync { db in
|
|
switch threadViewModel.threadVariant {
|
|
case .closedGroup:
|
|
try MessageSender
|
|
.leave(db, groupPublicKey: threadViewModel.threadId)
|
|
.retainUntilComplete()
|
|
|
|
case .openGroup:
|
|
OpenGroupManager.shared.delete(db, openGroupId: threadViewModel.threadId)
|
|
|
|
default: break
|
|
}
|
|
|
|
_ = try SessionThread
|
|
.filter(id: threadViewModel.threadId)
|
|
.deleteAll(db)
|
|
}
|
|
})
|
|
alert.addAction(UIAlertAction(
|
|
title: "TXT_CANCEL_TITLE".localized(),
|
|
style: .default
|
|
))
|
|
|
|
self?.present(alert, animated: true, completion: nil)
|
|
}
|
|
delete.backgroundColor = Colors.destructive
|
|
|
|
let pin: UITableViewRowAction = UITableViewRowAction(
|
|
style: .normal,
|
|
title: (threadViewModel.threadIsPinned ?
|
|
"UNPIN_BUTTON_TEXT".localized() :
|
|
"PIN_BUTTON_TEXT".localized()
|
|
)
|
|
) { _, _ in
|
|
Storage.shared.writeAsync { db in
|
|
try SessionThread
|
|
.filter(id: threadViewModel.threadId)
|
|
.updateAll(db, SessionThread.Columns.isPinned.set(to: !threadViewModel.threadIsPinned))
|
|
}
|
|
}
|
|
|
|
guard threadViewModel.threadVariant == .contact && !threadViewModel.threadIsNoteToSelf else {
|
|
return [ delete, pin ]
|
|
}
|
|
|
|
let block: UITableViewRowAction = UITableViewRowAction(
|
|
style: .normal,
|
|
title: (threadViewModel.threadIsBlocked == true ?
|
|
"BLOCK_LIST_UNBLOCK_BUTTON".localized() :
|
|
"BLOCK_LIST_BLOCK_BUTTON".localized()
|
|
)
|
|
) { _, _ in
|
|
Storage.shared.writeAsync { db in
|
|
try Contact
|
|
.filter(id: threadViewModel.threadId)
|
|
.updateAll(
|
|
db,
|
|
Contact.Columns.isBlocked.set(
|
|
to: (threadViewModel.threadIsBlocked == false ?
|
|
true:
|
|
false
|
|
)
|
|
)
|
|
)
|
|
|
|
try MessageSender.syncConfiguration(db, forceSyncNow: true)
|
|
.retainUntilComplete()
|
|
}
|
|
}
|
|
block.backgroundColor = Colors.unimportant
|
|
|
|
return [ delete, block, pin ]
|
|
|
|
default: return []
|
|
}
|
|
}
|
|
|
|
// MARK: - Interaction
|
|
|
|
func handleContinueButtonTapped(from seedReminderView: SeedReminderView) {
|
|
let seedVC = SeedVC()
|
|
let navigationController = OWSNavigationController(rootViewController: seedVC)
|
|
present(navigationController, animated: true, completion: nil)
|
|
}
|
|
|
|
func show(
|
|
_ threadId: String,
|
|
variant: SessionThread.Variant,
|
|
isMessageRequest: Bool,
|
|
with action: ConversationViewModel.Action,
|
|
focusedInteractionId: Int64?,
|
|
animated: Bool
|
|
) {
|
|
if let presentedVC = self.presentedViewController {
|
|
presentedVC.dismiss(animated: false, completion: nil)
|
|
}
|
|
|
|
let finalViewControllers: [UIViewController] = [
|
|
self,
|
|
(isMessageRequest ? MessageRequestsViewController() : nil),
|
|
ConversationVC(
|
|
threadId: threadId,
|
|
threadVariant: variant,
|
|
focusedInteractionId: focusedInteractionId
|
|
)
|
|
].compactMap { $0 }
|
|
|
|
self.navigationController?.setViewControllers(finalViewControllers, animated: animated)
|
|
}
|
|
|
|
@objc private func openSettings() {
|
|
let settingsVC = SettingsVC()
|
|
let navigationController = OWSNavigationController(rootViewController: settingsVC)
|
|
navigationController.modalPresentationStyle = .fullScreen
|
|
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 = JoinOpenGroupVC()
|
|
let navigationController: OWSNavigationController = OWSNavigationController(rootViewController: joinOpenGroupVC)
|
|
|
|
if UIDevice.current.isIPad {
|
|
navigationController.modalPresentationStyle = .fullScreen
|
|
}
|
|
|
|
present(navigationController, animated: true, completion: nil)
|
|
}
|
|
|
|
@objc func createNewDM() {
|
|
let newDMVC = NewDMVC()
|
|
let navigationController = OWSNavigationController(rootViewController: newDMVC)
|
|
if UIDevice.current.isIPad {
|
|
navigationController.modalPresentationStyle = .fullScreen
|
|
}
|
|
present(navigationController, animated: true, completion: nil)
|
|
}
|
|
|
|
@objc(createNewDMFromDeepLink:)
|
|
func createNewDMFromDeepLink(sessionID: String) {
|
|
let newDMVC = NewDMVC(sessionID: sessionID)
|
|
let navigationController = OWSNavigationController(rootViewController: newDMVC)
|
|
if UIDevice.current.isIPad {
|
|
navigationController.modalPresentationStyle = .fullScreen
|
|
}
|
|
present(navigationController, animated: true, completion: nil)
|
|
}
|
|
|
|
@objc func createClosedGroup() {
|
|
let newClosedGroupVC = NewClosedGroupVC()
|
|
let navigationController = OWSNavigationController(rootViewController: newClosedGroupVC)
|
|
if UIDevice.current.isIPad {
|
|
navigationController.modalPresentationStyle = .fullScreen
|
|
}
|
|
present(navigationController, animated: true, completion: nil)
|
|
}
|
|
}
|