From 545497691b84999c00b4bfdcadefb072b1105953 Mon Sep 17 00:00:00 2001 From: Matthew Fennell Date: Mon, 12 Aug 2024 21:30:34 +0100 Subject: [PATCH] Listen to live updates to users and their devices We subscribe to updates to kMonalOmemoStateUpdated and kMonalMucParticipantsAndMembersUpdated, to listen to device and member updates, respectively. These subscriptions take place in a view model, so that they can be created separately from the lifecycles of the views themselves. Without this approach, device updates in group chats would not sync correctly since there would be a race condition between the view being created and the devices being fetched from the server. We also simplify the OmemoKeysForContact view a little, by passing in the devices directly. --- Monal/Classes/ContactDetails.swift | 2 +- Monal/Classes/OmemoKeysView.swift | 130 +++++++++++++++++++---------- Monal/Classes/SwiftuiHelpers.swift | 4 +- 3 files changed, 87 insertions(+), 49 deletions(-) diff --git a/Monal/Classes/ContactDetails.swift b/Monal/Classes/ContactDetails.swift index 3c21c3510..105a890fd 100644 --- a/Monal/Classes/ContactDetails.swift +++ b/Monal/Classes/ContactDetails.swift @@ -369,7 +369,7 @@ struct ContactDetails: View { #if !DISABLE_OMEMO if !HelperTools.isContactBlacklisted(forEncryption:contact.obj) && !contact.isSelfChat { if !contact.isGroup || contact.mucType == "group" { - NavigationLink(destination: LazyClosureView(OmemoKeysView(contact: contact))) { + NavigationLink(destination: LazyClosureView(OmemoKeysView(omemoKeys: OmemoKeysForChat(viewContact: contact)))) { Text("Encryption Keys") } } diff --git a/Monal/Classes/OmemoKeysView.swift b/Monal/Classes/OmemoKeysView.swift index c777b4dc3..f2d1de065 100644 --- a/Monal/Classes/OmemoKeysView.swift +++ b/Monal/Classes/OmemoKeysView.swift @@ -196,33 +196,30 @@ struct OmemoKeysEntryView: View { } struct OmemoKeysForContactView: View { - @State private var deviceIds: OrderedSet @State private var showDeleteKeyAlert = false @State private var selectedDeviceForDeletion : NSNumber + @ObservedObject private var devices: OmemoKeysForContact private var deviceId: NSNumber { return account.omemo.getDeviceId() } + private var deviceIds: OrderedSet { + return OrderedSet(devices.devices.sorted { $0.intValue < $1.intValue }) + } + private let contactJid: String private let account: xmpp private let ownKeys: Bool - init(contact: ObservableKVOWrapper, account: xmpp) { + init(contact: ObservableKVOWrapper, devices: OmemoKeysForContact) { + let account = (contact.account as xmpp?)! self.ownKeys = (account.connectionProperties.identity.jid == contact.obj.contactJid) self.contactJid = contact.obj.contactJid self.account = account - self.deviceIds = OmemoKeysForContactView.knownDevices(account: self.account, jid: self.contactJid) + self.devices = devices self.selectedDeviceForDeletion = -1 } - - private static func knownDevices(account: xmpp, jid: String) -> OrderedSet { - return OrderedSet(account.omemo.knownDevices(forAddressName: jid).sorted { return $0.intValue < $1.intValue }) - } - - private func refreshKnownDevices() -> Void { - self.deviceIds = OmemoKeysForContactView.knownDevices(account: self.account, jid: self.contactJid) - } func deleteButton(deviceId: NSNumber) -> some View { Button(action: { @@ -261,13 +258,6 @@ struct OmemoKeysForContactView: View { } } } - .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalOmemoStateUpdated")).receive(on: RunLoop.main)) { notification in - if notification.userInfo?["jid"] as? String == self.contactJid { - withAnimation() { - refreshKnownDevices() - } - } - } } } @@ -279,27 +269,29 @@ struct OmemoKeysForChatView: View { @State private var scannedJid : String = "" @State private var scannedFingerprints : Dictionary = [:] - @State private var contacts: OrderedSet> // contact list may change/be reloaded -> state - @State var selectedContact : ObservableKVOWrapper? // for reason why see start of body @State private var navigateToQRCodeView = false @State private var navigateToQRCodeScanner = false @State private var showScannedContactMissmatchAlert = false - init(contact: ObservableKVOWrapper?) { - self.account = nil - self.selectedContact = nil - self.contacts = getContactList(viewContact: contact) - self.viewContact = contact + @ObservedObject private var omemoKeys: OmemoKeysForChat - if let contact = contact { - if let account = contact.obj.account { - self.account = account - } + private var contacts: [(ObservableKVOWrapper, OmemoKeysForContact)] { + return omemoKeys.contacts.sorted { (entry1, entry2) -> Bool in + let entry1Jid: String = entry1.0.contactJid + let entry2Jid: String = entry2.0.contactJid + return entry1Jid < entry2Jid } } + init(omemoKeys: OmemoKeysForChat) { + self.account = omemoKeys.viewContact?.account + self.selectedContact = nil + self.viewContact = omemoKeys.viewContact + self.omemoKeys = omemoKeys + } + private func isOwnKeys() -> Bool { if let contact = self.viewContact, let account = self.account { let isGroup = contact.isGroup && contact.mucType == "group" @@ -351,14 +343,14 @@ struct OmemoKeysForChatView: View { Text("You should trust a key when you have verified it. Verify by comparing the key below to the one on your contact's screen. Double tap onto a fingerprint to copy to clipboard.") Section(header:helpDescription) { - if (self.contacts.count == 1) { - ForEach(self.contacts, id: \.self.obj) { contact in - OmemoKeysForContactView(contact: contact, account: self.account!) + if (omemoKeys.contacts.count == 1) { + ForEach(self.contacts, id: \.0) { contact, devices in + OmemoKeysForContactView(contact: contact, devices: devices) } } else { - ForEach(self.contacts, id: \.self.obj) { contact in + ForEach(self.contacts, id: \.0) { contact, devices in DisclosureGroup(content: { - OmemoKeysForContactView(contact: contact, account: self.account!) + OmemoKeysForContactView(contact: contact, devices: devices) }, label: { HStack { Text("Keys of \(contact.obj.contactJid)") @@ -386,7 +378,7 @@ struct OmemoKeysForChatView: View { Image(systemName: "camera.fill") }) }*/ - if(self.contacts.count == 1 && self.account != nil) { + if(omemoKeys.contacts.count == 1 && self.account != nil) { Button(action: { self.navigateToQRCodeView = true }, label: { @@ -399,7 +391,7 @@ struct OmemoKeysForChatView: View { .accentColor(monalGreen) .navigationBarTitle(isOwnKeys() ? Text("My Encryption Keys") : Text("Encryption Keys"), displayMode: .inline) .onAppear(perform: { - self.selectedContact = self.contacts.first // needs to be done here as first is nil in init + self.selectedContact = self.omemoKeys.contacts.keys.first // needs to be done here as first is nil in init }) .alert(isPresented: $showScannedContactMissmatchAlert) { Alert( @@ -410,26 +402,33 @@ struct OmemoKeysForChatView: View { resetTrustFromQR(scannedJid: self.scannedJid, scannedFingerprints: self.scannedFingerprints) self.scannedJid = "" self.scannedFingerprints = [:] - self.contacts = getContactList(viewContact: self.viewContact) // refresh all contacts because trust may have changed })) } } } struct OmemoKeysView: View { - private var viewContact: ObservableKVOWrapper? - private var account: xmpp? - @State private var contacts: OrderedSet> + @ObservedObject private var omemoKeys: OmemoKeysForChat + + init(omemoKeys: OmemoKeysForChat) { + self.omemoKeys = omemoKeys + } - init(contact: ObservableKVOWrapper?) { - self.viewContact = contact - self.account = contact?.account - self.contacts = getContactList(viewContact: contact) + private var viewContact: ObservableKVOWrapper? { + return omemoKeys.viewContact + } + + private var account: xmpp? { + return viewContact?.account + } + + private var contacts: Set> { + return Set(omemoKeys.contacts.keys) } var body: some View { if self.account != nil && !self.contacts.isEmpty { - OmemoKeysForChatView(contact: viewContact) + OmemoKeysForChatView(omemoKeys: omemoKeys) } else if self.contacts.isEmpty { ContentUnavailableShimView("No Contacts", systemImage: "person.2.slash", description: Text("Cannot display keys as there are no contacts to display keys for.")) } else if self.account == nil { @@ -438,9 +437,48 @@ struct OmemoKeysView: View { } } +class OmemoKeysForContact: ObservableObject { + @Published var devices: Set + + init(devices: Set) { + self.devices = devices + } +} + +class OmemoKeysForChat: ObservableObject { + var viewContact: ObservableKVOWrapper? + @Published var contacts: Dictionary, OmemoKeysForContact> + + init(viewContact: ObservableKVOWrapper?) { + self.viewContact = viewContact + self.contacts = OmemoKeysForChat.knownDevices(viewContact: self.viewContact) + NotificationCenter.default.addObserver(self, selector: #selector(updateContactDevices), name: NSNotification.Name("kMonalOmemoStateUpdated"), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateContactDevices), name: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated"), object: nil) + } + + @objc + private func updateContactDevices() -> Void { + withAnimation() { + self.contacts = OmemoKeysForChat.knownDevices(viewContact: self.viewContact) + } + } + + private static func knownDevices(viewContact: ObservableKVOWrapper?) -> Dictionary, OmemoKeysForContact> { + let contacts: OrderedSet> = getContactList(viewContact: viewContact) + let devices = contacts.map { ($0, devicesForContact(contact: $0)) } + return Dictionary(uniqueKeysWithValues: devices) + } + + private static func devicesForContact(contact: ObservableKVOWrapper) -> OmemoKeysForContact { + let account: xmpp = (contact.account as xmpp?)! + let devicesForContact: Set = account.omemo.knownDevices(forAddressName: contact.contactJid) + return OmemoKeysForContact(devices: devicesForContact) + } +} + struct OmemoKeys_Previews: PreviewProvider { static var previews: some View { // TODO some dummy views, requires a dummy xmpp obj - OmemoKeysView(contact:nil); + OmemoKeysView(omemoKeys: OmemoKeysForChat(viewContact: nil)); } } diff --git a/Monal/Classes/SwiftuiHelpers.swift b/Monal/Classes/SwiftuiHelpers.swift index fb8c3e62d..99ae5933d 100644 --- a/Monal/Classes/SwiftuiHelpers.swift +++ b/Monal/Classes/SwiftuiHelpers.swift @@ -655,9 +655,9 @@ class SwiftuiInterface : NSObject { func makeOwnOmemoKeyView(_ ownContact: MLContact?) -> UIViewController { let host = UIHostingController(rootView:AnyView(EmptyView())) if(ownContact == nil) { - host.rootView = AnyView(OmemoKeysView(contact: nil)) + host.rootView = AnyView(OmemoKeysView(omemoKeys: OmemoKeysForChat(viewContact: nil))) } else { - host.rootView = AnyView(OmemoKeysView(contact: ObservableKVOWrapper(ownContact!))) + host.rootView = AnyView(OmemoKeysView(omemoKeys: OmemoKeysForChat(viewContact: ObservableKVOWrapper(ownContact!)))) } return host }