diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 897d9fba8..d93ee982f 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -32,6 +32,7 @@ final class ChatModel: ObservableObject { @Published var terminalItems: [TerminalItem] = [] @Published var userAddress: String? @Published var userSMPServers: [String]? + @Published var chatItemTTL: ChatItemTTL = .none @Published var appOpenUrl: URL? @Published var deviceToken: DeviceToken? @Published var savedToken: DeviceToken? diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 5edd965e8..c9492a7ae 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -132,7 +132,7 @@ func apiCreateActiveUser(_ p: Profile) throws -> User { } func apiStartChat() throws -> Bool { - let r = chatSendCmdSync(.startChat(subscribe: true)) + let r = chatSendCmdSync(.startChat(subscribe: true, expire: true)) switch r { case .chatStarted: return true case .chatRunning: return false @@ -319,6 +319,16 @@ func setUserSMPServers(smpServers: [String]) async throws { try await sendCommandOkResp(.setUserSMPServers(smpServers: smpServers)) } +func getChatItemTTL() throws -> ChatItemTTL { + let r = chatSendCmdSync(.apiGetChatItemTTL) + if case let .chatItemTTL(chatItemTTL) = r { return ChatItemTTL(chatItemTTL) } + throw r +} + +func setChatItemTTL(_ chatItemTTL: ChatItemTTL) async throws { + try await sendCommandOkResp(.apiSetChatItemTTL(seconds: chatItemTTL.seconds)) +} + func getNetworkConfig() async throws -> NetCfg? { let r = await chatSendCmd(.apiGetNetworkConfig) if case let .networkConfig(cfg) = r { return cfg } @@ -757,6 +767,7 @@ func startChat() throws { if justStarted { m.userAddress = try apiGetUserAddress() m.userSMPServers = try getUserSMPServers() + m.chatItemTTL = try getChatItemTTL() let chats = try apiGetChats() m.chats = chats.map { Chat.init($0) } NtfManager.shared.setNtfBadgeCount(m.totalUnreadCount()) diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index d5b023608..896bdefd7 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -18,6 +18,7 @@ enum DatabaseAlert: Identifiable { case chatDeleted case deleteLegacyDatabase case deleteFilesAndMedia + case setChatItemTTL(ttl: ChatItemTTL) case error(title: LocalizedStringKey, error: String = "") var id: String { @@ -30,6 +31,7 @@ enum DatabaseAlert: Identifiable { case .chatDeleted: return "chatDeleted" case .deleteLegacyDatabase: return "deleteLegacyDatabase" case .deleteFilesAndMedia: return "deleteFilesAndMedia" + case .setChatItemTTL: return "setChatItemTTL" case let .error(title, _): return "error \(title)" } } @@ -50,6 +52,9 @@ struct DatabaseView: View { @State private var useKeychain = storeDBPassphraseGroupDefault.get() @State private var appFilesCountAndSize: (Int, Int)? + @State var chatItemTTL: ChatItemTTL + @State private var currentChatItemTTL: ChatItemTTL = .none + var body: some View { ZStack { chatDatabaseView() @@ -152,11 +157,21 @@ struct DatabaseView: View { } Section { + Picker("Delete messages after", selection: $chatItemTTL) { + ForEach([ChatItemTTL.none, ChatItemTTL.month, ChatItemTTL.week, ChatItemTTL.day]) { ttl in + Text(ttl.deleteAfterText).tag(ttl) + } + if case let .seconds(seconds) = chatItemTTL { + let ttl: ChatItemTTL = .seconds(seconds) + Text(ttl.deleteAfterText).tag(ttl) + } + } Button("Delete files & media", role: .destructive) { alert = .deleteFilesAndMedia } + .disabled(!stopped || appFilesCountAndSize?.0 == 0) } header: { - Text("Files") + Text("Data") } footer: { if let (fileCount, size) = appFilesCountAndSize { if fileCount == 0 { @@ -166,11 +181,18 @@ struct DatabaseView: View { } } } - .disabled(!stopped || appFilesCountAndSize?.0 == 0) } .onAppear { runChat = m.chatRunning ?? true appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) + currentChatItemTTL = chatItemTTL + } + .onChange(of: chatItemTTL) { ttl in + if ttl < currentChatItemTTL { + alert = .setChatItemTTL(ttl: ttl) + } else if ttl != currentChatItemTTL { + setCiTTL(ttl) + } } .alert(item: $alert) { item in databaseAlert(item) } .fileImporter( @@ -254,6 +276,17 @@ struct DatabaseView: View { }, secondaryButton: .cancel() ) + case let .setChatItemTTL(ttl): + return Alert( + title: Text("Enable automatic message deletion?"), + message: Text("This action cannot be undone - once you confirm, messages older than specified age will start to get deleted. It may take up to several minutes to delete old messages initially after changing this setting."), + primaryButton: .destructive(Text("Delete messages")) { + setCiTTL(ttl) + }, + secondaryButton: .cancel() { + chatItemTTL = currentChatItemTTL + } + ) case let .error(title, error): return Alert(title: Text(title), message: Text("\(error)")) } @@ -389,6 +422,29 @@ struct DatabaseView: View { } } + private func setCiTTL(_ ttl: ChatItemTTL) { + logger.debug("DatabaseView setChatItemTTL \(ttl.seconds ?? -1)") + progressIndicator = true + Task { + do { + try await setChatItemTTL(ttl) + await MainActor.run { + m.chatItemTTL = ttl + currentChatItemTTL = ttl + progressIndicator = false + appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) + } + } catch { + await MainActor.run { + alert = .error(title: "Error changing automatic message deletion", error: responseError(error)) + chatItemTTL = currentChatItemTTL + progressIndicator = false + appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) + } + } + } + } + private func deleteFiles() { deleteAppFiles() appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) @@ -397,6 +453,6 @@ struct DatabaseView: View { struct DatabaseView_Previews: PreviewProvider { static var previews: some View { - DatabaseView(showSettings: Binding.constant(false)) + DatabaseView(showSettings: Binding.constant(false), chatItemTTL: .none) } } diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 61ac9a901..45fdd6eb1 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -101,7 +101,7 @@ struct SettingsView: View { .disabled(chatModel.chatRunning != true) NavigationLink { - DatabaseView(showSettings: $showSettings) + DatabaseView(showSettings: $showSettings, chatItemTTL: chatModel.chatItemTTL) .navigationTitle("Your chat database") } label: { let color: Color = chatModel.chatDbEncrypted == false ? .orange : .secondary diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 1ff189d25..05de795e4 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -245,7 +245,7 @@ func apiGetActiveUser() -> User? { } func apiStartChat() throws -> Bool { - let r = sendSimpleXCmd(.startChat(subscribe: false)) + let r = sendSimpleXCmd(.startChat(subscribe: false, expire: false)) switch r { case .chatStarted: return true case .chatRunning: return false diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 0db3480d7..011fa6a75 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -90,11 +90,6 @@ 5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; }; 5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; }; 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; }; - 5CE1330A28E71B8F00FFFD8C /* libHSsimplex-chat-4.0.1-6epkrQoB3nkFyTCo6XPd6m-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE1330528E71B8F00FFFD8C /* libHSsimplex-chat-4.0.1-6epkrQoB3nkFyTCo6XPd6m-ghc8.10.7.a */; }; - 5CE1330B28E71B8F00FFFD8C /* libHSsimplex-chat-4.0.1-6epkrQoB3nkFyTCo6XPd6m.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE1330628E71B8F00FFFD8C /* libHSsimplex-chat-4.0.1-6epkrQoB3nkFyTCo6XPd6m.a */; }; - 5CE1330C28E71B8F00FFFD8C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE1330728E71B8F00FFFD8C /* libgmp.a */; }; - 5CE1330D28E71B8F00FFFD8C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE1330828E71B8F00FFFD8C /* libffi.a */; }; - 5CE1330E28E71B8F00FFFD8C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE1330928E71B8F00FFFD8C /* libgmpxx.a */; }; 5CE1331028E7391000FFFD8C /* ContactConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE1330F28E7391000FFFD8C /* ContactConnectionInfo.swift */; }; 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; 5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -127,6 +122,11 @@ 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; 6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; }; 6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; }; + 6448BBA628EAF728000D2AB9 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA128EAF728000D2AB9 /* libffi.a */; }; + 6448BBA728EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA228EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a */; }; + 6448BBA828EAF728000D2AB9 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA328EAF728000D2AB9 /* libgmpxx.a */; }; + 6448BBA928EAF728000D2AB9 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA428EAF728000D2AB9 /* libgmp.a */; }; + 6448BBAA28EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA528EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.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 */; }; @@ -300,11 +300,6 @@ 5CDCAD80281A7E2700503DA2 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; 5CE1330328E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = "de.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5CE1330428E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; - 5CE1330528E71B8F00FFFD8C /* libHSsimplex-chat-4.0.1-6epkrQoB3nkFyTCo6XPd6m-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.0.1-6epkrQoB3nkFyTCo6XPd6m-ghc8.10.7.a"; sourceTree = ""; }; - 5CE1330628E71B8F00FFFD8C /* libHSsimplex-chat-4.0.1-6epkrQoB3nkFyTCo6XPd6m.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.0.1-6epkrQoB3nkFyTCo6XPd6m.a"; sourceTree = ""; }; - 5CE1330728E71B8F00FFFD8C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CE1330828E71B8F00FFFD8C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5CE1330928E71B8F00FFFD8C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5CE1330F28E7391000FFFD8C /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = ""; }; 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleXChat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SimpleXChat.h; sourceTree = ""; }; @@ -323,6 +318,11 @@ 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = ""; }; 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = ""; }; 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = ""; }; + 6448BBA128EAF728000D2AB9 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 6448BBA228EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a"; sourceTree = ""; }; + 6448BBA328EAF728000D2AB9 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 6448BBA428EAF728000D2AB9 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 6448BBA528EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.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 = ""; }; @@ -368,13 +368,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CE1330A28E71B8F00FFFD8C /* libHSsimplex-chat-4.0.1-6epkrQoB3nkFyTCo6XPd6m-ghc8.10.7.a in Frameworks */, - 5CE1330D28E71B8F00FFFD8C /* libffi.a in Frameworks */, - 5CE1330C28E71B8F00FFFD8C /* libgmp.a in Frameworks */, + 6448BBAA28EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, + 6448BBA828EAF728000D2AB9 /* libgmpxx.a in Frameworks */, + 6448BBA728EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a in Frameworks */, + 6448BBA628EAF728000D2AB9 /* libffi.a in Frameworks */, + 6448BBA928EAF728000D2AB9 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5CE1330B28E71B8F00FFFD8C /* libHSsimplex-chat-4.0.1-6epkrQoB3nkFyTCo6XPd6m.a in Frameworks */, - 5CE1330E28E71B8F00FFFD8C /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -429,11 +429,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CE1330828E71B8F00FFFD8C /* libffi.a */, - 5CE1330728E71B8F00FFFD8C /* libgmp.a */, - 5CE1330928E71B8F00FFFD8C /* libgmpxx.a */, - 5CE1330528E71B8F00FFFD8C /* libHSsimplex-chat-4.0.1-6epkrQoB3nkFyTCo6XPd6m-ghc8.10.7.a */, - 5CE1330628E71B8F00FFFD8C /* libHSsimplex-chat-4.0.1-6epkrQoB3nkFyTCo6XPd6m.a */, + 6448BBA128EAF728000D2AB9 /* libffi.a */, + 6448BBA428EAF728000D2AB9 /* libgmp.a */, + 6448BBA328EAF728000D2AB9 /* libgmpxx.a */, + 6448BBA228EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a */, + 6448BBA528EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index d794f785d..167227aab 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -15,14 +15,14 @@ let jsonEncoder = getJSONEncoder() public enum ChatCommand { case showActiveUser case createActiveUser(profile: Profile) - case startChat(subscribe: Bool) + case startChat(subscribe: Bool, expire: Bool) case apiStopChat case apiActivateChat case apiSuspendChat(timeoutMicroseconds: Int) case setFilesFolder(filesFolder: String) case setIncognito(incognito: Bool) case apiExportArchive(config: ArchiveConfig) - case apiImportArchive(config: ArchiveConfig) + case apiImportArchive(config: ArchiveConfig) case apiDeleteStorage case apiStorageEncryption(config: DBEncryptionConfig) case apiGetChats @@ -45,6 +45,8 @@ public enum ChatCommand { case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile) case getUserSMPServers case setUserSMPServers(smpServers: [String]) + case apiSetChatItemTTL(seconds: Int64?) + case apiGetChatItemTTL case apiSetNetworkConfig(networkConfig: NetCfg) case apiGetNetworkConfig case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) @@ -81,12 +83,12 @@ public enum ChatCommand { switch self { case .showActiveUser: return "/u" case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)" - case let .startChat(subscribe): return "/_start subscribe=\(subscribe ? "on" : "off") expire=off" + case let .startChat(subscribe, expire): return "/_start subscribe=\(onOff(subscribe)) expire=\(onOff(expire))" case .apiStopChat: return "/_stop" case .apiActivateChat: return "/_app activate" case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)" - case let .setIncognito(incognito): return "/incognito \(incognito ? "on" : "off")" + case let .setIncognito(incognito): return "/incognito \(onOff(incognito))" case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" case .apiDeleteStorage: return "/_db delete" @@ -113,6 +115,8 @@ public enum ChatCommand { case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))" case .getUserSMPServers: return "/smp_servers" case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))" + case let .apiSetChatItemTTL(seconds): return "/_ttl \(chatItemTTLStr(seconds: seconds))" + case .apiGetChatItemTTL: return "/ttl" case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" case .apiGetNetworkConfig: return "/network" case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))" @@ -180,6 +184,8 @@ public enum ChatCommand { case .apiUpdateGroupProfile: return "apiUpdateGroupProfile" case .getUserSMPServers: return "getUserSMPServers" case .setUserSMPServers: return "setUserSMPServers" + case .apiSetChatItemTTL: return "apiSetChatItemTTL" + case .apiGetChatItemTTL: return "apiGetChatItemTTL" case .apiSetNetworkConfig: return "apiSetNetworkConfig" case .apiGetNetworkConfig: return "apiGetNetworkConfig" case .apiSetChatSettings: return "apiSetChatSettings" @@ -221,6 +227,14 @@ public enum ChatCommand { smpServers.isEmpty ? "default" : smpServers.joined(separator: ",") } + func chatItemTTLStr(seconds: Int64?) -> String { + if let seconds = seconds { + return String(seconds) + } else { + return "none" + } + } + public var obfuscated: ChatCommand { switch self { case let .apiStorageEncryption(cfg): @@ -232,6 +246,10 @@ public enum ChatCommand { private func obfuscate(_ s: String) -> String { s == "" ? "" : "***" } + + private func onOff(_ b: Bool) -> String { + b ? "on" : "off" + } } struct APIResponse: Decodable { @@ -248,6 +266,7 @@ public enum ChatResponse: Decodable, Error { case apiChats(chats: [ChatData]) case apiChat(chat: ChatData) case userSMPServers(smpServers: [String]) + case chatItemTTL(chatItemTTL: Int64?) case networkConfig(networkConfig: NetCfg) case contactInfo(contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?) case groupMemberInfo(groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?) @@ -342,6 +361,7 @@ public enum ChatResponse: Decodable, Error { case .apiChats: return "apiChats" case .apiChat: return "apiChat" case .userSMPServers: return "userSMPServers" + case .chatItemTTL: return "chatItemTTL" case .networkConfig: return "networkConfig" case .contactInfo: return "contactInfo" case .groupMemberInfo: return "groupMemberInfo" @@ -436,6 +456,7 @@ public enum ChatResponse: Decodable, Error { case let .apiChats(chats): return String(describing: chats) case let .apiChat(chat): return String(describing: chat) case let .userSMPServers(smpServers): return String(describing: smpServers) + case let .chatItemTTL(chatItemTTL): return String(describing: chatItemTTL) case let .networkConfig(networkConfig): return String(describing: networkConfig) case let .contactInfo(contact, connectionStats, customUserProfile): return "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))" case let .groupMemberInfo(groupInfo, member, connectionStats_): return "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_)))" diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 5795225f6..076647d10 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1464,3 +1464,51 @@ public enum SndGroupEvent: Decodable { } } } + +public enum ChatItemTTL: Hashable, Identifiable, Comparable { + case day + case week + case month + case seconds(_ seconds: Int64) + case none + + public var id: Self { self } + + public init(_ seconds: Int64?) { + switch seconds { + case 86400: self = .day + case 7 * 86400: self = .week + case 30 * 86400: self = .month + case let .some(n): self = .seconds(n) + case .none: self = .none + } + } + + public var deleteAfterText: LocalizedStringKey { + switch self { + case .day: return "1 day" + case .week: return "1 week" + case .month: return "1 month" + case let .seconds(seconds): return "\(seconds) second(s)" + case .none: return "no" + } + } + + public var seconds: Int64? { + switch self { + case .day: return 86400 + case .week: return 7 * 86400 + case .month: return 30 * 86400 + case let .seconds(seconds): return seconds + case .none: return nil + } + } + + private var comparisonValue: Int64 { + self.seconds ?? Int64.max + } + + public static func < (lhs: Self, rhs: Self) -> Bool { + return lhs.comparisonValue < rhs.comparisonValue + } +}