From 6ee53a0e23a541e3be0619af518ff4e3550c4c86 Mon Sep 17 00:00:00 2001 From: Stefan Hofman Date: Fri, 19 Mar 2021 18:18:22 +0100 Subject: [PATCH 01/12] Initial commit Identity server support --- Nio.xcodeproj/project.pbxproj | 6 +++ Nio/Info.plist | 2 + Nio/Settings/SettingsView.swift | 39 +++++++++++++++++++ NioKit/Extensions/Contacts.swift | 42 +++++++++++++++++++++ NioKit/Session/AccountStore.swift | 15 ++++++++ NioShareExtension/ShareContentView.swift | 1 + NioShareExtension/ShareViewController.swift | 2 + 7 files changed, 107 insertions(+) create mode 100644 NioKit/Extensions/Contacts.swift diff --git a/Nio.xcodeproj/project.pbxproj b/Nio.xcodeproj/project.pbxproj index e5ed49eb..2571c1f1 100644 --- a/Nio.xcodeproj/project.pbxproj +++ b/Nio.xcodeproj/project.pbxproj @@ -56,6 +56,8 @@ 39DD77BB2470C49D00A29DEE /* Six Colors Light@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 39DD77B92470C49D00A29DEE /* Six Colors Light@2x.png */; }; 39E5032324EAFA8700FED642 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 39E5032224EAFA8700FED642 /* Introspect */; }; 4B0A2E47245E2EF800A79443 /* MultilineTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0A2E46245E2EF800A79443 /* MultilineTextField.swift */; }; + 4B0B26C826050818009E2D3A /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0B26C726050818009E2D3A /* Contacts.swift */; }; + 4B0B26C926050818009E2D3A /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0B26C726050818009E2D3A /* Contacts.swift */; }; 4B29F5B52466EC240084043B /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B29F5B42466EC240084043B /* ImagePicker.swift */; }; 4B693515249BBBFE0029D549 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BFEFD7E246F414D00CCF4A0 /* ShareViewController.swift */; }; 4BD867272460ADEF0014E3D6 /* NewConversationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD867262460ADEF0014E3D6 /* NewConversationView.swift */; }; @@ -348,6 +350,7 @@ 4B058B5524573A570059BC75 /* EditEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditEvent.swift; sourceTree = ""; }; 4B08F0A82495202A008E4286 /* LocalConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = LocalConfig.xcconfig; sourceTree = ""; }; 4B0A2E46245E2EF800A79443 /* MultilineTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextField.swift; sourceTree = ""; }; + 4B0B26C726050818009E2D3A /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.swift; sourceTree = ""; }; 4B29F5B42466EC240084043B /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; 4BD3D29224951E3B0031846F /* GlobalConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = GlobalConfig.xcconfig; sourceTree = ""; }; 4BD3DCA32482FF08009B5048 /* AccountStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStore.swift; sourceTree = ""; }; @@ -809,6 +812,7 @@ 393411C823904428003B49B8 /* MXEvent+Extensions.swift */, 4BEB8C03250403D200E90699 /* UserDefaults.swift */, E897AA3325F2716F00D11427 /* UXKit.swift */, + 4B0B26C726050818009E2D3A /* Contacts.swift */, ); path = Extensions; sourceTree = ""; @@ -1362,6 +1366,7 @@ E8B4725925F26D5A00ACEFCB /* NIORoomSummary.swift in Sources */, E8B4725725F26D5A00ACEFCB /* EditEvent.swift in Sources */, E8B4726125F26D5A00ACEFCB /* AccountStore.swift in Sources */, + 4B0B26C826050818009E2D3A /* Contacts.swift in Sources */, E8B4725825F26D5A00ACEFCB /* CustomEvent.swift in Sources */, E8B4725F25F26D5A00ACEFCB /* Configuration.swift in Sources */, E8B4725D25F26D5A00ACEFCB /* EventCollection.swift in Sources */, @@ -1445,6 +1450,7 @@ E897AA1B25F2707C00D11427 /* Configuration.swift in Sources */, E897AA1C25F2707C00D11427 /* CustomEvent.swift in Sources */, E897AA2325F2707C00D11427 /* Reaction.swift in Sources */, + 4B0B26C926050818009E2D3A /* Contacts.swift in Sources */, E897AA1925F2707C00D11427 /* EditEvent.swift in Sources */, E897AA1F25F2707C00D11427 /* MX+Identifiable.swift in Sources */, E897AA1D25F2707C00D11427 /* MXCredentials+Keychain.swift in Sources */, diff --git a/Nio/Info.plist b/Nio/Info.plist index b22410e7..41fa4419 100644 --- a/Nio/Info.plist +++ b/Nio/Info.plist @@ -83,6 +83,8 @@ DevelopmentTeam $(DEVELOPMENT_TEAM) + NSContactsUsageDescription + The contact information is used to find a matrix id on the identity server UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait diff --git a/Nio/Settings/SettingsView.swift b/Nio/Settings/SettingsView.swift index 636c51d1..b9097a0a 100644 --- a/Nio/Settings/SettingsView.swift +++ b/Nio/Settings/SettingsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import MatrixSDK import NioKit struct SettingsContainerView: View { @@ -49,6 +50,10 @@ private struct SettingsView: View { @AppStorage("accentColor") private var accentColor: Color = .purple @StateObject private var appIconTitle = AppIconTitle() let logoutAction: () -> Void + @AppStorage("identityServerBool") private var identityServerBool: Bool = false + @AppStorage("identityServer") private var identityServer: String = "https://vector.im" + @AppStorage("syncContacts") private var syncContacts: Bool = false + @EnvironmentObject var store: AccountStore @Environment(\.presentationMode) private var presentationMode @@ -73,6 +78,14 @@ private struct SettingsView: View { } } + Section { + Toggle("Enable Identity Server", isOn: $identityServerBool).onChange(of: identityServerBool, perform: syncIdentityServer(isSync:)) + if identityServerBool { + TextField("Identity URL", text: $identityServer) + Toggle("Sync Contacts", isOn: $syncContacts).onChange(of: syncContacts, perform: syncContacts(isSync:)) + } + } + Section { Button(action: self.logoutAction) { Text(verbatim: L10n.Settings.logOut) @@ -89,6 +102,32 @@ private struct SettingsView: View { } } } + + private func syncIdentityServer(isSync: Bool) { + if isSync { + store.setIdentityService() + } else { + // Revoke Contact Sync + syncContacts = false + } + } + + private func syncContacts(isSync: Bool) { + if isSync { + let contacts = Contacts.getAllContacts() + contacts.forEach { (contact) in + var mx3pids: [MX3PID] = [] + contact.emailAddresses.forEach { (email) in + mx3pids.append(MX3PID.init(medium: MX3PID.Medium.email, address: email.value as String)) + } + store.identityService?.lookup3PIDs(mx3pids) { response in + response.value?.forEach({ (responseItem: (key: MX3PID, value: String)) in + print(contact.givenName + " " + responseItem.value) + }) + } + } + } + } } struct SettingsView_Previews: PreviewProvider { diff --git a/NioKit/Extensions/Contacts.swift b/NioKit/Extensions/Contacts.swift new file mode 100644 index 00000000..f5dca5b5 --- /dev/null +++ b/NioKit/Extensions/Contacts.swift @@ -0,0 +1,42 @@ +// +// Contacts.swift +// Nio +// +// Created by Stefan Hofman on 19/03/2021. +// Copyright © 2021 Kilian Koeltzsch. All rights reserved. +// + +import Foundation +import Contacts + +public class Contacts { + public static func getContact() -> String { + let store = CNContactStore() + do { + let predicate = CNContact.predicateForContacts(matchingName: "Hofman") + let keysToFetch = [CNContactGivenNameKey, CNContactFamilyNameKey] as [CNKeyDescriptor] + let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch) + return "found" + } catch { + return "not found" + } + } + + public static func getAllContacts() -> [CNContact] { + var contacts = [CNContact]() + let store = CNContactStore() + do { + let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey] as [CNKeyDescriptor] + let request = CNContactFetchRequest(keysToFetch: keys) + try store.enumerateContacts(with: request) { (contact, stop) in + // Array containing all unified contacts from everywhere + if contact.emailAddresses.count > 0 { + contacts.append(contact) + } + } + return contacts + } catch { + return [] + } + } +} diff --git a/NioKit/Session/AccountStore.swift b/NioKit/Session/AccountStore.swift index 2facdeed..c9f935ed 100644 --- a/NioKit/Session/AccountStore.swift +++ b/NioKit/Session/AccountStore.swift @@ -2,6 +2,7 @@ import Foundation import Combine import MatrixSDK import KeychainAccess +import SwiftUI public enum LoginState { case loggedOut @@ -10,10 +11,16 @@ public enum LoginState { case loggedIn(userId: String) } +@available(iOS 14.0, *) public class AccountStore: ObservableObject { + @AppStorage("identityServer") private var identityServer: String = "https://vector.im" + @AppStorage("identityServerBool") private var identityServerBool: Bool = false + public var client: MXRestClient? public var session: MXSession? + public var identityService: MXIdentityService? + var fileStore: MXFileStore? var credentials: MXCredentials? @@ -120,6 +127,10 @@ public class AccountStore: ObservableObject { self.session = MXSession(matrixRestClient: self.client!) self.fileStore = MXFileStore() + if self.identityServerBool { + self.setIdentityService() + } + self.session!.setStore(fileStore!) { response in switch response { case .failure(let error): @@ -200,4 +211,8 @@ public class AccountStore: ObservableObject { self.objectWillChange.send() } } + + public func setIdentityService() { + self.identityService = MXIdentityService.init(identityServer: URL(string: identityServer)!, accessToken: nil, homeserverRestClient: self.client!) + } } diff --git a/NioShareExtension/ShareContentView.swift b/NioShareExtension/ShareContentView.swift index 228fe5f5..000152b6 100644 --- a/NioShareExtension/ShareContentView.swift +++ b/NioShareExtension/ShareContentView.swift @@ -2,6 +2,7 @@ import SwiftUI import NioKit import UIKit +@available(iOSApplicationExtension 14.0, *) struct ShareContentView: View { @State var parentView: ShareNavigationController @State var showConfirm = false diff --git a/NioShareExtension/ShareViewController.swift b/NioShareExtension/ShareViewController.swift index 4490bbb3..f8939e79 100644 --- a/NioShareExtension/ShareViewController.swift +++ b/NioShareExtension/ShareViewController.swift @@ -4,6 +4,8 @@ import MobileCoreServices import SwiftUI import NioKit +@available(macCatalystApplicationExtension 14.0, *) +@available(iOSApplicationExtension 14.0, *) @objc(ShareNavigationController) class ShareNavigationController: UIViewController { From d99186e0bea4d459e6357ae3a33948506507faaa Mon Sep 17 00:00:00 2001 From: Stefan Hofman Date: Mon, 22 Mar 2021 11:33:05 +0100 Subject: [PATCH 02/12] Begin of lookup on new conversation --- Nio/Generated/Strings.swift | 2 + Nio/NewConversation/NewConversationView.swift | 86 ++++++++++++++++++- Nio/Settings/SettingsView.swift | 12 ++- .../en.lproj/Localizable.strings | 1 + NioKit/Session/AccountStore.swift | 12 ++- 5 files changed, 102 insertions(+), 11 deletions(-) diff --git a/Nio/Generated/Strings.swift b/Nio/Generated/Strings.swift index 32ee5f0b..5fc60928 100644 --- a/Nio/Generated/Strings.swift +++ b/Nio/Generated/Strings.swift @@ -188,6 +188,8 @@ internal enum L10n { internal static let titleRoom = L10n.tr("Localizable", "new-conversation.title-room") /// Matrix ID internal static let usernamePlaceholder = L10n.tr("Localizable", "new-conversation.username-placeholder") + /// Matrix ID, Phone, or Email + internal static let usernamePlaceholderExtended = L10n.tr("Localizable", "new-conversation.username-placeholder-extended") } internal enum ReactionPicker { diff --git a/Nio/NewConversation/NewConversationView.swift b/Nio/NewConversation/NewConversationView.swift index 9e4cc291..87f61207 100644 --- a/Nio/NewConversation/NewConversationView.swift +++ b/Nio/NewConversation/NewConversationView.swift @@ -3,6 +3,13 @@ import MatrixSDK import NioKit +enum UserStatus { + case unkown + case notValid + case valid + case retrieving +} + struct NewConversationContainerView: View { @EnvironmentObject private var store: AccountStore @Binding var createdRoomId: ObjectIdentifier? @@ -14,10 +21,12 @@ struct NewConversationContainerView: View { private struct NewConversationView: View { @Environment(\.presentationMode) private var presentationMode + @AppStorage("identityServerBool") private var identityServerBool: Bool = false let store: AccountStore? @State private var users = [""] + @State private var usersVerified: [UserStatus] = [UserStatus.unkown] #if !os(macOS) @State private var editMode = EditMode.inactive #endif @@ -37,11 +46,42 @@ private struct NewConversationView: View { Section(footer: usersFooter) { ForEach(0.. 0 { + response.value?.forEach({ (responseItem: (key: MX3PID, value: String)) in + users[users.firstIndex(of: user)!] = responseItem.value + usersVerified[userIndex] = UserStatus.valid + }) + } else { + usersVerified[userIndex] = UserStatus.notValid + } + group.leave() + } + } + } else if result != nil { + usersVerified[userIndex] = UserStatus.valid + } else { + usersVerified[userIndex] = UserStatus.notValid + } + group.wait() + } + private func createRoom() { isWaiting = true + /* let parameters = MXRoomCreationParameters() if users.count == 1 { parameters.inviteArray = users @@ -174,7 +252,7 @@ private struct NewConversationView: View { @unknown default: fatalError("Unexpected Matrix response: \(response)") } - } + }*/ } } diff --git a/Nio/Settings/SettingsView.swift b/Nio/Settings/SettingsView.swift index b9097a0a..46287305 100644 --- a/Nio/Settings/SettingsView.swift +++ b/Nio/Settings/SettingsView.swift @@ -79,7 +79,13 @@ private struct SettingsView: View { } Section { - Toggle("Enable Identity Server", isOn: $identityServerBool).onChange(of: identityServerBool, perform: syncIdentityServer(isSync:)) + Toggle( + "Enable Identity Server", + isOn: $identityServerBool + ).onChange( + of: identityServerBool, + perform: syncIdentityServer(isSync:) + ) if identityServerBool { TextField("Identity URL", text: $identityServer) Toggle("Sync Contacts", isOn: $syncContacts).onChange(of: syncContacts, perform: syncContacts(isSync:)) @@ -102,7 +108,7 @@ private struct SettingsView: View { } } } - + private func syncIdentityServer(isSync: Bool) { if isSync { store.setIdentityService() @@ -111,7 +117,7 @@ private struct SettingsView: View { syncContacts = false } } - + private func syncContacts(isSync: Bool) { if isSync { let contacts = Contacts.getAllContacts() diff --git a/Nio/Supporting Files/en.lproj/Localizable.strings b/Nio/Supporting Files/en.lproj/Localizable.strings index 7779175a..0fe1841e 100644 --- a/Nio/Supporting Files/en.lproj/Localizable.strings +++ b/Nio/Supporting Files/en.lproj/Localizable.strings @@ -81,6 +81,7 @@ "new-conversation.title-chat" = "New Chat"; "new-conversation.title-room" = "New Room"; "new-conversation.username-placeholder" = "Matrix ID"; +"new-conversation.username-placeholder-extended" = "Matrix ID, Phone, or Email"; "new-conversation.for-example" = "For example"; "new-conversation.room-name" = "Room Name"; "new-conversation.public-room" = "Public Room"; diff --git a/NioKit/Session/AccountStore.swift b/NioKit/Session/AccountStore.swift index c9f935ed..64d19008 100644 --- a/NioKit/Session/AccountStore.swift +++ b/NioKit/Session/AccountStore.swift @@ -15,12 +15,12 @@ public enum LoginState { public class AccountStore: ObservableObject { @AppStorage("identityServer") private var identityServer: String = "https://vector.im" @AppStorage("identityServerBool") private var identityServerBool: Bool = false - + public var client: MXRestClient? public var session: MXSession? public var identityService: MXIdentityService? - + var fileStore: MXFileStore? var credentials: MXCredentials? @@ -211,8 +211,12 @@ public class AccountStore: ObservableObject { self.objectWillChange.send() } } - + public func setIdentityService() { - self.identityService = MXIdentityService.init(identityServer: URL(string: identityServer)!, accessToken: nil, homeserverRestClient: self.client!) + self.identityService = MXIdentityService.init( + identityServer: URL(string: identityServer)!, + accessToken: nil, + homeserverRestClient: self.client! + ) } } From ded777c0cc71f4d8e556b1d6c7c371b95b152f70 Mon Sep 17 00:00:00 2001 From: Stefan Hofman Date: Mon, 22 Mar 2021 16:32:28 +0100 Subject: [PATCH 03/12] New settings screen with Privacy Screen --- Nio.xcodeproj/project.pbxproj | 18 ++ Nio/Generated/Strings.swift | 33 +++ Nio/NewConversation/NewConversationView.swift | 104 ++++---- Nio/Resources/en.lproj/InfoPlist.strings | 1 + Nio/Resources/nl.lproj/InfoPlist.strings | 1 + Nio/Settings/IdentityServerSettings.swift | 225 ++++++++++++++++++ Nio/Settings/SettingsView.swift | 46 +--- .../en.lproj/Localizable.strings | 15 ++ 8 files changed, 351 insertions(+), 92 deletions(-) create mode 100644 Nio/Resources/en.lproj/InfoPlist.strings create mode 100644 Nio/Resources/nl.lproj/InfoPlist.strings create mode 100644 Nio/Settings/IdentityServerSettings.swift diff --git a/Nio.xcodeproj/project.pbxproj b/Nio.xcodeproj/project.pbxproj index 2571c1f1..761517f2 100644 --- a/Nio.xcodeproj/project.pbxproj +++ b/Nio.xcodeproj/project.pbxproj @@ -60,6 +60,8 @@ 4B0B26C926050818009E2D3A /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0B26C726050818009E2D3A /* Contacts.swift */; }; 4B29F5B52466EC240084043B /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B29F5B42466EC240084043B /* ImagePicker.swift */; }; 4B693515249BBBFE0029D549 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BFEFD7E246F414D00CCF4A0 /* ShareViewController.swift */; }; + 4B9AB8C32608D08A005E1092 /* IdentityServerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9AB8C22608D08A005E1092 /* IdentityServerSettings.swift */; }; + 4B9AB8D22608EED9005E1092 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4B9AB8D42608EED9005E1092 /* InfoPlist.strings */; }; 4BD867272460ADEF0014E3D6 /* NewConversationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD867262460ADEF0014E3D6 /* NewConversationView.swift */; }; 4BFEFD86246F414D00CCF4A0 /* NioShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4BFEFD7C246F414D00CCF4A0 /* NioShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4BFEFD8C246F458000CCF4A0 /* GetURL.js in Resources */ = {isa = PBXBuildFile; fileRef = 4BFEFD8B246F458000CCF4A0 /* GetURL.js */; }; @@ -352,6 +354,9 @@ 4B0A2E46245E2EF800A79443 /* MultilineTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextField.swift; sourceTree = ""; }; 4B0B26C726050818009E2D3A /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.swift; sourceTree = ""; }; 4B29F5B42466EC240084043B /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; + 4B9AB8C22608D08A005E1092 /* IdentityServerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityServerSettings.swift; sourceTree = ""; }; + 4B9AB8D32608EED9005E1092 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 4B9AB8DB2608EEE1005E1092 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; 4BD3D29224951E3B0031846F /* GlobalConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = GlobalConfig.xcconfig; sourceTree = ""; }; 4BD3DCA32482FF08009B5048 /* AccountStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStore.swift; sourceTree = ""; }; 4BD867262460ADEF0014E3D6 /* NewConversationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationView.swift; sourceTree = ""; }; @@ -476,6 +481,7 @@ children = ( 3902B8A22395935600698B87 /* SettingsView.swift */, 39DD77AE247006E300A29DEE /* AppIcon.swift */, + 4B9AB8C22608D08A005E1092 /* IdentityServerSettings.swift */, ); path = Settings; sourceTree = ""; @@ -508,6 +514,7 @@ 390D63BA246F4BEE00B8F640 /* Resources */ = { isa = PBXGroup; children = ( + 4B9AB8D42608EED9005E1092 /* InfoPlist.strings */, 390D63BB246F4BEE00B8F640 /* Alternate Icons */, ); path = Resources; @@ -1147,6 +1154,7 @@ 392117572442556700892B00 /* Localizable.strings in Resources */, 39C931F723846B2D004449E1 /* .swiftlint.yml in Resources */, 39DD77B62470C38300A29DEE /* Six Colors Dark@3x.png in Resources */, + 4B9AB8D22608EED9005E1092 /* InfoPlist.strings in Resources */, 39C931E92384328B004449E1 /* LaunchScreen.storyboard in Resources */, 39C931E62384328B004449E1 /* Preview Assets.xcassets in Resources */, 39DD77BA2470C49D00A29DEE /* Six Colors Light@3x.png in Resources */, @@ -1288,6 +1296,7 @@ 3923898F2388707E00B2E1DF /* RoomListItemView.swift in Sources */, 4B29F5B52466EC240084043B /* ImagePicker.swift in Sources */, CAF2AE8B2458EF0900D84133 /* MarkdownText.swift in Sources */, + 4B9AB8C32608D08A005E1092 /* IdentityServerSettings.swift in Sources */, 395E06FD25DDC1790059F6AD /* SFSymbol.swift in Sources */, 39DD77AF247006E300A29DEE /* AppIcon.swift in Sources */, CAFCB323245F6E6700869320 /* NSAttributedString+Extensions.swift in Sources */, @@ -1545,6 +1554,15 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + 4B9AB8D42608EED9005E1092 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 4B9AB8D32608EED9005E1092 /* en */, + 4B9AB8DB2608EEE1005E1092 /* nl */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ diff --git a/Nio/Generated/Strings.swift b/Nio/Generated/Strings.swift index 5fc60928..af2f0f1d 100644 --- a/Nio/Generated/Strings.swift +++ b/Nio/Generated/Strings.swift @@ -297,6 +297,39 @@ internal enum L10n { internal static let title = L10n.tr("Localizable", "settings.title") } + internal enum SettingsIdentityServer { + /// Closed Federation + internal static let closedFederation = L10n.tr("Localizable", "settings-identity-server.closed-federation") + /// At the moment, the identity servers are in a closed federation configuration. This means that there are only two identity servers (matrix.org, vector.im) and all data uploaded to one is copied to the other. + internal static let closedFederationText = L10n.tr("Localizable", "settings-identity-server.closed-federation-text") + /// Sync Contacts + internal static let contactSync = L10n.tr("Localizable", "settings-identity-server.contact-sync") + /// Continue + internal static let `continue` = L10n.tr("Localizable", "settings-identity-server.continue") + /// Data + internal static let data = L10n.tr("Localizable", "settings-identity-server.data") + /// Data & Privacy + internal static let dataPrivacy = L10n.tr("Localizable", "settings-identity-server.data-privacy") + /// By using the identity server, your email address and phone number will be sent to the identity server and will be linked to your Matrix ID (@example.nio.chat). + internal static let dataText = L10n.tr("Localizable", "settings-identity-server.data-text") + /// Learn More + internal static let learnMore = L10n.tr("Localizable", "settings-identity-server.learn-more") + /// Match + internal static let match = L10n.tr("Localizable", "settings-identity-server.match") + /// Any user can retrieve your Matrix ID by entering your email address or phone number, but cannot find your email address or phone number by entering your Matrix ID. + internal static let matchText = L10n.tr("Localizable", "settings-identity-server.match-text") + /// (Optional) Contact Sync + internal static let optionalContactSync = L10n.tr("Localizable", "settings-identity-server.optional-contact-sync") + /// When activating contact syncronization, Nio will periodically send the email addresses and phone numbers of all your contacts to the identity server to see if that contact has a linked Matrix ID. This contact information is never stored or shared by Nio. + internal static let optionalContactSyncText = L10n.tr("Localizable", "settings-identity-server.optional-contact-sync-text") + /// Identity Server + internal static let title = L10n.tr("Localizable", "settings-identity-server.title") + /// Enable Identity Server + internal static let toggle = L10n.tr("Localizable", "settings-identity-server.toggle") + /// Identity URL + internal static let url = L10n.tr("Localizable", "settings-identity-server.url") + } + internal enum TypingIndicator { /// Several people are typing internal static let many = L10n.tr("Localizable", "typing-indicator.many") diff --git a/Nio/NewConversation/NewConversationView.swift b/Nio/NewConversation/NewConversationView.swift index 87f61207..007b9cb0 100644 --- a/Nio/NewConversation/NewConversationView.swift +++ b/Nio/NewConversation/NewConversationView.swift @@ -4,10 +4,10 @@ import MatrixSDK import NioKit enum UserStatus { - case unkown - case notValid - case valid - case retrieving + case unkown // Validity of user is still unkown. + case notValid // User is not valid. + case valid // User is valid. + case retrieving // Validity of user is being retrieved from identity server. } struct NewConversationContainerView: View { @@ -27,6 +27,11 @@ private struct NewConversationView: View { @State private var users = [""] @State private var usersVerified: [UserStatus] = [UserStatus.unkown] + + @State private var numCalls: Int = 0 + + @State private var isRetrieving = false + #if !os(macOS) @State private var editMode = EditMode.inactive #endif @@ -54,34 +59,19 @@ private struct NewConversationView: View { Image(systemName: "multiply.circle") } else if usersVerified[index] == UserStatus.retrieving { Image(systemName: "arrow.2.circlepath.circle") - } - if identityServerBool { - // proxy binding prevents an index out of range crash on delete - TextField( - L10n.NewConversation.usernamePlaceholderExtended, - text: Binding(get: { users[index] }, set: { users[index] = $0 }), - onEditingChanged: { (editingChanged) in - if !editingChanged { - findUser(userIndex: index) - } - } - ) - .disableAutocorrection(true) - .autocapitalization(.none) - } else { - // proxy binding prevents an index out of range crash on delete - TextField( - L10n.NewConversation.usernamePlaceholder, - text: Binding(get: { users[index] }, set: { users[index] = $0 }), - onEditingChanged: { (editingChanged) in - if !editingChanged { - findUser(userIndex: index) - } + .rotationEffect(Angle.degrees(isRetrieving ? 360 : 0)) + .animation(Animation.linear.repeatForever(autoreverses: false).speed(0.25)) + .onAppear { + self.isRetrieving.toggle() } - ) - .disableAutocorrection(true) - .autocapitalization(.none) } + // proxy binding prevents an index out of range crash on delete + TextField( + (identityServerBool ? L10n.NewConversation.usernamePlaceholderExtended : L10n.NewConversation.usernamePlaceholder), + text: Binding(get: { users[index] }, set: { users[index] = $0; findUser(userIndex: index) }) + ) + .disableAutocorrection(true) + .autocapitalization(.none) Spacer() Button(action: addUser) { Image(systemName: "plus.circle") @@ -108,7 +98,13 @@ private struct NewConversationView: View { Button(action: createRoom) { Text(verbatim: L10n.NewConversation.createRoom) } - .disabled(users.contains("") || (roomName.isEmpty && users.count > 1)) + .disabled( + users.contains("") + || (roomName.isEmpty && users.count > 1) + || usersVerified.contains(UserStatus.notValid) + || usersVerified.contains(UserStatus.retrieving) + || usersVerified.contains(UserStatus.unkown) + ) #endif Spacer() ProgressView() @@ -181,32 +177,44 @@ private struct NewConversationView: View { } } - private func findUser(userIndex: Int) { + private func findUser(userIndex: Int, recheck: Bool = false) { usersVerified[userIndex] = UserStatus.retrieving let user = users[userIndex] + // Match for Matrix ID pattern. let pattern = "@[A-Z0-9a-z._%+-]+:[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" let result = user.range(of: pattern, options: .regularExpression) - let group = DispatchGroup() - group.enter() - if result == nil && identityServerBool { - DispatchQueue.global(qos: .background).async { - - let mx3pids: [MX3PID] = [ - MX3PID.init(medium: MX3PID.Medium.email, address: user), - MX3PID.init(medium: MX3PID.Medium.msisdn, address: user), - ] - store?.identityService?.lookup3PIDs(mx3pids) { response in - print(response) + // Check if user is not a Matrix ID, Identity Server is turned on does not start with an @ character + // (indicating a Matrix ID is coming), and check if user is long enough to be an email/ phone number + // to reduce the number of calls. + if result == nil && identityServerBool && !user.starts(with: "@") && user.count >= 6 { + numCalls += 1 + let mx3pids: [MX3PID] = [ + // Look for email occurrences on the identity server. + MX3PID.init(medium: MX3PID.Medium.email, address: user), + // Look for phone number occurrences on the identity server. + MX3PID.init( + medium: MX3PID.Medium.msisdn, + address: user.replacingOccurrences(of: "+", with: "").replacingOccurrences(of: " ", with: "") + ), + ] + store?.identityService?.lookup3PIDs(mx3pids) { response in + numCalls -= 1 + // Check if it is the last identity request. + if numCalls == 0 { if response.value?.count ?? 0 > 0 { response.value?.forEach({ (responseItem: (key: MX3PID, value: String)) in - users[users.firstIndex(of: user)!] = responseItem.value + users[userIndex] = responseItem.value usersVerified[userIndex] = UserStatus.valid }) } else { - usersVerified[userIndex] = UserStatus.notValid + // In case the request did not come back in the correct order send the last value again. + if !recheck { + findUser(userIndex: userIndex, recheck: true) + } else { + usersVerified[userIndex] = UserStatus.notValid + } } - group.leave() } } } else if result != nil { @@ -214,13 +222,11 @@ private struct NewConversationView: View { } else { usersVerified[userIndex] = UserStatus.notValid } - group.wait() } private func createRoom() { isWaiting = true - /* let parameters = MXRoomCreationParameters() if users.count == 1 { parameters.inviteArray = users @@ -252,7 +258,7 @@ private struct NewConversationView: View { @unknown default: fatalError("Unexpected Matrix response: \(response)") } - }*/ + } } } diff --git a/Nio/Resources/en.lproj/InfoPlist.strings b/Nio/Resources/en.lproj/InfoPlist.strings new file mode 100644 index 00000000..9ad45316 --- /dev/null +++ b/Nio/Resources/en.lproj/InfoPlist.strings @@ -0,0 +1 @@ +NSContactsUsageDescription = "The contact information is used to find a Matrix ID on the identity server"; diff --git a/Nio/Resources/nl.lproj/InfoPlist.strings b/Nio/Resources/nl.lproj/InfoPlist.strings new file mode 100644 index 00000000..8377cc34 --- /dev/null +++ b/Nio/Resources/nl.lproj/InfoPlist.strings @@ -0,0 +1 @@ +NSContactsUsageDescription = "De contact informatie wordt om een Matrix ID te vinden op de identity server"; diff --git a/Nio/Settings/IdentityServerSettings.swift b/Nio/Settings/IdentityServerSettings.swift new file mode 100644 index 00000000..c95b0867 --- /dev/null +++ b/Nio/Settings/IdentityServerSettings.swift @@ -0,0 +1,225 @@ +import SwiftUI +import MatrixSDK + +import NioKit + +struct IdentityServerSettingsContainerView: View { + @EnvironmentObject var store: AccountStore + + var body: some View { + IdentityServerSettingsView() + } +} + +struct ButtonModifier: ViewModifier { + @AppStorage("accentColor") private var accentColor: Color = .purple + + func body(content: Content) -> some View { + content + .foregroundColor(.white) + .font(.headline) + .padding() + .frame(minWidth: 0, maxWidth: .infinity, alignment: .center) + .background(RoundedRectangle(cornerRadius: 15, style: .continuous) + .fill(accentColor)) + .padding(.bottom) + } +} + +extension View { + func customButton() -> ModifiedContent { + return modifier(ButtonModifier()) + } +} + +extension Text { + func customTitleText() -> Text { + self + .fontWeight(.black) + .font(.system(size: 36)) + } +} + +struct InformationDetailView: View { + @AppStorage("accentColor") private var accentColor: Color = .purple + + var title: String = "" + var subTitle: String = "" + var imageName: String = "" + + var body: some View { + HStack(alignment: .center) { + Image(systemName: imageName) + .font(.largeTitle) + .foregroundColor(accentColor) + .padding() + .accessibility(hidden: true) + .fixedSize(horizontal: true, vertical: false) + + VStack(alignment: .leading) { + Text(title) + .font(.headline) + .foregroundColor(.primary) + .accessibility(addTraits: .isHeader) + + Text(subTitle) + //.font(.body) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } + .padding(.top) + } +} + +struct InformationContainerView: View { + var body: some View { + VStack(alignment: .leading) { + InformationDetailView( + title: L10n.SettingsIdentityServer.data, + subTitle: L10n.SettingsIdentityServer.dataText, + imageName: "arrow.up.doc" + ) + + InformationDetailView( + title: L10n.SettingsIdentityServer.match, + subTitle: L10n.SettingsIdentityServer.matchText, + imageName: "magnifyingglass" + ) + + InformationDetailView(title: L10n.SettingsIdentityServer.closedFederation, + subTitle: L10n.SettingsIdentityServer.closedFederationText, + imageName: "globe" + ) + + InformationDetailView(title: L10n.SettingsIdentityServer.optionalContactSync, + subTitle: L10n.SettingsIdentityServer.optionalContactSyncText, + imageName: "person") + } + .padding(.horizontal) + } +} + +struct TitleView: View { + @AppStorage("accentColor") private var accentColor: Color = .purple + + var body: some View { + VStack { + Image(systemName: "person.3") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 180, alignment: .center) + .accessibility(hidden: true) + .foregroundColor(accentColor) + + Text(L10n.SettingsIdentityServer.dataPrivacy) + .customTitleText() + + Text(L10n.SettingsIdentityServer.title) + .customTitleText() + .foregroundColor(accentColor) + } + } +} + +struct IdentityServerInfoView: View { + @AppStorage("accentColor") private var accentColor: Color = .purple + + @Environment(\.presentationMode) var presentationMode + + var body: some View { + ScrollView { + VStack(alignment: .center) { + + Spacer() + + TitleView() + + InformationContainerView() + + Spacer(minLength: 30) + + Button(action: { + presentationMode.wrappedValue.dismiss() + }) { + Text(L10n.SettingsIdentityServer.continue) + .customButton() + } + .padding(.horizontal) + + Spacer() + + Link( + L10n.SettingsIdentityServer.learnMore, + destination: URL(string: "https://matrix.org/legal/identity-server-privacy-notice-1")! + ) + .foregroundColor(accentColor) + } + } + .padding(.vertical) + } +} + +struct IdentityServerSettingsView: View { + @AppStorage("identityServerBool") private var identityServerBool: Bool = false + @AppStorage("identityServer") private var identityServer: String = "https://vector.im" + @AppStorage("syncContacts") private var syncContacts: Bool = false + + @EnvironmentObject var store: AccountStore + + @State private var showModal = false + + var identityServerInfo: some View { + Button(action: { + showModal.toggle() + }) { + HStack { + Text(L10n.SettingsIdentityServer.title) + Image(systemName: "info.circle") + } + } + .fullScreenCover(isPresented: $showModal, content: IdentityServerInfoView.init) + } + + var body: some View { + Section(header: identityServerInfo) { + Toggle( + L10n.SettingsIdentityServer.toggle, + isOn: $identityServerBool + ).onChange( + of: identityServerBool, + perform: syncIdentityServer(isSync:) + ) + if identityServerBool { + TextField(L10n.SettingsIdentityServer.url, text: $identityServer) + Toggle(L10n.SettingsIdentityServer.contactSync, isOn: $syncContacts).onChange(of: syncContacts, perform: syncContacts(isSync:)) + } + } + } + + private func syncIdentityServer(isSync: Bool) { + if isSync { + store.setIdentityService() + } else { + // Revoke Contact Sync + syncContacts = false + } + } + + private func syncContacts(isSync: Bool) { + if isSync { + let contacts = Contacts.getAllContacts() + contacts.forEach { (contact) in + var mx3pids: [MX3PID] = [] + contact.emailAddresses.forEach { (email) in + mx3pids.append(MX3PID.init(medium: MX3PID.Medium.email, address: email.value as String)) + } + store.identityService?.lookup3PIDs(mx3pids) { response in + response.value?.forEach({ (responseItem: (key: MX3PID, value: String)) in + print(contact.givenName + " " + responseItem.value) + }) + } + } + } + } +} diff --git a/Nio/Settings/SettingsView.swift b/Nio/Settings/SettingsView.swift index 46287305..a42b2c75 100644 --- a/Nio/Settings/SettingsView.swift +++ b/Nio/Settings/SettingsView.swift @@ -50,13 +50,10 @@ private struct SettingsView: View { @AppStorage("accentColor") private var accentColor: Color = .purple @StateObject private var appIconTitle = AppIconTitle() let logoutAction: () -> Void - @AppStorage("identityServerBool") private var identityServerBool: Bool = false - @AppStorage("identityServer") private var identityServer: String = "https://vector.im" - @AppStorage("syncContacts") private var syncContacts: Bool = false @EnvironmentObject var store: AccountStore @Environment(\.presentationMode) private var presentationMode - + var body: some View { NavigationView { Form { @@ -78,19 +75,8 @@ private struct SettingsView: View { } } - Section { - Toggle( - "Enable Identity Server", - isOn: $identityServerBool - ).onChange( - of: identityServerBool, - perform: syncIdentityServer(isSync:) - ) - if identityServerBool { - TextField("Identity URL", text: $identityServer) - Toggle("Sync Contacts", isOn: $syncContacts).onChange(of: syncContacts, perform: syncContacts(isSync:)) - } - } + // Text("Using an Identity Server will allow you to find other users by their email address and/ or phone number. This will mean that your email address and/ or phone number will be uploaded to the Identity Server") + IdentityServerSettingsContainerView() Section { Button(action: self.logoutAction) { @@ -108,32 +94,6 @@ private struct SettingsView: View { } } } - - private func syncIdentityServer(isSync: Bool) { - if isSync { - store.setIdentityService() - } else { - // Revoke Contact Sync - syncContacts = false - } - } - - private func syncContacts(isSync: Bool) { - if isSync { - let contacts = Contacts.getAllContacts() - contacts.forEach { (contact) in - var mx3pids: [MX3PID] = [] - contact.emailAddresses.forEach { (email) in - mx3pids.append(MX3PID.init(medium: MX3PID.Medium.email, address: email.value as String)) - } - store.identityService?.lookup3PIDs(mx3pids) { response in - response.value?.forEach({ (responseItem: (key: MX3PID, value: String)) in - print(contact.givenName + " " + responseItem.value) - }) - } - } - } - } } struct SettingsView_Previews: PreviewProvider { diff --git a/Nio/Supporting Files/en.lproj/Localizable.strings b/Nio/Supporting Files/en.lproj/Localizable.strings index 0fe1841e..99b1e581 100644 --- a/Nio/Supporting Files/en.lproj/Localizable.strings +++ b/Nio/Supporting Files/en.lproj/Localizable.strings @@ -78,6 +78,21 @@ "settings.app-icon" = "App Icon"; "settings.log-out" = "Log Out"; "settings.dismiss" = "Done"; +"settings-identity-server.title" = "Identity Server"; +"settings-identity-server.toggle" = "Enable Identity Server"; +"settings-identity-server.url" = "Identity URL"; +"settings-identity-server.contact-sync" = "Sync Contacts"; +"settings-identity-server.continue" = "Continue"; +"settings-identity-server.learn-more" = "Learn More"; +"settings-identity-server.data-privacy" = "Data & Privacy"; +"settings-identity-server.data" = "Data"; +"settings-identity-server.data-text" = "By using the identity server, your email address and phone number will be sent to the identity server and will be linked to your Matrix ID (@example.nio.chat)."; +"settings-identity-server.match" = "Match"; +"settings-identity-server.match-text" = "Any user can retrieve your Matrix ID by entering your email address or phone number, but cannot find your email address or phone number by entering your Matrix ID."; +"settings-identity-server.closed-federation" = "Closed Federation"; +"settings-identity-server.closed-federation-text" = "At the moment, the identity servers are in a closed federation configuration. This means that there are only two identity servers (matrix.org, vector.im) and all data uploaded to one is copied to the other."; +"settings-identity-server.optional-contact-sync" = "(Optional) Contact Sync"; +"settings-identity-server.optional-contact-sync-text" = "When activating contact syncronization, Nio will periodically send the email addresses and phone numbers of all your contacts to the identity server to see if that contact has a linked Matrix ID. This contact information is never stored or shared by Nio."; "new-conversation.title-chat" = "New Chat"; "new-conversation.title-room" = "New Room"; "new-conversation.username-placeholder" = "Matrix ID"; From 9eab50b7b1126e5db13004254d8428fb0db595b7 Mon Sep 17 00:00:00 2001 From: Stefan Hofman Date: Mon, 22 Mar 2021 16:41:58 +0100 Subject: [PATCH 04/12] Remove non-functional contanct sync support --- Nio/Settings/IdentityServerSettings.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Nio/Settings/IdentityServerSettings.swift b/Nio/Settings/IdentityServerSettings.swift index c95b0867..b3bbee6c 100644 --- a/Nio/Settings/IdentityServerSettings.swift +++ b/Nio/Settings/IdentityServerSettings.swift @@ -92,9 +92,9 @@ struct InformationContainerView: View { imageName: "globe" ) - InformationDetailView(title: L10n.SettingsIdentityServer.optionalContactSync, - subTitle: L10n.SettingsIdentityServer.optionalContactSyncText, - imageName: "person") + //InformationDetailView(title: L10n.SettingsIdentityServer.optionalContactSync, + // subTitle: L10n.SettingsIdentityServer.optionalContactSyncText, + // imageName: "person") } .padding(.horizontal) } @@ -192,7 +192,7 @@ struct IdentityServerSettingsView: View { ) if identityServerBool { TextField(L10n.SettingsIdentityServer.url, text: $identityServer) - Toggle(L10n.SettingsIdentityServer.contactSync, isOn: $syncContacts).onChange(of: syncContacts, perform: syncContacts(isSync:)) + //Toggle(L10n.SettingsIdentityServer.contactSync, isOn: $syncContacts).onChange(of: syncContacts, perform: syncContacts(isSync:)) } } } From 7d0cc878c2d3a3aa2986a8ce7a7df21768b65a0e Mon Sep 17 00:00:00 2001 From: Stefan Hofman Date: Mon, 22 Mar 2021 19:53:25 +0100 Subject: [PATCH 05/12] Move plist file and add localization support --- Nio.xcodeproj/project.pbxproj | 22 +++++++++---------- Nio/NewConversation/NewConversationView.swift | 2 ++ Nio/Resources/en.lproj/InfoPlist.strings | 1 - Nio/Resources/nl.lproj/InfoPlist.strings | 1 - Nio/Settings/IdentityServerSettings.swift | 18 +++++++++------ Nio/{ => Supporting Files}/Info.plist | 2 +- .../en.lproj/InfoPlist.strings | 1 + .../nl.lproj/InfoPlist.strings | 1 + NioKit/Extensions/Contacts.swift | 16 ++++++++++++++ swiftgen.yml | 2 +- 10 files changed, 44 insertions(+), 22 deletions(-) delete mode 100644 Nio/Resources/en.lproj/InfoPlist.strings delete mode 100644 Nio/Resources/nl.lproj/InfoPlist.strings rename Nio/{ => Supporting Files}/Info.plist (95%) create mode 100644 Nio/Supporting Files/en.lproj/InfoPlist.strings create mode 100644 Nio/Supporting Files/nl.lproj/InfoPlist.strings diff --git a/Nio.xcodeproj/project.pbxproj b/Nio.xcodeproj/project.pbxproj index 761517f2..d6fe11e9 100644 --- a/Nio.xcodeproj/project.pbxproj +++ b/Nio.xcodeproj/project.pbxproj @@ -60,8 +60,8 @@ 4B0B26C926050818009E2D3A /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0B26C726050818009E2D3A /* Contacts.swift */; }; 4B29F5B52466EC240084043B /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B29F5B42466EC240084043B /* ImagePicker.swift */; }; 4B693515249BBBFE0029D549 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BFEFD7E246F414D00CCF4A0 /* ShareViewController.swift */; }; + 4B8225A426091A5F0013E733 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4B8225A626091A5F0013E733 /* InfoPlist.strings */; }; 4B9AB8C32608D08A005E1092 /* IdentityServerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9AB8C22608D08A005E1092 /* IdentityServerSettings.swift */; }; - 4B9AB8D22608EED9005E1092 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4B9AB8D42608EED9005E1092 /* InfoPlist.strings */; }; 4BD867272460ADEF0014E3D6 /* NewConversationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD867262460ADEF0014E3D6 /* NewConversationView.swift */; }; 4BFEFD86246F414D00CCF4A0 /* NioShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 4BFEFD7C246F414D00CCF4A0 /* NioShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4BFEFD8C246F458000CCF4A0 /* GetURL.js in Resources */ = {isa = PBXBuildFile; fileRef = 4BFEFD8B246F458000CCF4A0 /* GetURL.js */; }; @@ -354,9 +354,9 @@ 4B0A2E46245E2EF800A79443 /* MultilineTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextField.swift; sourceTree = ""; }; 4B0B26C726050818009E2D3A /* Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contacts.swift; sourceTree = ""; }; 4B29F5B42466EC240084043B /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; + 4B8225A526091A5F0013E733 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 4B8225AD26091A650013E733 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; 4B9AB8C22608D08A005E1092 /* IdentityServerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityServerSettings.swift; sourceTree = ""; }; - 4B9AB8D32608EED9005E1092 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - 4B9AB8DB2608EEE1005E1092 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; 4BD3D29224951E3B0031846F /* GlobalConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = GlobalConfig.xcconfig; sourceTree = ""; }; 4BD3DCA32482FF08009B5048 /* AccountStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountStore.swift; sourceTree = ""; }; 4BD867262460ADEF0014E3D6 /* NewConversationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationView.swift; sourceTree = ""; }; @@ -514,7 +514,6 @@ 390D63BA246F4BEE00B8F640 /* Resources */ = { isa = PBXGroup; children = ( - 4B9AB8D42608EED9005E1092 /* InfoPlist.strings */, 390D63BB246F4BEE00B8F640 /* Alternate Icons */, ); path = Resources; @@ -671,7 +670,6 @@ 39D166C42385C7F6006DD257 /* Extensions */, 3921175D2442563A00892B00 /* Generated */, 39C931E22384328B004449E1 /* Assets.xcassets */, - 39C931EA2384328B004449E1 /* Info.plist */, 390D63BA246F4BEE00B8F640 /* Resources */, 39C931F3238449C2004449E1 /* Supporting Files */, 39C931E42384328B004449E1 /* Preview Content */, @@ -690,10 +688,12 @@ 39C931F3238449C2004449E1 /* Supporting Files */ = { isa = PBXGroup; children = ( + 39C931EA2384328B004449E1 /* Info.plist */, 39C931F623846B2D004449E1 /* .swiftlint.yml */, 3921175B244255D600892B00 /* swiftgen.yml */, 39C931E72384328B004449E1 /* LaunchScreen.storyboard */, 392117592442556700892B00 /* Localizable.strings */, + 4B8225A626091A5F0013E733 /* InfoPlist.strings */, ); path = "Supporting Files"; sourceTree = ""; @@ -1154,7 +1154,7 @@ 392117572442556700892B00 /* Localizable.strings in Resources */, 39C931F723846B2D004449E1 /* .swiftlint.yml in Resources */, 39DD77B62470C38300A29DEE /* Six Colors Dark@3x.png in Resources */, - 4B9AB8D22608EED9005E1092 /* InfoPlist.strings in Resources */, + 4B8225A426091A5F0013E733 /* InfoPlist.strings in Resources */, 39C931E92384328B004449E1 /* LaunchScreen.storyboard in Resources */, 39C931E62384328B004449E1 /* Preview Assets.xcassets in Resources */, 39DD77BA2470C49D00A29DEE /* Six Colors Light@3x.png in Resources */, @@ -1554,11 +1554,11 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; - 4B9AB8D42608EED9005E1092 /* InfoPlist.strings */ = { + 4B8225A626091A5F0013E733 /* InfoPlist.strings */ = { isa = PBXVariantGroup; children = ( - 4B9AB8D32608EED9005E1092 /* en */, - 4B9AB8DB2608EEE1005E1092 /* nl */, + 4B8225A526091A5F0013E733 /* en */, + 4B8225AD26091A650013E733 /* nl */, ); name = InfoPlist.strings; sourceTree = ""; @@ -1735,7 +1735,7 @@ CURRENT_PROJECT_VERSION = 33; DEVELOPMENT_ASSET_PATHS = "\"Nio/Preview Content\""; ENABLE_PREVIEWS = YES; - INFOPLIST_FILE = Nio/Info.plist; + INFOPLIST_FILE = "Nio/Supporting Files/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1759,7 +1759,7 @@ DEVELOPMENT_ASSET_PATHS = "\"Nio/Preview Content\""; DEVELOPMENT_TEAM = HU85FER47E; ENABLE_PREVIEWS = YES; - INFOPLIST_FILE = Nio/Info.plist; + INFOPLIST_FILE = "Nio/Supporting Files/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Nio/NewConversation/NewConversationView.swift b/Nio/NewConversation/NewConversationView.swift index 007b9cb0..6f6834c4 100644 --- a/Nio/NewConversation/NewConversationView.swift +++ b/Nio/NewConversation/NewConversationView.swift @@ -55,8 +55,10 @@ private struct NewConversationView: View { Image(systemName: "questionmark.circle") } else if usersVerified[index] == UserStatus.valid { Image(systemName: "checkmark.circle") + .foregroundColor(Color.green) } else if usersVerified[index] == UserStatus.notValid { Image(systemName: "multiply.circle") + .foregroundColor(Color.red) } else if usersVerified[index] == UserStatus.retrieving { Image(systemName: "arrow.2.circlepath.circle") .rotationEffect(Angle.degrees(isRetrieving ? 360 : 0)) diff --git a/Nio/Resources/en.lproj/InfoPlist.strings b/Nio/Resources/en.lproj/InfoPlist.strings deleted file mode 100644 index 9ad45316..00000000 --- a/Nio/Resources/en.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -NSContactsUsageDescription = "The contact information is used to find a Matrix ID on the identity server"; diff --git a/Nio/Resources/nl.lproj/InfoPlist.strings b/Nio/Resources/nl.lproj/InfoPlist.strings deleted file mode 100644 index 8377cc34..00000000 --- a/Nio/Resources/nl.lproj/InfoPlist.strings +++ /dev/null @@ -1 +0,0 @@ -NSContactsUsageDescription = "De contact informatie wordt om een Matrix ID te vinden op de identity server"; diff --git a/Nio/Settings/IdentityServerSettings.swift b/Nio/Settings/IdentityServerSettings.swift index b3bbee6c..b654207f 100644 --- a/Nio/Settings/IdentityServerSettings.swift +++ b/Nio/Settings/IdentityServerSettings.swift @@ -92,9 +92,9 @@ struct InformationContainerView: View { imageName: "globe" ) - //InformationDetailView(title: L10n.SettingsIdentityServer.optionalContactSync, - // subTitle: L10n.SettingsIdentityServer.optionalContactSyncText, - // imageName: "person") + InformationDetailView(title: L10n.SettingsIdentityServer.optionalContactSync, + subTitle: L10n.SettingsIdentityServer.optionalContactSyncText, + imageName: "person") } .padding(.horizontal) } @@ -163,7 +163,7 @@ struct IdentityServerInfoView: View { struct IdentityServerSettingsView: View { @AppStorage("identityServerBool") private var identityServerBool: Bool = false @AppStorage("identityServer") private var identityServer: String = "https://vector.im" - @AppStorage("syncContacts") private var syncContacts: Bool = false + @AppStorage("locSyncContacts") private var locSyncContacts: Bool = false @EnvironmentObject var store: AccountStore @@ -192,7 +192,11 @@ struct IdentityServerSettingsView: View { ) if identityServerBool { TextField(L10n.SettingsIdentityServer.url, text: $identityServer) - //Toggle(L10n.SettingsIdentityServer.contactSync, isOn: $syncContacts).onChange(of: syncContacts, perform: syncContacts(isSync:)) + Toggle( + L10n.SettingsIdentityServer.contactSync, + isOn: Binding(get: { Contacts.hasPermission() && locSyncContacts }, set: { status in locSyncContacts = status }) + ) + .onChange(of: locSyncContacts, perform: syncContacts(isSync:)) } } } @@ -201,12 +205,12 @@ struct IdentityServerSettingsView: View { if isSync { store.setIdentityService() } else { - // Revoke Contact Sync - syncContacts = false + locSyncContacts = false } } private func syncContacts(isSync: Bool) { + print(isSync) if isSync { let contacts = Contacts.getAllContacts() contacts.forEach { (contact) in diff --git a/Nio/Info.plist b/Nio/Supporting Files/Info.plist similarity index 95% rename from Nio/Info.plist rename to Nio/Supporting Files/Info.plist index 41fa4419..35598db3 100644 --- a/Nio/Info.plist +++ b/Nio/Supporting Files/Info.plist @@ -84,7 +84,7 @@ DevelopmentTeam $(DEVELOPMENT_TEAM) NSContactsUsageDescription - The contact information is used to find a matrix id on the identity server + The contact information is used to find a Matrix ID on the Identity Server for all your contacts. UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait diff --git a/Nio/Supporting Files/en.lproj/InfoPlist.strings b/Nio/Supporting Files/en.lproj/InfoPlist.strings new file mode 100644 index 00000000..6f2d37e9 --- /dev/null +++ b/Nio/Supporting Files/en.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"NSContactsUsageDescription" = "The contact information is used to find a Matrix ID on the Identity Server for all your contacts."; diff --git a/Nio/Supporting Files/nl.lproj/InfoPlist.strings b/Nio/Supporting Files/nl.lproj/InfoPlist.strings new file mode 100644 index 00000000..05291211 --- /dev/null +++ b/Nio/Supporting Files/nl.lproj/InfoPlist.strings @@ -0,0 +1 @@ +"NSContactsUsageDescription" = "The contact informatie wordt gebruikt om een Matrix ID op te halen bij de Identity Server voor al uw contacten."; diff --git a/NioKit/Extensions/Contacts.swift b/NioKit/Extensions/Contacts.swift index f5dca5b5..9c81e8ba 100644 --- a/NioKit/Extensions/Contacts.swift +++ b/NioKit/Extensions/Contacts.swift @@ -10,6 +10,22 @@ import Foundation import Contacts public class Contacts { + public static func hasPermission() -> Bool { + switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) { + case .authorized: + return true + case .notDetermined: + return true + default: + print((Bundle.main.infoDictionary?["CFBundleName"] as? String) ?? "") + CNContactStore().requestAccess(for: CNEntityType.contacts) { result, error in + print(result) + print(error) + } + return false + } + } + public static func getContact() -> String { let store = CNContactStore() do { diff --git a/swiftgen.yml b/swiftgen.yml index 54341918..e2433900 100644 --- a/swiftgen.yml +++ b/swiftgen.yml @@ -1,6 +1,6 @@ strings: inputs: "Nio/Supporting Files/en.lproj" - filter: .+\.strings$ + filter: (\b(Localizable)\b)\.strings$ outputs: - templateName: structured-swift4 output: Nio/Generated/Strings.swift From f9cf6dedfc1a6561d5ac5d5036bb8deb47c637a8 Mon Sep 17 00:00:00 2001 From: Stefan Hofman Date: Tue, 23 Mar 2021 02:12:31 +0100 Subject: [PATCH 06/12] Working contact sync --- Nio/Generated/Strings.swift | 10 +++ Nio/NewConversation/NewConversationView.swift | 69 +++++++++++++++++-- Nio/Settings/IdentityServerSettings.swift | 61 ++++++++-------- Nio/Settings/SettingsView.swift | 3 +- .../en.lproj/Localizable.strings | 5 ++ NioKit/Extensions/Contacts.swift | 46 ++++++++----- NioKit/Session/AccountStore.swift | 34 ++++++++- 7 files changed, 175 insertions(+), 53 deletions(-) diff --git a/Nio/Generated/Strings.swift b/Nio/Generated/Strings.swift index af2f0f1d..673a5b3c 100644 --- a/Nio/Generated/Strings.swift +++ b/Nio/Generated/Strings.swift @@ -170,6 +170,8 @@ internal enum L10n { internal static let alertFailed = L10n.tr("Localizable", "new-conversation.alert-failed") /// Cancel internal static let cancel = L10n.tr("Localizable", "new-conversation.cancel") + /// Contacts on Matrix + internal static let contactsMatrix = L10n.tr("Localizable", "new-conversation.contacts-matrix") /// Start Chat internal static let createRoom = L10n.tr("Localizable", "new-conversation.create-room") /// Done @@ -322,6 +324,14 @@ internal enum L10n { internal static let optionalContactSync = L10n.tr("Localizable", "settings-identity-server.optional-contact-sync") /// When activating contact syncronization, Nio will periodically send the email addresses and phone numbers of all your contacts to the identity server to see if that contact has a linked Matrix ID. This contact information is never stored or shared by Nio. internal static let optionalContactSyncText = L10n.tr("Localizable", "settings-identity-server.optional-contact-sync-text") + /// Please go to Settings and turn on the permissions. + internal static let permissionAlertBody = L10n.tr("Localizable", "settings-identity-server.permission-alert-body") + /// No permissions + internal static let permissionAlertTitle = L10n.tr("Localizable", "settings-identity-server.permission-alert-title") + /// Cancel + internal static let permissionCancelButton = L10n.tr("Localizable", "settings-identity-server.permission-cancel-button") + /// Settings + internal static let permissionSettingsButton = L10n.tr("Localizable", "settings-identity-server.permission-settings-button") /// Identity Server internal static let title = L10n.tr("Localizable", "settings-identity-server.title") /// Enable Identity Server diff --git a/Nio/NewConversation/NewConversationView.swift b/Nio/NewConversation/NewConversationView.swift index 6f6834c4..1cab3db9 100644 --- a/Nio/NewConversation/NewConversationView.swift +++ b/Nio/NewConversation/NewConversationView.swift @@ -13,9 +13,24 @@ enum UserStatus { struct NewConversationContainerView: View { @EnvironmentObject private var store: AccountStore @Binding var createdRoomId: ObjectIdentifier? + @AppStorage("matrixUsers") private var matrixUsersJSON: String = "" var body: some View { - NewConversationView(store: store, createdRoomId: $createdRoomId) + NewConversationView( + store: store, + createdRoomId: $createdRoomId, + matrixUsers: { () -> [MatrixUser] in + do { + var matrixArray: [MatrixUser] = try JSONDecoder().decode( + [MatrixUser].self, from: matrixUsersJSON.data(using: .utf8) ?? Data() + ) + matrixArray.sort(by: { (lhs, rhs) in return lhs.getFirstName() > rhs.getFirstName() }) + return matrixArray + } catch { + return [] + } + }() + ) } } @@ -29,7 +44,7 @@ private struct NewConversationView: View { @State private var usersVerified: [UserStatus] = [UserStatus.unkown] @State private var numCalls: Int = 0 - + @State private var isRetrieving = false #if !os(macOS) @@ -42,6 +57,8 @@ private struct NewConversationView: View { @Binding var createdRoomId: ObjectIdentifier? @State private var errorMessage: String? + let matrixUsers: [MatrixUser] + private var usersFooter: some View { Text("\(L10n.NewConversation.forExample) \(store?.session?.myUserId ?? "@username:server.org")") } @@ -69,7 +86,11 @@ private struct NewConversationView: View { } // proxy binding prevents an index out of range crash on delete TextField( - (identityServerBool ? L10n.NewConversation.usernamePlaceholderExtended : L10n.NewConversation.usernamePlaceholder), + ( + identityServerBool ? + L10n.NewConversation.usernamePlaceholderExtended : + L10n.NewConversation.usernamePlaceholder + ), text: Binding(get: { users[index] }, set: { users[index] = $0; findUser(userIndex: index) }) ) .disableAutocorrection(true) @@ -111,12 +132,52 @@ private struct NewConversationView: View { Spacer() ProgressView() .opacity(isWaiting ? 1.0 : 0.0) + } } .alert(item: $errorMessage) { errorMessage in Alert(title: Text(verbatim: L10n.NewConversation.alertFailed), message: Text(errorMessage)) } + + Section(header: Text(L10n.NewConversation.contactsMatrix)) { + ForEach(0.. String { + return self.firstName ?? "" + } + + public func getLastName() -> String { + return self.lastName ?? "" + } + + public func getMatrixID() -> String { + return self.matrixID ?? "" + } +} + public class Contacts { + public static func hasPermission() -> Bool { switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) { case .authorized: @@ -17,34 +42,17 @@ public class Contacts { case .notDetermined: return true default: - print((Bundle.main.infoDictionary?["CFBundleName"] as? String) ?? "") - CNContactStore().requestAccess(for: CNEntityType.contacts) { result, error in - print(result) - print(error) - } return false } } - - public static func getContact() -> String { - let store = CNContactStore() - do { - let predicate = CNContact.predicateForContacts(matchingName: "Hofman") - let keysToFetch = [CNContactGivenNameKey, CNContactFamilyNameKey] as [CNKeyDescriptor] - let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch) - return "found" - } catch { - return "not found" - } - } - + public static func getAllContacts() -> [CNContact] { var contacts = [CNContact]() let store = CNContactStore() do { let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactEmailAddressesKey] as [CNKeyDescriptor] let request = CNContactFetchRequest(keysToFetch: keys) - try store.enumerateContacts(with: request) { (contact, stop) in + try store.enumerateContacts(with: request) { (contact, _) in // Array containing all unified contacts from everywhere if contact.emailAddresses.count > 0 { contacts.append(contact) diff --git a/NioKit/Session/AccountStore.swift b/NioKit/Session/AccountStore.swift index 64d19008..b692332b 100644 --- a/NioKit/Session/AccountStore.swift +++ b/NioKit/Session/AccountStore.swift @@ -15,6 +15,8 @@ public enum LoginState { public class AccountStore: ObservableObject { @AppStorage("identityServer") private var identityServer: String = "https://vector.im" @AppStorage("identityServerBool") private var identityServerBool: Bool = false + @AppStorage("matrixUsers") private var matrixUsersJSON: String = "" + @AppStorage("locSyncContacts") private var locSyncContacts: Bool = false public var client: MXRestClient? public var session: MXSession? @@ -130,7 +132,11 @@ public class AccountStore: ObservableObject { if self.identityServerBool { self.setIdentityService() } - + + if self.locSyncContacts && Contacts.hasPermission(){ + self.updateMatrixContacts() + } + self.session!.setStore(fileStore!) { response in switch response { case .failure(let error): @@ -219,4 +225,30 @@ public class AccountStore: ObservableObject { homeserverRestClient: self.client! ) } + + public func updateMatrixContacts() { + var matrixUsers: [MatrixUser] = [] + let contacts = Contacts.getAllContacts() + contacts.forEach { (contact) in + var mx3pids: [MX3PID] = [] + contact.emailAddresses.forEach { (email) in + mx3pids.append(MX3PID.init(medium: MX3PID.Medium.email, address: email.value as String)) + } + self.identityService?.lookup3PIDs(mx3pids) { [self] response in + response.value?.forEach({ (responseItem: (key: MX3PID, value: String)) in + do { + matrixUsers.append( + MatrixUser( + firstName: contact.givenName, + lastName: contact.familyName, + matrixID: responseItem.value + ) + ) + let jsonData = try JSONEncoder().encode(matrixUsers) + self.matrixUsersJSON = String(data: jsonData, encoding: .utf8)! + } catch { print(error) } + }) + } + } + } } From d5a57610954b3d8a7a3c483c31f24c8b61f48c6b Mon Sep 17 00:00:00 2001 From: Stefan Hofman Date: Tue, 23 Mar 2021 11:37:26 +0100 Subject: [PATCH 07/12] Fix linter error --- Nio/Settings/IdentityServerSettings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nio/Settings/IdentityServerSettings.swift b/Nio/Settings/IdentityServerSettings.swift index 94cbc61b..61c3f545 100644 --- a/Nio/Settings/IdentityServerSettings.swift +++ b/Nio/Settings/IdentityServerSettings.swift @@ -13,7 +13,7 @@ struct IdentityServerSettingsContainerView: View { struct ButtonModifier: ViewModifier { @AppStorage("accentColor") private var accentColor: Color = .purple - + func body(content: Content) -> some View { content .foregroundColor(.white) From 62c9eb69ee5062de3939920be43011176d3a9949 Mon Sep 17 00:00:00 2001 From: Stefan Hofman Date: Fri, 26 Mar 2021 11:45:25 +0100 Subject: [PATCH 08/12] Contact persistency --- Nio/Settings/IdentityServerSettings.swift | 7 ++++++ NioKit/Extensions/Contacts.swift | 12 ++++----- NioKit/Session/AccountStore.swift | 30 ++++++++++++++++------- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/Nio/Settings/IdentityServerSettings.swift b/Nio/Settings/IdentityServerSettings.swift index 61c3f545..ed9d6ee5 100644 --- a/Nio/Settings/IdentityServerSettings.swift +++ b/Nio/Settings/IdentityServerSettings.swift @@ -163,6 +163,7 @@ struct IdentityServerSettingsView: View { @AppStorage("identityServerBool") private var identityServerBool: Bool = false @AppStorage("identityServer") private var identityServer: String = "https://vector.im" @AppStorage("locSyncContacts") private var locSyncContacts: Bool = false + @AppStorage("matrixUsers") private var matrixUsersJSON: String = "" @EnvironmentObject var store: AccountStore @@ -216,6 +217,12 @@ struct IdentityServerSettingsView: View { } })) } + Button(action: { + self.matrixUsersJSON = "" + self.syncContacts(isSync: locSyncContacts) + }, label: { + Text(verbatim: "Force Sync") + }) } } } diff --git a/NioKit/Extensions/Contacts.swift b/NioKit/Extensions/Contacts.swift index 6883aa1e..0fe86cf1 100644 --- a/NioKit/Extensions/Contacts.swift +++ b/NioKit/Extensions/Contacts.swift @@ -10,9 +10,9 @@ import Foundation import Contacts public struct MatrixUser: Codable { - let firstName: String? - let lastName: String? - let matrixID: String? + let firstName: String + let lastName: String + let matrixID: String public init(firstName: String, lastName: String, matrixID: String) { self.firstName = firstName @@ -21,15 +21,15 @@ public struct MatrixUser: Codable { } public func getFirstName() -> String { - return self.firstName ?? "" + return self.firstName } public func getLastName() -> String { - return self.lastName ?? "" + return self.lastName } public func getMatrixID() -> String { - return self.matrixID ?? "" + return self.matrixID } } diff --git a/NioKit/Session/AccountStore.swift b/NioKit/Session/AccountStore.swift index b692332b..ba08105c 100644 --- a/NioKit/Session/AccountStore.swift +++ b/NioKit/Session/AccountStore.swift @@ -227,7 +227,15 @@ public class AccountStore: ObservableObject { } public func updateMatrixContacts() { - var matrixUsers: [MatrixUser] = [] + var matrixUsers: [MatrixUser] = { () -> [MatrixUser] in + do { + return try JSONDecoder().decode( + [MatrixUser].self, from: matrixUsersJSON.data(using: .utf8) ?? Data() + ) + } catch { + return [] + } + }() let contacts = Contacts.getAllContacts() contacts.forEach { (contact) in var mx3pids: [MX3PID] = [] @@ -237,15 +245,19 @@ public class AccountStore: ObservableObject { self.identityService?.lookup3PIDs(mx3pids) { [self] response in response.value?.forEach({ (responseItem: (key: MX3PID, value: String)) in do { - matrixUsers.append( - MatrixUser( - firstName: contact.givenName, - lastName: contact.familyName, - matrixID: responseItem.value + if (!matrixUsers.contains(where: { user in + return user.matrixID == responseItem.value + })) { + matrixUsers.append( + MatrixUser( + firstName: contact.givenName, + lastName: contact.familyName, + matrixID: responseItem.value + ) ) - ) - let jsonData = try JSONEncoder().encode(matrixUsers) - self.matrixUsersJSON = String(data: jsonData, encoding: .utf8)! + let jsonData = try JSONEncoder().encode(matrixUsers) + self.matrixUsersJSON = String(data: jsonData, encoding: .utf8)! + } } catch { print(error) } }) } From c8b1e36ac113df602f9242c1718f027600e82957 Mon Sep 17 00:00:00 2001 From: Stefan Hofman Date: Fri, 26 Mar 2021 15:04:59 +0100 Subject: [PATCH 09/12] Fix reverse spinner --- Nio/NewConversation/NewConversationView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Nio/NewConversation/NewConversationView.swift b/Nio/NewConversation/NewConversationView.swift index 1cab3db9..383bcea3 100644 --- a/Nio/NewConversation/NewConversationView.swift +++ b/Nio/NewConversation/NewConversationView.swift @@ -83,6 +83,9 @@ private struct NewConversationView: View { .onAppear { self.isRetrieving.toggle() } + .onDisappear { + self.isRetrieving.toggle() + } } // proxy binding prevents an index out of range crash on delete TextField( From f73cd3ff82b278d6030075f600a3a29b16378a71 Mon Sep 17 00:00:00 2001 From: Stefan Hofman Date: Fri, 26 Mar 2021 18:23:15 +0100 Subject: [PATCH 10/12] Linter Co-authored-by: Kilian Koeltzsch --- NioKit/Extensions/Contacts.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/NioKit/Extensions/Contacts.swift b/NioKit/Extensions/Contacts.swift index 0fe86cf1..8d894803 100644 --- a/NioKit/Extensions/Contacts.swift +++ b/NioKit/Extensions/Contacts.swift @@ -19,7 +19,6 @@ public struct MatrixUser: Codable { self.lastName = lastName self.matrixID = matrixID } - public func getFirstName() -> String { return self.firstName } From be9706e0956839484617c12fb3a76e56872de10d Mon Sep 17 00:00:00 2001 From: Stefan Hofman Date: Wed, 31 Mar 2021 13:23:05 +0200 Subject: [PATCH 11/12] Typo enum UserStatus Co-authored-by: Kilian Koeltzsch Typo enum UserStatus Co-authored-by: Kilian Koeltzsch Typo enum UserStatus Co-authored-by: Kilian Koeltzsch --- Nio/NewConversation/NewConversationView.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Nio/NewConversation/NewConversationView.swift b/Nio/NewConversation/NewConversationView.swift index 383bcea3..7159dbbe 100644 --- a/Nio/NewConversation/NewConversationView.swift +++ b/Nio/NewConversation/NewConversationView.swift @@ -4,7 +4,7 @@ import MatrixSDK import NioKit enum UserStatus { - case unkown // Validity of user is still unkown. + case unknown // Validity of user is still unknown. case notValid // User is not valid. case valid // User is valid. case retrieving // Validity of user is being retrieved from identity server. @@ -68,7 +68,7 @@ private struct NewConversationView: View { Section(footer: usersFooter) { ForEach(0.. 1) - || usersVerified.contains(UserStatus.notValid) - || usersVerified.contains(UserStatus.retrieving) - || usersVerified.contains(UserStatus.unkown) + users.contains("") + || (roomName.isEmpty && users.count > 1) + || usersVerified.contains(UserStatus.notValid) + || usersVerified.contains(UserStatus.retrieving) + || usersVerified.contains(UserStatus.unknown) ) #endif Spacer() From 2bc6f323650264cb12ac8e98c783dcf9e57bc5f3 Mon Sep 17 00:00:00 2001 From: Stefan Hofman Date: Wed, 31 Mar 2021 13:56:41 +0200 Subject: [PATCH 12/12] Review remarks --- Nio/Generated/Strings.swift | 8 +++++--- Nio/NewConversation/NewConversationView.swift | 6 +++--- Nio/Settings/IdentityServerSettings.swift | 6 +++++- Nio/Supporting Files/en.lproj/Localizable.strings | 4 ++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Nio/Generated/Strings.swift b/Nio/Generated/Strings.swift index 673a5b3c..921dcc15 100644 --- a/Nio/Generated/Strings.swift +++ b/Nio/Generated/Strings.swift @@ -312,8 +312,10 @@ internal enum L10n { internal static let data = L10n.tr("Localizable", "settings-identity-server.data") /// Data & Privacy internal static let dataPrivacy = L10n.tr("Localizable", "settings-identity-server.data-privacy") - /// By using the identity server, your email address and phone number will be sent to the identity server and will be linked to your Matrix ID (@example.nio.chat). - internal static let dataText = L10n.tr("Localizable", "settings-identity-server.data-text") + /// By using the identity server, your email address and phone number will be sent to the identity server and will be linked to your Matrix ID (%@). + internal static func dataText(_ p1: Any) -> String { + return L10n.tr("Localizable", "settings-identity-server.data-text", String(describing: p1)) + } /// Learn More internal static let learnMore = L10n.tr("Localizable", "settings-identity-server.learn-more") /// Match @@ -322,7 +324,7 @@ internal enum L10n { internal static let matchText = L10n.tr("Localizable", "settings-identity-server.match-text") /// (Optional) Contact Sync internal static let optionalContactSync = L10n.tr("Localizable", "settings-identity-server.optional-contact-sync") - /// When activating contact syncronization, Nio will periodically send the email addresses and phone numbers of all your contacts to the identity server to see if that contact has a linked Matrix ID. This contact information is never stored or shared by Nio. + /// When activating contact syncronization, Nio will periodically send the email addresses and phone numbers of all your contacts to the identity server to see if that contact has a linked Matrix ID. This contact information is never stored or shared with other parties by Nio. internal static let optionalContactSyncText = L10n.tr("Localizable", "settings-identity-server.optional-contact-sync-text") /// Please go to Settings and turn on the permissions. internal static let permissionAlertBody = L10n.tr("Localizable", "settings-identity-server.permission-alert-body") diff --git a/Nio/NewConversation/NewConversationView.swift b/Nio/NewConversation/NewConversationView.swift index 7159dbbe..b0151cd9 100644 --- a/Nio/NewConversation/NewConversationView.swift +++ b/Nio/NewConversation/NewConversationView.swift @@ -41,12 +41,12 @@ private struct NewConversationView: View { let store: AccountStore? @State private var users = [""] - @State private var usersVerified: [UserStatus] = [UserStatus.unkown] + @State private var usersVerified: [UserStatus] = [UserStatus.unknown] @State private var numCalls: Int = 0 @State private var isRetrieving = false - + #if !os(macOS) @State private var editMode = EditMode.inactive #endif @@ -238,7 +238,7 @@ private struct NewConversationView: View { private func addUser() { withAnimation { - usersVerified.append(UserStatus.unkown) + usersVerified.append(UserStatus.unknown) users.append("") } } diff --git a/Nio/Settings/IdentityServerSettings.swift b/Nio/Settings/IdentityServerSettings.swift index ed9d6ee5..c9ce7feb 100644 --- a/Nio/Settings/IdentityServerSettings.swift +++ b/Nio/Settings/IdentityServerSettings.swift @@ -61,10 +61,12 @@ struct InformationDetailView: View { .font(.headline) .foregroundColor(.primary) .accessibility(addTraits: .isHeader) + .textCase(.none) Text(subTitle) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) + .textCase(.none) } } .padding(.top) @@ -72,11 +74,13 @@ struct InformationDetailView: View { } struct InformationContainerView: View { + @EnvironmentObject private var store: AccountStore + var body: some View { VStack(alignment: .leading) { InformationDetailView( title: L10n.SettingsIdentityServer.data, - subTitle: L10n.SettingsIdentityServer.dataText, + subTitle: L10n.SettingsIdentityServer.dataText(store.session?.myUserId ?? "@username:server.org"), imageName: "arrow.up.doc" ) diff --git a/Nio/Supporting Files/en.lproj/Localizable.strings b/Nio/Supporting Files/en.lproj/Localizable.strings index 61ec01be..974a73f8 100644 --- a/Nio/Supporting Files/en.lproj/Localizable.strings +++ b/Nio/Supporting Files/en.lproj/Localizable.strings @@ -90,13 +90,13 @@ "settings-identity-server.learn-more" = "Learn More"; "settings-identity-server.data-privacy" = "Data & Privacy"; "settings-identity-server.data" = "Data"; -"settings-identity-server.data-text" = "By using the identity server, your email address and phone number will be sent to the identity server and will be linked to your Matrix ID (@example.nio.chat)."; +"settings-identity-server.data-text" = "By using the identity server, your email address and phone number will be sent to the identity server and will be linked to your Matrix ID (%@)."; "settings-identity-server.match" = "Match"; "settings-identity-server.match-text" = "Any user can retrieve your Matrix ID by entering your email address or phone number, but cannot find your email address or phone number by entering your Matrix ID."; "settings-identity-server.closed-federation" = "Closed Federation"; "settings-identity-server.closed-federation-text" = "At the moment, the identity servers are in a closed federation configuration. This means that there are only two identity servers (matrix.org, vector.im) and all data uploaded to one is copied to the other."; "settings-identity-server.optional-contact-sync" = "(Optional) Contact Sync"; -"settings-identity-server.optional-contact-sync-text" = "When activating contact syncronization, Nio will periodically send the email addresses and phone numbers of all your contacts to the identity server to see if that contact has a linked Matrix ID. This contact information is never stored or shared by Nio."; +"settings-identity-server.optional-contact-sync-text" = "When activating contact syncronization, Nio will periodically send the email addresses and phone numbers of all your contacts to the identity server to see if that contact has a linked Matrix ID. This contact information is never stored or shared with other parties by Nio."; "new-conversation.title-chat" = "New Chat"; "new-conversation.title-room" = "New Room"; "new-conversation.username-placeholder" = "Matrix ID";