diff --git a/Package.swift b/Package.swift index c9b7a2e02b33d1bd9f333da8ea79efa363bfd87d..f654545cea2f13c6c8e6ce89cb9c9a5e942e5ff2 100644 --- a/Package.swift +++ b/Package.swift @@ -190,7 +190,10 @@ let package = Package( name: "CrashReporting" ), .target( - name: "NetworkMonitor" + name: "NetworkMonitor", + dependencies: [ + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), + ] ), .target( name: "VersionChecking" @@ -452,6 +455,7 @@ let package = Package( .product(name: "ChatLayout", package: "ChatLayout"), .product(name: "DifferenceKit", package: "DifferenceKit"), .product(name: "ScrollViewController", package: "ScrollViewController"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), ] ), .testTarget( @@ -631,6 +635,7 @@ let package = Package( .target(name: "DrawerFeature"), .target(name: "ReportingFeature"), .target(name: "DependencyInjection"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), ] ), .target( diff --git a/Sources/ChatFeature/Controllers/SingleChatController.swift b/Sources/ChatFeature/Controllers/SingleChatController.swift index 2028fc2de02497b066e8181bdf6912109973a06f..5bded7c91dd9550d65fa300fbf7cc61b986cae9a 100644 --- a/Sources/ChatFeature/Controllers/SingleChatController.swift +++ b/Sources/ChatFeature/Controllers/SingleChatController.swift @@ -135,6 +135,7 @@ public final class SingleChatController: UIViewController { setupBindings() KeyboardListener.shared.add(delegate: self) + screenView.bringSubviewToFront(screenView.snackBar) } // MARK: Private @@ -532,13 +533,6 @@ extension SingleChatController: KeyboardListenerDelegate { func keyboardWillChangeFrame(info: KeyboardInfo) { let keyWindow = UIApplication.shared.windows.filter { $0.isKeyWindow }.first -// let keyWindow: UIWindow? = UIApplication.shared.connectedScenes -// .filter { $0.activationState == .foregroundActive } -// .compactMap { $0 as? UIWindowScene } -// .first? -// .windows -// .first(where: \.isKeyWindow) - guard let keyWindow = keyWindow else { fatalError("[keyboardWillChangeFrame]: Couldn't get key window") } diff --git a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift index 157b03b1c1d30c3602acf1163f7d2b3e5d3273fc..958634b6b678c86f021f66fad37c17784f46295d 100644 --- a/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift +++ b/Sources/ChatFeature/ViewModels/SingleChatViewModel.swift @@ -14,9 +14,11 @@ import DifferenceKit import ReportingFeature import DependencyInjection import XXMessengerClient +import XXClient import struct XXModels.Message import struct XXModels.FileTransfer +import NetworkMonitor enum SingleChatNavigationRoutes: Equatable { case none @@ -36,6 +38,7 @@ final class SingleChatViewModel: NSObject { @Dependency var messenger: Messenger @Dependency var permissions: PermissionHandling @Dependency var toastController: ToastController + @Dependency var networkMonitor: NetworkMonitoring @Dependency var transferManager: XXClient.FileTransfer @KeyObject(.username, defaultValue: nil) var username: String? @@ -49,14 +52,19 @@ final class SingleChatViewModel: NSObject { private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, Message>], Never>([]) private let reportPopupSubject = PassthroughSubject<Void, Never>() + private var healthCancellable: XXClient.Cancellable? + var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) var isOnline: AnyPublisher<Bool, Never> { - // TO REFACTOR: - Just(.init(true)).eraseToAnyPublisher() + networkMonitor + .statusPublisher + .map { $0 == .available } + .eraseToAnyPublisher() } + var myId: Data { try! messenger.e2e.get()!.getContact().getId() } @@ -116,6 +124,11 @@ final class SingleChatViewModel: NSObject { }.receive(on: DispatchQueue.main) .sink { [unowned self] in sectionsRelay.send($0) } .store(in: &cancellables) + + healthCancellable = messenger.cMix.get()!.addHealthCallback(.init(handle: { [weak self] in + guard let self = self else { return } + self.networkMonitor.update($0) + })) } // MARK: Public @@ -316,50 +329,54 @@ final class SingleChatViewModel: NSObject { replyMessageId: stagedReply?.messageId ) - do { - message = try database.saveMessage(message) - - let report = try messenger.e2e.get()!.send( - messageType: 2, - recipientId: contact.id, - payload: Payload(text: message.text, reply: stagedReply).asData(), - e2eParams: GetE2EParams.liveDefault() - ) - - try messenger.cMix.get()!.waitForRoundResult( - roundList: try report.encode(), - timeoutMS: 5_000, - callback: .init(handle: { - switch $0 { - case .delivered: - message.status = .sent - _ = try? self.database.saveMessage(message) - - case .notDelivered(timedOut: let timedOut): - if timedOut { - message.status = .sendingTimedOut - } else { - message.status = .sendingFailed + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + + do { + message = try self.database.saveMessage(message) + + let report = try self.messenger.e2e.get()!.send( + messageType: 2, + recipientId: self.contact.id, + payload: Payload(text: message.text, reply: self.stagedReply).asData(), + e2eParams: GetE2EParams.liveDefault() + ) + + try self.messenger.cMix.get()!.waitForRoundResult( + roundList: try report.encode(), + timeoutMS: 5_000, + callback: .init(handle: { + switch $0 { + case .delivered: + message.status = .sent + _ = try? self.database.saveMessage(message) + + case .notDelivered(timedOut: let timedOut): + if timedOut { + message.status = .sendingTimedOut + } else { + message.status = .sendingFailed + } + + _ = try? self.database.saveMessage(message) } + }) + ) - _ = try? self.database.saveMessage(message) - } - }) - ) + message.networkId = report.messageId + if let timestamp = report.timestamp { + message.date = Date.fromTimestamp(Int(timestamp)) + } - message.networkId = report.messageId - if let timestamp = report.timestamp { - message.date = Date.fromTimestamp(Int(timestamp)) + message = try self.database.saveMessage(message) + } catch { + print(error.localizedDescription) + message.status = .sendingFailed + _ = try? self.database.saveMessage(message) } - message = try database.saveMessage(message) - } catch { - print(error.localizedDescription) - message.status = .sendingFailed - _ = try? database.saveMessage(message) + self.stagedReply = nil } - - stagedReply = nil } func didRequestReply(_ message: Message) { diff --git a/Sources/ChatFeature/Views/ChatView.swift b/Sources/ChatFeature/Views/ChatView.swift index ee3c7d98cdf0e96266cd110a41ac6dc8f8733f9d..0d3529709d1b646a43c3d3fa5b41cdf05d016821 100644 --- a/Sources/ChatFeature/Views/ChatView.swift +++ b/Sources/ChatFeature/Views/ChatView.swift @@ -28,10 +28,10 @@ final class ChatView: UIView { networkIssueInvisibleConstraint?.isActive = true snackBar.translatesAutoresizingMaskIntoConstraints = false - titleLabel.snp.makeConstraints { make in - make.top.equalTo(safeAreaLayoutGuide).offset(45) - make.left.equalToSuperview().offset(48) - make.right.equalToSuperview().offset(-61) + titleLabel.snp.makeConstraints { + $0.top.equalTo(safeAreaLayoutGuide).offset(45) + $0.left.equalToSuperview().offset(48) + $0.right.equalToSuperview().offset(-61) } } diff --git a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift index dbe20f1b03bc34c26f5cffc8492d30006f776efd..c01169dd2144bbcbaaea549952c3a54eb19dc2b0 100644 --- a/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift +++ b/Sources/ChatListFeature/ViewModel/ChatListViewModel.swift @@ -50,6 +50,7 @@ final class ChatListViewModel { var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> { let query = Contact.Query( + authStatus: [.friend], isRecent: true, isBlocked: reportingStatus.isEnabled() ? false : nil, isBanned: reportingStatus.isEnabled() ? false : nil diff --git a/Sources/Defaults/KeyObject.swift b/Sources/Defaults/KeyObject.swift index 1effae9ca395716e826b9f5864739cbeb92ddab6..0ade4e83639f54a5181b4292936a2d9dee049f60 100644 --- a/Sources/Defaults/KeyObject.swift +++ b/Sources/Defaults/KeyObject.swift @@ -39,6 +39,7 @@ public enum Key: String { case crashReporting case icognitoKeyboard + case dummyTrafficOn case askedDummyTrafficOnce } diff --git a/Sources/LaunchFeature/LaunchViewModel.swift b/Sources/LaunchFeature/LaunchViewModel.swift index 6c27c1de9b35b4b3f2fe55d8eca4948e984848c8..12b722f466eecc8c5310fc3f943679621ff45a1d 100644 --- a/Sources/LaunchFeature/LaunchViewModel.swift +++ b/Sources/LaunchFeature/LaunchViewModel.swift @@ -21,6 +21,7 @@ import class XXClient.Cancellable import XXDatabase import XXLegacyDatabaseMigrator import XXMessengerClient +import NetworkMonitor struct Update { let content: String @@ -44,11 +45,13 @@ final class LaunchViewModel { @Dependency var reportingStatus: ReportingStatus @Dependency var toastController: ToastController @Dependency var keychainHandler: KeychainHandling + @Dependency var networkMonitor: NetworkMonitoring @Dependency var processBannedList: ProcessBannedList @Dependency var permissionHandler: PermissionHandling @KeyObject(.username, defaultValue: nil) var username: String? @KeyObject(.biometrics, defaultValue: false) var isBiometricsOn: Bool + @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn: Bool var hudPublisher: AnyPublisher<HUDStatus, Never> { hudSubject.eraseToAnyPublisher() @@ -153,7 +156,25 @@ final class LaunchViewModel { senderId: nil, messageType: 2, callback: .init(handle: { - print(">>> \(String(data: $0.payload, encoding: .utf8))") + // let roundId = $0.roundId + + guard let payload = try? Payload(with: $0.payload) else { + fatalError("Couldn't decode payload: \(String(data: $0.payload, encoding: .utf8) ?? "nil")") + } + + try! self.database.saveMessage(.init( + networkId: $0.id, + senderId: $0.sender, + recipientId: messenger.e2e.get()!.getContact().getId(), + groupId: nil, + date: Date.fromTimestamp($0.timestamp), + status: .received, + isUnread: true, + text: payload.text, + replyMessageId: payload.reply?.messageId, + roundURL: "https://www.google.com.br", + fileTransferId: nil + )) }) ) @@ -161,6 +182,8 @@ final class LaunchViewModel { try generateTrafficManager(messenger: messenger) try generateTransferManager(messenger: messenger) + networkMonitor.start() + if messenger.isLoggedIn() == false { if try messenger.isRegistered() == false { hudSubject.send(.none) @@ -452,6 +475,7 @@ extension LaunchViewModel { ) DependencyInjection.Container.shared.register(manager) + try! manager.setStatus(dummyTrafficOn) } } @@ -551,12 +575,14 @@ extension LaunchViewModel { return } - let leaderId = try! group.getMembership() // This is all users on the group, the 1st is the leader/creator. + guard let members = try? group.getMembership(), let leader = members.first else { + fatalError("Failed to get group membership/leader") + } try! database.saveGroup(.init( id: group.getId(), name: String(data: group.getName(), encoding: .utf8)!, - leaderId: leaderId, + leaderId: leader.id, createdAt: Date.fromTimestamp(Int(group.getCreatedMS())), authStatus: .pending, serialized: group.serialize() @@ -564,7 +590,7 @@ extension LaunchViewModel { if let initialMessage = String(data: group.getInitMessage(), encoding: .utf8) { try! database.saveMessage(.init( - senderId: leaderId, + senderId: leader.id, recipientId: nil, groupId: group.getId(), date: Date.fromTimestamp(Int(group.getCreatedMS())), diff --git a/Sources/NetworkMonitor/NetworkMonitor.swift b/Sources/NetworkMonitor/NetworkMonitor.swift index ab805ca524e337bc8961c9dac719ac79d74f72f2..84899d7eb063acaab0f862fbb1b0031fa0a32d37 100644 --- a/Sources/NetworkMonitor/NetworkMonitor.swift +++ b/Sources/NetworkMonitor/NetworkMonitor.swift @@ -2,6 +2,7 @@ import Network import Combine +import XXClient import Foundation public enum NetworkStatus: Equatable { diff --git a/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift b/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift index d28de8f783f059455082177effb0639a463f51f3..ad307df16619eea7fb9207bd2718198f3f314bf2 100644 --- a/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift +++ b/Sources/ScanFeature/ViewModels/ScanDisplayViewModel.swift @@ -72,10 +72,10 @@ final class ScanDisplayViewModel { } let e2e = messenger.e2e.get()! - let contactData = e2e.getContact().data - let wrappedContact = try! SetFactsOnContact.live(contactData: contactData, facts: facts) + var contact = e2e.getContact() + try! contact.setFacts(facts) - filter.setValue(wrappedContact, forKey: "inputMessage") + filter.setValue(contact.data, forKey: "inputMessage") let transform = CGAffineTransform(scaleX: 5, y: 5) if let output = filter.outputImage?.transformed(by: transform) { diff --git a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift index 9ca3794c922d0ff49e24e3a6db450c57fbd853ea..c3f0f59efd5eb3b2400c8e4f959608f72ab01a75 100644 --- a/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchContainerViewModel.swift @@ -9,6 +9,7 @@ final class SearchContainerViewModel { @Dependency var pushHandler: PushHandling @Dependency var dummyTrafficManager: DummyTraffic + @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications @KeyObject(.askedDummyTrafficOnce, defaultValue: false) var offeredCoverTraffic @@ -25,6 +26,7 @@ final class SearchContainerViewModel { func didEnableCoverTraffic() { try! dummyTrafficManager.setStatus(true) + dummyTrafficOn = dummyTrafficManager.getStatus() } private func verifyCoverTraffic() { diff --git a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift index c5ec4513510b9e37cf82cfa0ed8314984984c905..a8f0135d85ec7c76ac332b1a5cfbdbd8ce850a94 100644 --- a/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift +++ b/Sources/SearchFeature/ViewModels/SearchLeftViewModel.swift @@ -9,6 +9,7 @@ import XXClient import Defaults import Countries import CustomDump +import ToastFeature import NetworkMonitor import ReportingFeature import CombineSchedulers @@ -28,6 +29,7 @@ final class SearchLeftViewModel { @Dependency var database: Database @Dependency var messenger: Messenger @Dependency var reportingStatus: ReportingStatus + @Dependency var toastController: ToastController @Dependency var networkMonitor: NetworkMonitoring @KeyObject(.username, defaultValue: nil) var username: String? @@ -208,6 +210,7 @@ final class SearchLeftViewModel { contact = try self.database.saveContact(contact) self.hudSubject.send(.none) + self.presentSuccessToast(for: contact, resent: true) } catch { contact.authStatus = .requestFailed _ = try? self.database.saveContact(contact) @@ -242,6 +245,7 @@ final class SearchLeftViewModel { self.hudSubject.send(.none) self.successSubject.send(contact) + self.presentSuccessToast(for: contact, resent: false) } catch { contact.authStatus = .requestFailed _ = try? self.database.saveContact(contact) @@ -299,4 +303,15 @@ final class SearchLeftViewModel { private func removeMyself(from collection: [XXModels.Contact]) -> [XXModels.Contact]? { collection.filter { $0.id != myId } } + + private func presentSuccessToast(for contact: XXModels.Contact, resent: Bool) { + let name = contact.nickname ?? contact.username + let sentTitle = Localized.Requests.Sent.Toast.sent(name ?? "") + let resentTitle = Localized.Requests.Sent.Toast.resent(name ?? "") + + toastController.enqueueToast(model: .init( + title: resent ? resentTitle : sentTitle, + leftImage: Asset.sharedSuccess.image + )) + } } diff --git a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift index ee5be65a72df17338ab3daca3ddcf58531057338..0f583b42aae6e0cc7fccbe2312cccd00e3e10403 100644 --- a/Sources/SettingsFeature/Controllers/AccountDeleteController.swift +++ b/Sources/SettingsFeature/Controllers/AccountDeleteController.swift @@ -51,7 +51,7 @@ public final class AccountDeleteController: UIViewController { } private func setupBindings() { - viewModel.hud + viewModel.hudPublisher .receive(on: DispatchQueue.main) .sink { [hud] in hud.update(with: $0) } .store(in: &cancellables) diff --git a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift index 96586dea576f39d472cc2ac41020fcdc3a4f79ee..94869e07e57ac1b060335dca964c43b722fe5e12 100644 --- a/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/AccountDeleteViewModel.swift @@ -1,46 +1,98 @@ import HUD +import Models import Combine import Defaults -import Foundation +import Keychain import XXClient +import Foundation import XXMessengerClient import DependencyInjection -import Models +import Retry final class AccountDeleteViewModel { @Dependency var messenger: Messenger + @Dependency var keychain: KeychainHandling @KeyObject(.username, defaultValue: nil) var username: String? - var deleting = false + var hudPublisher: AnyPublisher<HUDStatus, Never> { + hudSubject.eraseToAnyPublisher() + } - var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } - private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) + private var isCurrentlyDeleting = false + private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) func didTapDelete() { - guard deleting == false else { return } - deleting = true + guard isCurrentlyDeleting == false else { return } + isCurrentlyDeleting = true DispatchQueue.main.async { [weak self] in - self?.hudRelay.send(.on) + guard let self = self else { return } + self.hudSubject.send(.on) } - do { - let fact = Fact(fact: username!, type: FactType.username.rawValue) - try messenger.ud.get()!.permanentDeleteAccount(username: fact) - try messenger.destroy() - - DispatchQueue.main.async { [weak self] in - self?.hudRelay.send(.error(.init( - content: "Now kill the app and re-open", - title: "Account deleted", - dismissable: false - ))) + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + + do { + try self.cleanUD() + try self.stopNetwork() + try self.messenger.destroy() + try self.keychain.clear() + try self.deleteDatabase() + + UserDefaults.resetStandardUserDefaults() + UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) + UserDefaults.standard.synchronize() + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.hudSubject.send(.error(.init( + content: "Now kill the app and re-open", + title: "Account deleted", + dismissable: false + ))) + } + } catch { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.hudSubject.send(.error(.init(with: error))) + } } - } catch { - DispatchQueue.main.async { [weak self] in - self?.hudRelay.send(.error(.init(with: error))) + } + } + + private func cleanUD() throws { + let fact = Fact(fact: username!, type: FactType.username.rawValue) + + print(">>> Deleting my username (\(fact.fact)) from ud") + try messenger.ud.get()!.permanentDeleteAccount(username: fact) + } + + private func stopNetwork() throws { + let cMix = messenger.cMix.get()! + + print(">>> Stopping network follower...") + try cMix.stopNetworkFollower() + + retry(max: 10, retryStrategy: .delay(seconds: 2)) { + if cMix.networkFollowerStatus() != .stopped { + print(">>> Network still hasn't stopped. Its \(cMix.networkFollowerStatus())") + throw NSError.create("Gave up on stopping the network.") } + + print(">>> Network has stopped") } } + + private func deleteDatabase() throws { + print(">>> Deleting database...") + + let dbPath = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")! + .appendingPathComponent("xxm_database") + .appendingPathExtension("sqlite").path + + try FileManager.default.removeItem(atPath: dbPath) + } } diff --git a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift index b44c37d85ccbaa03cb46559dc40b42494b1ae174..bee9de2901e28b3b1f00750e6a92741e939313d0 100644 --- a/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift +++ b/Sources/SettingsFeature/ViewModels/SettingsViewModel.swift @@ -21,15 +21,16 @@ struct SettingsViewState: Equatable { } final class SettingsViewModel { + @Dependency var pushHandler: PushHandling + @Dependency var permissions: PermissionHandling @Dependency var dummyTrafficManager: DummyTraffic - @Dependency private var pushHandler: PushHandling - @Dependency private var permissions: PermissionHandling - @KeyObject(.biometrics, defaultValue: false) private var biometrics - @KeyObject(.hideAppList, defaultValue: false) private var hideAppList - @KeyObject(.icognitoKeyboard, defaultValue: false) private var icognitoKeyboard - @KeyObject(.pushNotifications, defaultValue: false) private var pushNotifications - @KeyObject(.inappnotifications, defaultValue: true) private var inAppNotifications + @KeyObject(.biometrics, defaultValue: false) var biometrics + @KeyObject(.hideAppList, defaultValue: false) var hideAppList + @KeyObject(.dummyTrafficOn, defaultValue: false) var dummyTrafficOn + @KeyObject(.icognitoKeyboard, defaultValue: false) var icognitoKeyboard + @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications + @KeyObject(.inappnotifications, defaultValue: true) var inAppNotifications var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler() @@ -66,6 +67,7 @@ final class SettingsViewModel { let currently = dummyTrafficManager.getStatus() try! dummyTrafficManager.setStatus(!currently) stateRelay.value.isDummyTrafficOn = !currently + dummyTrafficOn = stateRelay.value.isDummyTrafficOn } func didToggleHideActiveApps() {