mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			285 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			285 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | |
| 
 | |
| import Foundation
 | |
| import Combine
 | |
| import GRDB
 | |
| import DifferenceKit
 | |
| import SessionUIKit
 | |
| import SessionUtilitiesKit
 | |
| import SignalUtilitiesKit
 | |
| 
 | |
| class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTableSource, PagedObservationSource {
 | |
|     typealias TableItem = SessionThreadViewModel
 | |
|     typealias PagedTable = SessionThread
 | |
|     typealias PagedDataModel = SessionThreadViewModel
 | |
|     
 | |
|     // MARK: - Variables
 | |
|     
 | |
|     public static let pageSize: Int = (UIDevice.current.isIPad ? 20 : 15)
 | |
|     public let dependencies: Dependencies
 | |
|     public let state: TableDataState<Section, TableItem> = TableDataState()
 | |
|     public let observableState: ObservableTableSourceState<Section, SessionThreadViewModel> = ObservableTableSourceState()
 | |
|     public let navigatableState: NavigatableState = NavigatableState()
 | |
|     
 | |
|     // MARK: - Initialization
 | |
|     
 | |
|     init(using dependencies: Dependencies = Dependencies()) {
 | |
|         self.dependencies = dependencies
 | |
|         self.pagedDataObserver = nil
 | |
|         
 | |
|         // Note: Since this references self we need to finish initializing before setting it, we
 | |
|         // also want to skip the initial query and trigger it async so that the push animation
 | |
|         // doesn't stutter (it should load basically immediately but without this there is a
 | |
|         // distinct stutter)
 | |
|         let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
 | |
|         let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
 | |
|         self.pagedDataObserver = PagedDatabaseObserver(
 | |
|             pagedTable: SessionThread.self,
 | |
|             pageSize: MessageRequestsViewModel.pageSize,
 | |
|             idColumn: .id,
 | |
|             observedChanges: [
 | |
|                 PagedData.ObservedChanges(
 | |
|                     table: SessionThread.self,
 | |
|                     columns: [
 | |
|                         .id,
 | |
|                         .shouldBeVisible
 | |
|                     ]
 | |
|                 ),
 | |
|                 PagedData.ObservedChanges(
 | |
|                     table: Interaction.self,
 | |
|                     columns: [
 | |
|                         .body,
 | |
|                         .wasRead
 | |
|                     ],
 | |
|                     joinToPagedType: {
 | |
|                         let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
 | |
|                         
 | |
|                         return SQL("JOIN \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])")
 | |
|                     }()
 | |
|                 ),
 | |
|                 PagedData.ObservedChanges(
 | |
|                     table: Contact.self,
 | |
|                     columns: [.isBlocked],
 | |
|                     joinToPagedType: {
 | |
|                         let contact: TypedTableAlias<Contact> = TypedTableAlias()
 | |
|                         
 | |
|                         return SQL("JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])")
 | |
|                     }()
 | |
|                 ),
 | |
|                 PagedData.ObservedChanges(
 | |
|                     table: Profile.self,
 | |
|                     columns: [.name, .nickname, .profilePictureFileName],
 | |
|                     joinToPagedType: {
 | |
|                         let profile: TypedTableAlias<Profile> = TypedTableAlias()
 | |
|                         
 | |
|                         return SQL("JOIN \(Profile.self) ON \(profile[.id]) = \(thread[.id])")
 | |
|                     }()
 | |
|                 ),
 | |
|                 PagedData.ObservedChanges(
 | |
|                     table: RecipientState.self,
 | |
|                     columns: [.state],
 | |
|                     joinToPagedType: {
 | |
|                         let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
 | |
|                         let recipientState: TypedTableAlias<RecipientState> = TypedTableAlias()
 | |
|                         
 | |
|                         return """
 | |
|                             JOIN \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
 | |
|                             JOIN \(RecipientState.self) ON \(recipientState[.interactionId]) = \(interaction[.id])
 | |
|                         """
 | |
|                     }()
 | |
|                 )
 | |
|             ],
 | |
|             /// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query but differs
 | |
|             /// from the JOINs that are actually used for performance reasons as the basic logic can be simpler for where it's used
 | |
|             joinSQL: SessionThreadViewModel.optimisedJoinSQL,
 | |
|             filterSQL: SessionThreadViewModel.messageRequestsFilterSQL(userPublicKey: userPublicKey),
 | |
|             groupSQL: SessionThreadViewModel.groupSQL,
 | |
|             orderSQL: SessionThreadViewModel.messageRequetsOrderSQL,
 | |
|             dataQuery: SessionThreadViewModel.baseQuery(
 | |
|                 userPublicKey: userPublicKey,
 | |
|                 groupSQL: SessionThreadViewModel.groupSQL,
 | |
|                 orderSQL: SessionThreadViewModel.messageRequetsOrderSQL
 | |
|             ),
 | |
|             onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in
 | |
|                 PagedData.processAndTriggerUpdates(
 | |
|                     updatedData: self?.process(data: updatedData, for: updatedPageInfo),
 | |
|                     currentDataRetriever: { self?.tableData },
 | |
|                     valueSubject: self?.pendingTableDataSubject
 | |
|                 )
 | |
|             }
 | |
|         )
 | |
|         
 | |
|         // Run the initial query on a background thread so we don't block the push transition
 | |
|         DispatchQueue.global(qos: .userInitiated).async { [weak self] in
 | |
|             // The `.pageBefore` will query from a `0` offset loading the first page
 | |
|             self?.pagedDataObserver?.load(.pageBefore)
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     // MARK: - Section
 | |
|     
 | |
|     public enum Section: SessionTableSection {
 | |
|         case threads
 | |
|         case loadMore
 | |
|         
 | |
|         var style: SessionTableSectionStyle {
 | |
|             switch self {
 | |
|                 case .threads: return .none
 | |
|                 case .loadMore: return .loadMore
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     // MARK: - Content
 | |
|     
 | |
|     public let title: String = "MESSAGE_REQUESTS_TITLE".localized()
 | |
|     public let initialLoadMessage: String? = "LOADING_CONVERSATIONS".localized()
 | |
|     public let emptyStateTextPublisher: AnyPublisher<String?, Never> = Just("MESSAGE_REQUESTS_EMPTY_TEXT".localized())
 | |
|         .eraseToAnyPublisher()
 | |
|     public let cellType: SessionTableViewCellType = .fullConversation
 | |
|     public private(set) var pagedDataObserver: PagedDatabaseObserver<SessionThread, SessionThreadViewModel>?
 | |
|     
 | |
|     private func process(data: [SessionThreadViewModel], for pageInfo: PagedData.PageInfo) -> [SectionModel] {
 | |
|         let groupedOldData: [String: [SessionCell.Info<SessionThreadViewModel>]] = (self.tableData
 | |
|             .first(where: { $0.model == .threads })?
 | |
|             .elements)
 | |
|             .defaulting(to: [])
 | |
|             .grouped(by: \.id.threadId)
 | |
|         
 | |
|         return [
 | |
|             [
 | |
|                 SectionModel(
 | |
|                     section: .threads,
 | |
|                     elements: data
 | |
|                         .sorted { lhs, rhs -> Bool in lhs.lastInteractionDate > rhs.lastInteractionDate }
 | |
|                         .map { viewModel -> SessionCell.Info<SessionThreadViewModel> in
 | |
|                             SessionCell.Info(
 | |
|                                 id: viewModel.populatingCurrentUserBlindedKeys(
 | |
|                                     currentUserBlinded15PublicKeyForThisThread: groupedOldData[viewModel.threadId]?
 | |
|                                         .first?
 | |
|                                         .id
 | |
|                                         .currentUserBlinded15PublicKey,
 | |
|                                     currentUserBlinded25PublicKeyForThisThread: groupedOldData[viewModel.threadId]?
 | |
|                                         .first?
 | |
|                                         .id
 | |
|                                         .currentUserBlinded25PublicKey
 | |
|                                 ),
 | |
|                                 accessibility: Accessibility(
 | |
|                                     identifier: "Message request"
 | |
|                                 ),
 | |
|                                 onTap: { [weak self] in
 | |
|                                     let viewController: ConversationVC = ConversationVC(
 | |
|                                         threadId: viewModel.threadId,
 | |
|                                         threadVariant: viewModel.threadVariant
 | |
|                                     )
 | |
|                                     self?.transitionToScreen(viewController, transitionType: .push)
 | |
|                                 }
 | |
|                             )
 | |
|                         }
 | |
|                 )
 | |
|             ],
 | |
|             (!data.isEmpty && (pageInfo.pageOffset + pageInfo.currentCount) < pageInfo.totalCount ?
 | |
|                 [SectionModel(section: .loadMore)] :
 | |
|                 []
 | |
|             )
 | |
|         ].flatMap { $0 }
 | |
|     }
 | |
|     
 | |
|     lazy var footerButtonInfo: AnyPublisher<SessionButton.Info?, Never> = observableState
 | |
|         .pendingTableDataSubject
 | |
|         .map { [dependencies] (currentThreadData: [SectionModel], _: StagedChangeset<[SectionModel]>) in
 | |
|             let threadInfo: [(id: String, variant: SessionThread.Variant)] = (currentThreadData
 | |
|                 .first(where: { $0.model == .threads })?
 | |
|                 .elements
 | |
|                 .map { ($0.id.id, $0.id.threadVariant) })
 | |
|                 .defaulting(to: [])
 | |
|             
 | |
|             return SessionButton.Info(
 | |
|                 style: .destructive,
 | |
|                 title: "MESSAGE_REQUESTS_CLEAR_ALL".localized(),
 | |
|                 isEnabled: !threadInfo.isEmpty,
 | |
|                 accessibility: Accessibility(
 | |
|                     identifier: "Clear all"
 | |
|                 ),
 | |
|                 onTap: { [weak self] in
 | |
|                     let modal: ConfirmationModal = ConfirmationModal(
 | |
|                         info: ConfirmationModal.Info(
 | |
|                             title: "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE".localized(),
 | |
|                             accessibility: Accessibility(
 | |
|                                 identifier: "Clear all"
 | |
|                             ),
 | |
|                             confirmTitle: "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON".localized(),
 | |
|                             confirmAccessibility: Accessibility(
 | |
|                                 identifier: "Clear"
 | |
|                             ),
 | |
|                             confirmStyle: .danger,
 | |
|                             cancelStyle: .alert_text,
 | |
|                             onConfirm: { _ in
 | |
|                                 // Clear the requests
 | |
|                                 dependencies.storage.write { db in
 | |
|                                     // Remove the one-to-one requests
 | |
|                                     try SessionThread.deleteOrLeave(
 | |
|                                         db,
 | |
|                                         threadIds: threadInfo
 | |
|                                             .filter { _, variant in variant == .contact }
 | |
|                                             .map { id, _ in id },
 | |
|                                         threadVariant: .contact,
 | |
|                                         groupLeaveType: .silent,
 | |
|                                         calledFromConfigHandling: false
 | |
|                                     )
 | |
|                                     
 | |
|                                     // Remove the group requests
 | |
|                                     try SessionThread.deleteOrLeave(
 | |
|                                         db,
 | |
|                                         threadIds: threadInfo
 | |
|                                             .filter { _, variant in variant == .legacyGroup || variant == .group }
 | |
|                                             .map { id, _ in id },
 | |
|                                         threadVariant: .group,
 | |
|                                         groupLeaveType: .silent,
 | |
|                                         calledFromConfigHandling: false
 | |
|                                     )
 | |
|                                 }
 | |
|                             }
 | |
|                         )
 | |
|                     )
 | |
| 
 | |
|                     self?.transitionToScreen(modal, transitionType: .present)
 | |
|                 }
 | |
|             )
 | |
|         }
 | |
|         .eraseToAnyPublisher()
 | |
|     
 | |
|     // MARK: - Functions
 | |
|     
 | |
|     func canEditRow(at indexPath: IndexPath) -> Bool {
 | |
|         let section: SectionModel = tableData[indexPath.section]
 | |
|         
 | |
|         return (section.model == .threads)
 | |
|     }
 | |
|     
 | |
|     func trailingSwipeActionsConfiguration(forRowAt indexPath: IndexPath, in tableView: UITableView, of viewController: UIViewController) -> UISwipeActionsConfiguration? {
 | |
|         let section: SectionModel = tableData[indexPath.section]
 | |
|         
 | |
|         switch section.model {
 | |
|             case .threads:
 | |
|                 let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row].id
 | |
|                 
 | |
|                 return UIContextualAction.configuration(
 | |
|                     for: UIContextualAction.generateSwipeActions(
 | |
|                         [
 | |
|                             (threadViewModel.threadVariant != .contact ? nil : .block),
 | |
|                             .delete
 | |
|                         ].compactMap { $0 },
 | |
|                         for: .trailing,
 | |
|                         indexPath: indexPath,
 | |
|                         tableView: tableView,
 | |
|                         threadViewModel: threadViewModel,
 | |
|                         viewController: viewController
 | |
|                     )
 | |
|                 )
 | |
|                 
 | |
|             default: return nil
 | |
|         }
 | |
|     }
 | |
| }
 |