From 72c0c61a86087e5734cdf5ba9aaa67e06fef1152 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Sun, 16 Jul 2023 14:55:31 +0400 Subject: [PATCH] ios: delivery receipts (#2701) * ios: delivery receipts wip * remove state variable * fix 1 toggle * fix 2nd toggle * undo some changes, remove prints, remove commented code * remove diff * icon color * comment * refactor, double tick * remove color from spaces * update messages * do not show Enable delievery receipts screen if any of the profiles was enabled * update footer * fix text * better double ticks * softer double tick * improve double ticks * a bit bigger gap in double tick --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/ios/Shared/Model/SimpleXAPI.swift | 13 +++ apps/ios/Shared/Views/Chat/ChatInfoView.swift | 35 +++++- .../Views/Chat/ChatItem/CIMetaView.swift | 33 +++++- .../Views/Onboarding/WhatsNewView.swift | 2 +- .../Views/UserSettings/PrivacySettings.swift | 107 ++++++++++++++++-- .../SetDeliveryReceiptsView.swift | 54 ++++++--- apps/ios/SimpleX.xcodeproj/project.pbxproj | 40 +++---- apps/ios/SimpleXChat/APITypes.swift | 24 +++- apps/ios/SimpleXChat/ChatTypes.swift | 26 ++++- 9 files changed, 280 insertions(+), 54 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index ed5d18b1f..0a7aa00a5 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -159,6 +159,18 @@ func apiSetActiveUserAsync(_ userId: Int64, viewPwd: String?) async throws -> Us throw r } +func apiSetAllContactReceipts(enable: Bool) async throws { + let r = await chatSendCmd(.setAllContactReceipts(enable: enable)) + if case .cmdOk = r { return } + throw r +} + +func apiSetUserContactReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) async throws { + let r = await chatSendCmd(.apiSetUserContactReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings)) + if case .cmdOk = r { return } + throw r +} + func apiHideUser(_ userId: Int64, viewPwd: String) async throws -> User { try await setUserPrivacy_(.apiHideUser(userId: userId, viewPwd: viewPwd)) } @@ -1121,6 +1133,7 @@ func startChat(refreshInvitations: Bool = true) throws { m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1 ? .step3_CreateSimpleXAddress : savedOnboardingStage + // TODO don't show on first start if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() { m.setDeliveryReceipts = true } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index c189fd220..63fe6f072 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -71,6 +71,21 @@ enum SendReceipts: Identifiable, Hashable { case let .userDefault(on): return on ? "default (yes)" : "default (no)" } } + + func bool() -> Bool? { + switch self { + case .yes: return true + case .no: return false + case .userDefault: return nil + } + } + + static func fromBool(_ enable: Bool?, userDefault def: Bool) -> SendReceipts { + if let enable = enable { + return enable ? .yes : .no + } + return .userDefault(def) + } } struct ChatInfoView: View { @@ -84,7 +99,8 @@ struct ChatInfoView: View { @Binding var connectionCode: String? @FocusState private var aliasTextFieldFocused: Bool @State private var alert: ChatInfoViewAlert? = nil - @State private var sendReceipts = SendReceipts.yes + @State private var sendReceipts = SendReceipts.userDefault(true) + @State private var sendReceiptsUserDefault = true @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false enum ChatInfoViewAlert: Identifiable { @@ -200,6 +216,12 @@ struct ChatInfoView: View { .navigationBarHidden(true) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .onAppear { + if let currentUser = chatModel.currentUser { + sendReceiptsUserDefault = currentUser.sendRcptsContacts + } + sendReceipts = SendReceipts.fromBool(contact.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault) + } .alert(item: $alert) { alertItem in switch(alertItem) { case .deleteContactAlert: return deleteContactAlert() @@ -315,13 +337,22 @@ struct ChatInfoView: View { private func sendReceiptsOption() -> some View { Picker(selection: $sendReceipts) { - ForEach([.yes, .no, .userDefault(true)]) { (opt: SendReceipts) in + ForEach([.yes, .no, .userDefault(sendReceiptsUserDefault)]) { (opt: SendReceipts) in Text(opt.text) } } label: { Label("Send receipts", systemImage: "checkmark.message") } .frame(height: 36) + .onChange(of: sendReceipts) { _ in + setSendReceipts() + } + } + + private func setSendReceipts() { + var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults + chatSettings.sendRcpts = sendReceipts.bool() + updateChatSettings(chat, chatSettings: chatSettings) } private func synchronizeConnectionButton() -> some View { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index dbcee3fbb..de7b3e251 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -18,12 +18,30 @@ struct CIMetaView: View { if chatItem.isDeletedContent { chatItem.timestampText.font(.caption).foregroundColor(metaColor) } else { - ciMetaText(chatItem.meta, chatTTL: chat.chatInfo.timedMessagesTTL, color: metaColor) + let meta = chatItem.meta + let ttl = chat.chatInfo.timedMessagesTTL + switch meta.itemStatus { + case .sndSent: + ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .sent) + case .sndRcvd: + ZStack { + ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd1) + ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd2) + } + default: + ciMetaText(meta, chatTTL: ttl, color: metaColor) + } } } } -func ciMetaText(_ meta: CIMeta, chatTTL: Int?, color: Color = .clear, transparent: Bool = false) -> Text { +enum SentCheckmark { + case sent + case rcvd1 + case rcvd2 +} + +func ciMetaText(_ meta: CIMeta, chatTTL: Int?, color: Color = .clear, transparent: Bool = false, sent: SentCheckmark? = nil) -> Text { var r = Text("") if meta.itemEdited { r = r + statusIconText("pencil", color) @@ -37,7 +55,16 @@ func ciMetaText(_ meta: CIMeta, chatTTL: Int?, color: Color = .clear, transparen r = r + Text(" ") } if let (icon, statusColor) = meta.statusIcon(color) { - r = r + statusIconText(icon, transparent ? .clear : statusColor) + Text(" ") + let t = Text(Image(systemName: icon)).font(.caption2) + let gap = Text(" ").kerning(-1.25) + let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) + switch sent { + case nil: r = r + t1 + case .sent: r = r + t1 + gap + case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : color.opacity(0.67)) + gap + case .rcvd2: r = r + gap + t1 + } + r = r + Text(" ") } else if !meta.disappearing { r = r + statusIconText("circlebadge.fill", .clear) + Text(" ") } diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index f69e537d8..cc7e36559 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -248,7 +248,7 @@ private let versionDescriptions: [VersionDescription] = [ FeatureDescription( icon: "gift", title: "A few more things", - description: "- more stabile message delivery.\n- a bit better groups.\n- and more!" + description: "- more stable message delivery.\n- a bit better groups.\n- and more!" ), ] ) diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 14ccf950f..789a5330e 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -10,12 +10,28 @@ import SwiftUI import SimpleXChat struct PrivacySettings: View { + @EnvironmentObject var m: ChatModel @AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true @AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true @State private var simplexLinkMode = privacySimplexLinkModeDefault.get() @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @State private var currentLAMode = privacyLocalAuthModeDefault.get() + @State private var contactReceipts = false + @State private var contactReceiptsReset = false + @State private var contactReceiptsOverrides = 0 + @State private var contactReceiptsDialogue = false + @State private var alert: PrivacySettingsViewAlert? + + enum PrivacySettingsViewAlert: Identifiable { + case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + + var id: String { + switch self { + case let .error(title, _): return "error \(title)" + } + } + } var body: some View { VStack { @@ -71,22 +87,99 @@ struct PrivacySettings: View { Section { settingsRow("person") { - Toggle("Contacts", isOn: $useLinkPreviews) + Toggle("Contacts", isOn: $contactReceipts) } - settingsRow("person.2") { - Toggle("Small groups (max 10)", isOn: Binding.constant(false)) - } - .foregroundColor(.secondary) - .disabled(true) +// settingsRow("person.2") { +// Toggle("Small groups (max 20)", isOn: Binding.constant(false)) +// } } header: { Text("Send delivery receipts to") } footer: { VStack(alignment: .leading) { Text("These settings are for your current profile **\(ChatModel.shared.currentUser?.displayName ?? "")**.") - Text("They can be overridden in contact and group settings") + Text("They can be overridden in contact settings") } .frame(maxWidth: .infinity, alignment: .leading) } + .confirmationDialog(contactReceiptsDialogTitle, isPresented: $contactReceiptsDialogue, titleVisibility: .visible) { + Button(contactReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") { + setSendReceiptsContacts(contactReceipts, clearOverrides: false) + } + Button(contactReceipts ? "Enable for all" : "Disable for all", role: .destructive) { + setSendReceiptsContacts(contactReceipts, clearOverrides: true) + } + Button("Cancel", role: .cancel) { + contactReceiptsReset = true + contactReceipts.toggle() + } + } + } + } + .onChange(of: contactReceipts) { _ in // sometimes there is race with onAppear + if contactReceiptsReset { + contactReceiptsReset = false + } else { + setOrAskSendReceiptsContacts(contactReceipts) + } + } + .onAppear { + if let u = m.currentUser, contactReceipts != u.sendRcptsContacts { + contactReceiptsReset = true + contactReceipts = u.sendRcptsContacts + } + } + .alert(item: $alert) { alert in + switch alert { + case let .error(title, error): + return Alert(title: Text(title), message: Text(error)) + } + } + } + + private func setOrAskSendReceiptsContacts(_ enable: Bool) { + contactReceiptsOverrides = m.chats.reduce(0) { count, chat in + let sendRcpts = chat.chatInfo.contact?.chatSettings.sendRcpts + return count + (sendRcpts == nil || sendRcpts == enable ? 0 : 1) + } + if contactReceiptsOverrides == 0 { + setSendReceiptsContacts(enable, clearOverrides: false) + } else { + contactReceiptsDialogue = true + } + } + + private var contactReceiptsDialogTitle: LocalizedStringKey { + contactReceipts + ? "Sending receipts is disabled for \(contactReceiptsOverrides) contacts" + : "Sending receipts is enabled for \(contactReceiptsOverrides) contacts" + } + + private func setSendReceiptsContacts(_ enable: Bool, clearOverrides: Bool) { + Task { + do { + if let currentUser = m.currentUser { + let userMsgReceiptSettings = UserMsgReceiptSettings(enable: enable, clearOverrides: clearOverrides) + try await apiSetUserContactReceipts(currentUser.userId, userMsgReceiptSettings: userMsgReceiptSettings) + privacyDeliveryReceiptsSet.set(true) + await MainActor.run { + var updatedUser = currentUser + updatedUser.sendRcptsContacts = enable + m.updateUser(updatedUser) + if clearOverrides { + m.chats.forEach { chat in + if var contact = chat.chatInfo.contact { + let sendRcpts = contact.chatSettings.sendRcpts + if sendRcpts != nil && sendRcpts != enable { + contact.chatSettings.sendRcpts = nil + m.updateContact(contact) + } + } + } + } + } + } + } catch let error { + alert = .error(title: "Error setting delivery receipts!", error: "Error: \(responseError(error))") } } } diff --git a/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift b/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift index eee204018..3990d0178 100644 --- a/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct SetDeliveryReceiptsView: View { @EnvironmentObject var m: ChatModel @@ -22,36 +23,61 @@ struct SetDeliveryReceiptsView: View { Spacer() Button("Enable") { - m.setDeliveryReceipts = false + Task { + do { + try await apiSetAllContactReceipts(enable: true) + await MainActor.run { + m.setDeliveryReceipts = false + privacyDeliveryReceiptsSet.set(true) + } + } catch let error { + AlertManager.shared.showAlert(Alert( + title: Text("Error enabling delivery receipts!"), + message: Text("Error: \(responseError(error))") + )) + await MainActor.run { + m.setDeliveryReceipts = false + } + } + } } .font(.largeTitle) Group { if m.users.count > 1 { - Text("Delivery receipts will be enabled for all contacts in all visible chat profiles.") + Text("Sending delivery receipts will be enabled for all contacts in all visible chat profiles.") } else { - Text("Delivery receipts will be enabled for all contacts.") + Text("Sending delivery receipts will be enabled for all contacts.") } } .multilineTextAlignment(.center) Spacer() - Button("Enable later via Settings") { - AlertManager.shared.showAlert(Alert( - title: Text("Delivery receipts are disabled!"), - message: Text("You can enable them later via app Privacy & Security settings."), - primaryButton: .default(Text("Don't show again")) { - m.setDeliveryReceipts = false - }, - secondaryButton: .default(Text("Ok")) { - m.setDeliveryReceipts = false + VStack(spacing: 8) { + Button { + AlertManager.shared.showAlert(Alert( + title: Text("Delivery receipts are disabled!"), + message: Text("You can enable them later via app Privacy & Security settings."), + primaryButton: .default(Text("Don't show again")) { + m.setDeliveryReceipts = false + privacyDeliveryReceiptsSet.set(true) + }, + secondaryButton: .default(Text("Ok")) { + m.setDeliveryReceipts = false + } + )) + } label: { + HStack { + Text("Don't enable") + Image(systemName: "chevron.right") } - )) + } + Text("You can enable later via Settings").font(.footnote) } } .padding() .padding(.horizontal) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(uiColor: .systemBackground)) } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 04a7f1f9f..a8f239961 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -160,11 +160,11 @@ 644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */; }; 644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; }; 644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */; }; - 645041592A5C5749000221AD /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041542A5C5748000221AD /* libffi.a */; }; - 6450415A2A5C5749000221AD /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041552A5C5748000221AD /* libgmp.a */; }; - 6450415B2A5C5749000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041562A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a */; }; - 6450415C2A5C5749000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041572A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a */; }; - 6450415D2A5C5749000221AD /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 645041582A5C5748000221AD /* libgmpxx.a */; }; + 64519A182A615B010011988A /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64519A132A615B010011988A /* libgmpxx.a */; }; + 64519A192A615B020011988A /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64519A142A615B010011988A /* libgmp.a */; }; + 64519A1A2A615B020011988A /* libHSsimplex-chat-5.2.0.1-7dcwuQLxmes5EQ6Qc6lMkL.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64519A152A615B010011988A /* libHSsimplex-chat-5.2.0.1-7dcwuQLxmes5EQ6Qc6lMkL.a */; }; + 64519A1B2A615B020011988A /* libHSsimplex-chat-5.2.0.1-7dcwuQLxmes5EQ6Qc6lMkL-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64519A162A615B010011988A /* libHSsimplex-chat-5.2.0.1-7dcwuQLxmes5EQ6Qc6lMkL-ghc8.10.7.a */; }; + 64519A1C2A615B020011988A /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64519A172A615B010011988A /* libffi.a */; }; 6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; }; 646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; }; 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; }; @@ -437,11 +437,11 @@ 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIVoiceView.swift; sourceTree = ""; }; 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = ""; }; 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkedDeletedItemView.swift; sourceTree = ""; }; - 645041542A5C5748000221AD /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 645041552A5C5748000221AD /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 645041562A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a"; sourceTree = ""; }; - 645041572A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a"; sourceTree = ""; }; - 645041582A5C5748000221AD /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 64519A132A615B010011988A /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 64519A142A615B010011988A /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 64519A152A615B010011988A /* libHSsimplex-chat-5.2.0.1-7dcwuQLxmes5EQ6Qc6lMkL.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.1-7dcwuQLxmes5EQ6Qc6lMkL.a"; sourceTree = ""; }; + 64519A162A615B010011988A /* libHSsimplex-chat-5.2.0.1-7dcwuQLxmes5EQ6Qc6lMkL-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.0.1-7dcwuQLxmes5EQ6Qc6lMkL-ghc8.10.7.a"; sourceTree = ""; }; + 64519A172A615B010011988A /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = ""; }; 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; }; 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = ""; }; @@ -501,13 +501,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 64519A1A2A615B020011988A /* libHSsimplex-chat-5.2.0.1-7dcwuQLxmes5EQ6Qc6lMkL.a in Frameworks */, + 64519A182A615B010011988A /* libgmpxx.a in Frameworks */, + 64519A1B2A615B020011988A /* libHSsimplex-chat-5.2.0.1-7dcwuQLxmes5EQ6Qc6lMkL-ghc8.10.7.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 645041592A5C5749000221AD /* libffi.a in Frameworks */, - 6450415B2A5C5749000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a in Frameworks */, - 6450415A2A5C5749000221AD /* libgmp.a in Frameworks */, - 6450415C2A5C5749000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a in Frameworks */, + 64519A1C2A615B020011988A /* libffi.a in Frameworks */, + 64519A192A615B020011988A /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 6450415D2A5C5749000221AD /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -568,11 +568,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 645041542A5C5748000221AD /* libffi.a */, - 645041552A5C5748000221AD /* libgmp.a */, - 645041582A5C5748000221AD /* libgmpxx.a */, - 645041562A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7-ghc8.10.7.a */, - 645041572A5C5748000221AD /* libHSsimplex-chat-5.2.0.1-EEhQOsrCplxKU03XLccWe7.a */, + 64519A172A615B010011988A /* libffi.a */, + 64519A142A615B010011988A /* libgmp.a */, + 64519A132A615B010011988A /* libgmpxx.a */, + 64519A162A615B010011988A /* libHSsimplex-chat-5.2.0.1-7dcwuQLxmes5EQ6Qc6lMkL-ghc8.10.7.a */, + 64519A152A615B010011988A /* libHSsimplex-chat-5.2.0.1-7dcwuQLxmes5EQ6Qc6lMkL.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index e11e620b1..ea16af715 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -17,6 +17,8 @@ public enum ChatCommand { case createActiveUser(profile: Profile?, sameServers: Bool, pastTimestamp: Bool) case listUsers case apiSetActiveUser(userId: Int64, viewPwd: String?) + case setAllContactReceipts(enable: Bool) + case apiSetUserContactReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) case apiHideUser(userId: Int64, viewPwd: String) case apiUnhideUser(userId: Int64, viewPwd: String) case apiMuteUser(userId: Int64) @@ -122,6 +124,10 @@ public enum ChatCommand { return "/_create user \(encodeJSON(user))" case .listUsers: return "/users" case let .apiSetActiveUser(userId, viewPwd): return "/_user \(userId)\(maybePwd(viewPwd))" + case let .setAllContactReceipts(enable): return "/set receipts all \(onOff(enable))" + case let .apiSetUserContactReceipts(userId, userMsgReceiptSettings): + let umrs = userMsgReceiptSettings + return "/_set receipts \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))" case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId) \(encodeJSON(viewPwd))" case let .apiMuteUser(userId): return "/_mute user \(userId)" @@ -249,6 +255,8 @@ public enum ChatCommand { case .createActiveUser: return "createActiveUser" case .listUsers: return "listUsers" case .apiSetActiveUser: return "apiSetActiveUser" + case .setAllContactReceipts: return "setAllContactReceipts" + case .apiSetUserContactReceipts: return "apiSetUserContactReceipts" case .apiHideUser: return "apiHideUser" case .apiUnhideUser: return "apiUnhideUser" case .apiMuteUser: return "apiMuteUser" @@ -1134,14 +1142,26 @@ public struct KeepAliveOpts: Codable, Equatable { public struct ChatSettings: Codable { public var enableNtfs: Bool + public var sendRcpts: Bool? public var favorite: Bool - public init(enableNtfs: Bool, favorite: Bool) { + public init(enableNtfs: Bool, sendRcpts: Bool?, favorite: Bool) { self.enableNtfs = enableNtfs + self.sendRcpts = sendRcpts self.favorite = favorite } - public static let defaults: ChatSettings = ChatSettings(enableNtfs: true, favorite: false) + public static let defaults: ChatSettings = ChatSettings(enableNtfs: true, sendRcpts: nil, favorite: false) +} + +public struct UserMsgReceiptSettings: Codable { + public var enable: Bool + public var clearOverrides: Bool + + public init(enable: Bool, clearOverrides: Bool) { + self.enable = enable + self.clearOverrides = clearOverrides + } } public struct ConnectionStats: Decodable { diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index ab37f58d7..a78ea558e 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -16,6 +16,8 @@ public struct User: Decodable, NamedChat, Identifiable { public var profile: LocalProfile public var fullPreferences: FullPreferences public var activeUser: Bool + public var sendRcptsContacts: Bool + public var sendRcptsSmallGroups: Bool public var displayName: String { get { profile.displayName } } public var fullName: String { get { profile.fullName } } @@ -44,6 +46,8 @@ public struct User: Decodable, NamedChat, Identifiable { profile: LocalProfile.sampleData, fullPreferences: FullPreferences.sampleData, activeUser: true, + sendRcptsContacts: true, + sendRcptsSmallGroups: false, showNtfs: true ) } @@ -2269,6 +2273,11 @@ public struct CIMeta: Decodable { public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? { switch itemStatus { case .sndSent: return ("checkmark", metaColor) + case let .sndRcvd(msgRcptStatus): + switch msgRcptStatus { + case .ok: return ("checkmark", metaColor) // ("checkmark.circle", metaColor) + case .badMsgHash: return ("checkmark", .red) // ("checkmark.circle", .red) + } case .sndErrorAuth: return ("multiply", .red) case .sndError: return ("exclamationmark.triangle.fill", .yellow) case .rcvNew: return ("circlebadge.fill", Color.accentColor) @@ -2337,6 +2346,7 @@ private func recent(_ date: Date) -> Bool { public enum CIStatus: Decodable { case sndNew case sndSent + case sndRcvd(msgRcptStatus: MsgReceiptStatus) case sndErrorAuth case sndError(agentError: String) case rcvNew @@ -2344,16 +2354,22 @@ public enum CIStatus: Decodable { var id: String { switch self { - case .sndNew: return "sndNew" - case .sndSent: return "sndSent" - case .sndErrorAuth: return "sndErrorAuth" - case .sndError: return "sndError" - case .rcvNew: return "rcvNew" + case .sndNew: return "sndNew" + case .sndSent: return "sndSent" + case .sndRcvd: return "sndRcvd" + case .sndErrorAuth: return "sndErrorAuth" + case .sndError: return "sndError" + case .rcvNew: return "rcvNew" case .rcvRead: return "rcvRead" } } } +public enum MsgReceiptStatus: String, Decodable { + case ok + case badMsgHash +} + public enum CIDeleted: Decodable { case deleted(deletedTs: Date?) case moderated(deletedTs: Date?, byGroupMember: GroupMember)