diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index a6d574e38d..764db8335e 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -561,6 +561,18 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (Gro throw r } +func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) { + let r = await chatSendCmd(.apiContactQueueInfo(contactId: contactId)) + if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } + throw r +} + +func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) { + let r = await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId)) + if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } + throw r +} + func apiSwitchContact(contactId: Int64) throws -> ConnectionStats { let r = chatSendCmdSync(.apiSwitchContact(contactId: contactId)) if case let .contactSwitchStarted(_, _, connectionStats) = r { return connectionStats } @@ -1265,7 +1277,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] { let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil } return ChatModel.shared.chats .compactMap{ $0.chatInfo.contact } - .filter{ c in c.ready && c.active && !memberContactIds.contains(c.apiId) } + .filter{ c in c.sendMsgEnabled && !c.nextSendGrpInv && !memberContactIds.contains(c.apiId) } .sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } } @@ -1835,7 +1847,7 @@ func processReceivedMsg(_ res: ChatResponse) async { } case let .sndFileCompleteXFTP(user, aChatItem, _): await chatItemSimpleUpdate(user, aChatItem) - case let .sndFileError(user, aChatItem, _): + case let .sndFileError(user, aChatItem, _, _): if let aChatItem = aChatItem { await chatItemSimpleUpdate(user, aChatItem) Task { cleanupFile(aChatItem) } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 2e5a4f2af6..f5abfe9c58 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -110,6 +110,7 @@ struct ChatInfoView: View { case switchAddressAlert case abortSwitchAddressAlert case syncConnectionForceAlert + case queueInfo(info: String) case error(title: LocalizedStringKey, error: LocalizedStringKey = "") var id: String { @@ -119,6 +120,7 @@ struct ChatInfoView: View { case .switchAddressAlert: return "switchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" + case let .queueInfo(info): return "queueInfo \(info)" case let .error(title, _): return "error \(title)" } } @@ -224,6 +226,18 @@ struct ChatInfoView: View { Section(header: Text("For console")) { infoRow("Local name", chat.chatInfo.localDisplayName) infoRow("Database ID", "\(chat.chatInfo.apiId)") + Button ("Debug delivery") { + Task { + do { + let info = queueInfoText(try await apiContactQueueInfo(chat.chatInfo.apiId)) + await MainActor.run { alert = .queueInfo(info: info) } + } catch let e { + logger.error("apiContactQueueInfo error: \(responseError(e))") + let a = getErrorAlert(e, "Error") + await MainActor.run { alert = .error(title: a.title, error: a.message) } + } + } + } } } } @@ -243,6 +257,7 @@ struct ChatInfoView: View { case .switchAddressAlert: return switchAddressAlert(switchContactAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress) case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) }) + case let .queueInfo(info): return queueInfoAlert(info) case let .error(title, error): return mkAlert(title: title, message: error) } } @@ -577,6 +592,22 @@ func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Al ) } +func queueInfoText(_ info: (RcvMsgInfo?, QueueInfo)) -> String { + let (rcvMsgInfo, qInfo) = info + var msgInfo: String + if let rcvMsgInfo { msgInfo = encodeJSON(rcvMsgInfo) } else { msgInfo = "none" } + return String.localizedStringWithFormat(NSLocalizedString("server queue info: %@\n\nlast received msg: %@", comment: "queue info"), encodeJSON(qInfo), msgInfo) +} + +func queueInfoAlert(_ info: String) -> Alert { + Alert( + title: Text("Message queue info"), + message: Text(info), + primaryButton: .default(Text("Ok")), + secondaryButton: .default(Text("Copy")) { UIPasteboard.general.string = info } + ) +} + struct ChatInfoView_Previews: PreviewProvider { static var previews: some View { ChatInfoView( diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index 0af0469e42..53d840306e 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -54,7 +54,7 @@ struct CIFileView: View { switch (file.fileStatus) { case .sndStored: return file.fileProtocol == .local case .sndTransfer: return false - case .sndComplete: return false + case .sndComplete: return true case .sndCancelled: return false case .sndError: return false case .rcvInvitation: return true @@ -113,6 +113,11 @@ struct CIFileView: View { if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) { saveCryptoFile(fileSource) } + case .sndComplete: + logger.debug("CIFileView fileAction - in .sndComplete") + if let fileSource = getLoadedFileSource(file) { + saveCryptoFile(fileSource) + } default: break } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift index 0299a5e6f8..40ed8bc76c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift @@ -24,7 +24,7 @@ struct CIInvalidJSONView: View { .cornerRadius(18) .textSelection(.disabled) .onTapGesture { showJSON = true } - .sheet(isPresented: $showJSON) { + .appSheet(isPresented: $showJSON) { invalidJSONView(json) } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 4055ca2b28..27eb3bd653 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -131,7 +131,7 @@ struct ChatView: View { } label: { ChatInfoToolbar(chat: chat) } - .sheet(isPresented: $showChatInfoSheet, onDismiss: { + .appSheet(isPresented: $showChatInfoSheet, onDismiss: { connectionStats = nil customUserProfile = nil connectionCode = nil diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index a24608b7e7..a851e3fc1d 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -35,6 +35,7 @@ struct GroupMemberInfoView: View { case abortSwitchAddressAlert case syncConnectionForceAlert case planAndConnectAlert(alert: PlanAndConnectAlert) + case queueInfo(info: String) case error(title: LocalizedStringKey, error: LocalizedStringKey) var id: String { @@ -49,6 +50,7 @@ struct GroupMemberInfoView: View { case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)" + case let .queueInfo(info): return "queueInfo \(info)" case let .error(title, _): return "error \(title)" } } @@ -178,6 +180,18 @@ struct GroupMemberInfoView: View { Section("For console") { infoRow("Local name", member.localDisplayName) infoRow("Database ID", "\(member.groupMemberId)") + Button ("Debug delivery") { + Task { + do { + let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId)) + await MainActor.run { alert = .queueInfo(info: info) } + } catch let e { + logger.error("apiContactQueueInfo error: \(responseError(e))") + let a = getErrorAlert(e, "Error") + await MainActor.run { alert = .error(title: a.title, error: a.message) } + } + } + } } } } @@ -223,6 +237,7 @@ struct GroupMemberInfoView: View { case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress) case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) }) case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true) + case let .queueInfo(info): return queueInfoAlert(info) case let .error(title, error): return Alert(title: Text(title), message: Text(error)) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index efe254323e..73c3c73556 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -349,10 +349,12 @@ struct ChatListNavLink: View { .tint(.accentColor) } .frame(height: rowHeights[dynamicTypeSize]) - .sheet(isPresented: $showContactConnectionInfo) { - if case let .contactConnection(contactConnection) = chat.chatInfo { - ContactConnectionInfo(contactConnection: contactConnection) - .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + .appSheet(isPresented: $showContactConnectionInfo) { + Group { + if case let .contactConnection(contactConnection) = chat.chatInfo { + ContactConnectionInfo(contactConnection: contactConnection) + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + } } } .onTapGesture { @@ -467,7 +469,7 @@ struct ChatListNavLink: View { .padding(4) .frame(height: rowHeights[dynamicTypeSize]) .onTapGesture { showInvalidJSON = true } - .sheet(isPresented: $showInvalidJSON) { + .appSheet(isPresented: $showInvalidJSON) { invalidJSONView(json) .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) } diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 209c440b16..86e14b74f7 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -406,7 +406,28 @@ private let versionDescriptions: [VersionDescription] = [ description: "More reliable network connection." ) ] - ) + ), + VersionDescription( + version: "v5.8", + post: URL(string: "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html"), + features: [ + FeatureDescription( + icon: "arrow.forward", + title: "Private message routing 🚀", + description: "Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." + ), + FeatureDescription( + icon: "network.badge.shield.half.filled", + title: "Safely receive files", + description: "Confirm files from unknown servers." + ), + FeatureDescription( + icon: "battery.50", + title: "Improved message delivery", + description: "With reduced battery usage." + ) + ] + ), ] private let lastVersion = versionDescriptions.last!.version diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index 8c1a3bf4e1..5da7c8e877 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -123,7 +123,7 @@ struct UserProfilesView: View { deleteModeButton("Profile and server connections", true) deleteModeButton("Local profile data only", false) } - .sheet(item: $selectedUser) { user in + .appSheet(item: $selectedUser) { user in HiddenProfileView(user: user, profileHidden: $profileHidden) } .onChange(of: profileHidden) { _ in @@ -131,7 +131,7 @@ struct UserProfilesView: View { withAnimation { profileHidden = false } } } - .sheet(item: $profileAction) { action in + .appSheet(item: $profileAction) { action in profileActionView(action) } .alert(item: $alert) { alert in diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 8657b17c03..28abc7221a 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -1296,6 +1296,10 @@ Потвърди актуализаациите на базата данни No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Потвърди мрежовите настройки @@ -4412,6 +4416,10 @@ Error: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes Лични бележки @@ -4510,6 +4518,11 @@ Error: %@ Защити екрана на приложението No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Защитете чат профилите с парола! @@ -4850,6 +4863,10 @@ Error: %@ SMP сървъри No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups По-безопасни групи diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 8d52e045b1..41e770b180 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -1250,6 +1250,10 @@ Potvrdit aktualizaci databáze No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings No comment provided by engineer. @@ -4234,6 +4238,10 @@ Error: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes name of notes to self @@ -4327,6 +4335,11 @@ Error: %@ Ochrana obrazovky aplikace No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Chraňte své chat profily heslem! @@ -4656,6 +4669,10 @@ Error: %@ SMP servery No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index b21e35382a..5c3c74ba77 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -1296,6 +1296,10 @@ Datenbank-Aktualisierungen bestätigen No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Bestätigen Sie die Netzwerkeinstellungen @@ -4412,6 +4416,10 @@ Fehler: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes Private Notizen @@ -4510,6 +4518,11 @@ Fehler: %@ App-Bildschirm schützen No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Ihre Chat-Profile mit einem Passwort schützen! @@ -4850,6 +4863,10 @@ Fehler: %@ SMP-Server No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups Sicherere Gruppen diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 396d419a09..777bd312de 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -1299,6 +1299,11 @@ Confirm database upgrades No comment provided by engineer. + + Confirm files from unknown servers. + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Confirm network settings @@ -4428,6 +4433,11 @@ Error: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + Private message routing 🚀 + No comment provided by engineer. + Private notes Private notes @@ -4528,6 +4538,13 @@ Error: %@ Protect app screen No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Protect your chat profiles with a password! @@ -4868,6 +4885,11 @@ Error: %@ SMP servers No comment provided by engineer. + + Safely receive files + Safely receive files + No comment provided by engineer. + Safer groups Safer groups diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index da42957714..2479e5f07a 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -1296,6 +1296,10 @@ Confirmar actualizaciones de la bases de datos No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Confirmar configuración de red @@ -4412,6 +4416,10 @@ Error: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes Notas privadas @@ -4510,6 +4518,11 @@ Error: %@ Proteger la pantalla de la aplicación No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! ¡Protege tus perfiles con contraseña! @@ -4850,6 +4863,10 @@ Error: %@ Servidores SMP No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups Grupos más seguros diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index fbdb5dfe19..b15c2b1779 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -1243,6 +1243,10 @@ Vahvista tietokannan päivitykset No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings No comment provided by engineer. @@ -4222,6 +4226,10 @@ Error: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes name of notes to self @@ -4315,6 +4323,11 @@ Error: %@ Suojaa sovellusnäyttö No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Suojaa keskusteluprofiilisi salasanalla! @@ -4644,6 +4657,10 @@ Error: %@ SMP-palvelimet No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 81bd0f3741..28ddaccb53 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -1296,6 +1296,10 @@ Confirmer la mise à niveau de la base de données No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Confirmer les paramètres réseau @@ -4412,6 +4416,10 @@ Erreur : %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes Notes privées @@ -4510,6 +4518,11 @@ Erreur : %@ Protéger l'écran de l'app No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Protégez vos profils de chat par un mot de passe ! @@ -4850,6 +4863,10 @@ Erreur : %@ Serveurs SMP No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups Groupes plus sûrs diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index bd8b71fe65..439bd0a842 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -1296,6 +1296,10 @@ Adatbázis frissítés megerősítése No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Hálózati beállítások megerősítése @@ -4412,6 +4416,10 @@ Hiba: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes Privát jegyzetek @@ -4510,6 +4518,11 @@ Hiba: %@ Alkalmazás képernyőjének védelme No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Csevegési profiljok védelme jelszóval! @@ -4850,6 +4863,10 @@ Hiba: %@ Üzenetküldő (SMP) kiszolgálók No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups Biztonságosabb csoportok diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index dda9effd4c..f1693f5350 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -1296,6 +1296,10 @@ Conferma aggiornamenti database No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Conferma le impostazioni di rete @@ -4412,6 +4416,10 @@ Errore: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes Note private @@ -4510,6 +4518,11 @@ Errore: %@ Proteggi la schermata dell'app No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Proteggi i tuoi profili di chat con una password! @@ -4850,6 +4863,10 @@ Errore: %@ Server SMP No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups Gruppi più sicuri diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 35afbf2827..de92dd9e14 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -1267,6 +1267,10 @@ データベースのアップグレードを確認 No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings No comment provided by engineer. @@ -4248,6 +4252,10 @@ Error: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes name of notes to self @@ -4341,6 +4349,11 @@ Error: %@ アプリ画面を守る No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! チャットのプロフィールをパスワードで保護します! @@ -4669,6 +4682,10 @@ Error: %@ SMPサーバ No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 7e3775188b..24c53f1cdb 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -1296,6 +1296,10 @@ Bevestig database upgrades No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Bevestig netwerk instellingen @@ -4412,6 +4416,10 @@ Fout: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes Privé notities @@ -4510,6 +4518,11 @@ Fout: %@ App scherm verbergen No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Bescherm je chat profielen met een wachtwoord! @@ -4850,6 +4863,10 @@ Fout: %@ SMP servers No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups Veiligere groepen diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 72add6a8ad..6f5536d7d2 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -1296,6 +1296,10 @@ Potwierdź aktualizacje bazy danych No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Potwierdź ustawienia sieciowe @@ -4412,6 +4416,10 @@ Błąd: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes Prywatne notatki @@ -4510,6 +4518,11 @@ Błąd: %@ Chroń ekran aplikacji No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Chroń swoje profile czatu hasłem! @@ -4850,6 +4863,10 @@ Błąd: %@ Serwery SMP No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups Bezpieczniejsze grupy diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index c0a9df6b88..2569f49546 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -1296,6 +1296,10 @@ Подтвердить обновление базы данных No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Подтвердите настройки сети @@ -4412,6 +4416,10 @@ Error: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes Личные заметки @@ -4510,6 +4518,11 @@ Error: %@ Защитить экран приложения No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Защитите Ваши профили чата паролем! @@ -4850,6 +4863,10 @@ Error: %@ SMP серверы No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups Более безопасные группы diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index c2a2d47bdc..69d3820c7f 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -1235,6 +1235,10 @@ ยืนยันการอัพเกรดฐานข้อมูล No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings No comment provided by engineer. @@ -4203,6 +4207,10 @@ Error: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes name of notes to self @@ -4296,6 +4304,11 @@ Error: %@ ปกป้องหน้าจอแอป No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! ปกป้องโปรไฟล์การแชทของคุณด้วยรหัสผ่าน! @@ -4623,6 +4636,10 @@ Error: %@ เซิร์ฟเวอร์ SMP No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index f0954e750f..0eef87534d 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -1296,6 +1296,10 @@ Veritabanı geliştirmelerini onayla No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Ağ ayarlarını onaylayın @@ -4412,6 +4416,10 @@ Hata: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes Gizli notlar @@ -4510,6 +4518,11 @@ Hata: %@ Uygulama ekranını koru No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Bir parolayla birlikte sohbet profillerini koru! @@ -4850,6 +4863,10 @@ Hata: %@ SMP sunucuları No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups Daha güvenli gruplar diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 80686b6f5a..c5150d702d 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -1294,6 +1294,10 @@ Підтвердити оновлення бази даних No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Підтвердьте налаштування мережі @@ -4395,6 +4399,10 @@ Error: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes Приватні нотатки @@ -4491,6 +4499,11 @@ Error: %@ Захистіть екран програми No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Захистіть свої профілі чату паролем! @@ -4830,6 +4843,10 @@ Error: %@ Сервери SMP No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups Безпечніші групи diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index edb01ac8ab..5303064f1c 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -1282,6 +1282,10 @@ 确认数据库升级 No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings 确认网络设置 @@ -4366,6 +4370,10 @@ Error: %@ Private message routing No comment provided by engineer. + + Private message routing 🚀 + No comment provided by engineer. + Private notes 私密笔记 @@ -4462,6 +4470,11 @@ Error: %@ 保护应用程序屏幕 No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! 使用密码保护您的聊天资料! @@ -4800,6 +4813,10 @@ Error: %@ SMP 服务器 No comment provided by engineer. + + Safely receive files + No comment provided by engineer. + Safer groups 更安全的群组 diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 22542016dd..bcc61023cb 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -24,6 +24,11 @@ 5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; }; 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; }; 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; }; + 5C0EA1272C08ADAF00AD2E5E /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1222C08ADAF00AD2E5E /* libgmp.a */; }; + 5C0EA1282C08ADAF00AD2E5E /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1232C08ADAF00AD2E5E /* libffi.a */; }; + 5C0EA1292C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1242C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5-ghc9.6.3.a */; }; + 5C0EA12A2C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1252C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5.a */; }; + 5C0EA12B2C08ADAF00AD2E5E /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1262C08ADAF00AD2E5E /* libgmpxx.a */; }; 5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */; }; 5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */; }; 5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; }; @@ -139,11 +144,6 @@ 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; 5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; }; 5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; }; - 5CEE87942C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE878F2C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a */; }; - 5CEE87952C024F4F00583B8A /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87902C024F4F00583B8A /* libgmp.a */; }; - 5CEE87962C024F4F00583B8A /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87912C024F4F00583B8A /* libgmpxx.a */; }; - 5CEE87972C024F4F00583B8A /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87922C024F4F00583B8A /* libffi.a */; }; - 5CEE87982C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87932C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a */; }; 5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; }; 5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; }; 5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; }; @@ -278,6 +278,11 @@ 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; 5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = ""; }; 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = ""; }; + 5C0EA1222C08ADAF00AD2E5E /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C0EA1232C08ADAF00AD2E5E /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C0EA1242C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5-ghc9.6.3.a"; sourceTree = ""; }; + 5C0EA1252C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5.a"; sourceTree = ""; }; + 5C0EA1262C08ADAF00AD2E5E /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = ""; }; 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMediaView.swift; sourceTree = ""; }; 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = ""; }; @@ -440,11 +445,6 @@ 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = ""; }; 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = ""; }; - 5CEE878F2C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a"; sourceTree = ""; }; - 5CEE87902C024F4F00583B8A /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CEE87912C024F4F00583B8A /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5CEE87922C024F4F00583B8A /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5CEE87932C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a"; sourceTree = ""; }; 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = ""; }; 5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = ""; }; 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = ""; }; @@ -539,13 +539,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CEE87942C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a in Frameworks */, - 5CEE87972C024F4F00583B8A /* libffi.a in Frameworks */, - 5CEE87962C024F4F00583B8A /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5CEE87952C024F4F00583B8A /* libgmp.a in Frameworks */, + 5C0EA1272C08ADAF00AD2E5E /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5CEE87982C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a in Frameworks */, + 5C0EA12A2C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5.a in Frameworks */, + 5C0EA1292C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5-ghc9.6.3.a in Frameworks */, + 5C0EA1282C08ADAF00AD2E5E /* libffi.a in Frameworks */, + 5C0EA12B2C08ADAF00AD2E5E /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -611,11 +611,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CEE87922C024F4F00583B8A /* libffi.a */, - 5CEE87902C024F4F00583B8A /* libgmp.a */, - 5CEE87912C024F4F00583B8A /* libgmpxx.a */, - 5CEE878F2C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a */, - 5CEE87932C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a */, + 5C0EA1232C08ADAF00AD2E5E /* libffi.a */, + 5C0EA1222C08ADAF00AD2E5E /* libgmp.a */, + 5C0EA1262C08ADAF00AD2E5E /* libgmpxx.a */, + 5C0EA1242C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5-ghc9.6.3.a */, + 5C0EA1252C08ADAF00AD2E5E /* libHSsimplex-chat-5.8.0.4-2QdMx3VWkTS77pHWzbDqa5.a */, ); path = Libraries; sourceTree = ""; @@ -1562,7 +1562,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 220; + CURRENT_PROJECT_VERSION = 221; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1611,7 +1611,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 220; + CURRENT_PROJECT_VERSION = 221; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1697,7 +1697,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 220; + CURRENT_PROJECT_VERSION = 221; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -1734,7 +1734,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 220; + CURRENT_PROJECT_VERSION = 221; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -1771,7 +1771,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 220; + CURRENT_PROJECT_VERSION = 221; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1822,7 +1822,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 220; + CURRENT_PROJECT_VERSION = 221; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 12683bc3a4..1574d43ac0 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -82,6 +82,8 @@ public enum ChatCommand { case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings) case apiContactInfo(contactId: Int64) case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64) + case apiContactQueueInfo(contactId: Int64) + case apiGroupMemberQueueInfo(groupId: Int64, groupMemberId: Int64) case apiSwitchContact(contactId: Int64) case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64) case apiAbortSwitchContact(contactId: Int64) @@ -228,6 +230,8 @@ public enum ChatCommand { case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))" case let .apiContactInfo(contactId): return "/_info @\(contactId)" case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)" + case let .apiContactQueueInfo(contactId): return "/_queue info @\(contactId)" + case let .apiGroupMemberQueueInfo(groupId, groupMemberId): return "/_queue info #\(groupId) \(groupMemberId)" case let .apiSwitchContact(contactId): return "/_switch @\(contactId)" case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)" case let .apiAbortSwitchContact(contactId): return "/_abort switch @\(contactId)" @@ -375,6 +379,8 @@ public enum ChatCommand { case .apiSetMemberSettings: return "apiSetMemberSettings" case .apiContactInfo: return "apiContactInfo" case .apiGroupMemberInfo: return "apiGroupMemberInfo" + case .apiContactQueueInfo: return "apiContactQueueInfo" + case .apiGroupMemberQueueInfo: return "apiGroupMemberQueueInfo" case .apiSwitchContact: return "apiSwitchContact" case .apiSwitchGroupMember: return "apiSwitchGroupMember" case .apiAbortSwitchContact: return "apiAbortSwitchContact" @@ -516,6 +522,7 @@ public enum ChatResponse: Decodable, Error { case networkConfig(networkConfig: NetCfg) case contactInfo(user: UserRef, contact: Contact, connectionStats_: ConnectionStats?, customUserProfile: Profile?) case groupMemberInfo(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?) + case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: QueueInfo) case contactSwitchStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) case groupMemberSwitchStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) case contactSwitchAborted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) @@ -628,7 +635,7 @@ public enum ChatResponse: Decodable, Error { case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) case sndStandaloneFileComplete(user: UserRef, fileTransferMeta: FileTransferMeta, rcvURIs: [String]) case sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta) - case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta) + case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) // call events case callInvitation(callInvitation: RcvCallInvitation) case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool) @@ -678,6 +685,7 @@ public enum ChatResponse: Decodable, Error { case .networkConfig: return "networkConfig" case .contactInfo: return "contactInfo" case .groupMemberInfo: return "groupMemberInfo" + case .queueInfo: return "queueInfo" case .contactSwitchStarted: return "contactSwitchStarted" case .groupMemberSwitchStarted: return "groupMemberSwitchStarted" case .contactSwitchAborted: return "contactSwitchAborted" @@ -836,6 +844,9 @@ public enum ChatResponse: Decodable, Error { case let .networkConfig(networkConfig): return String(describing: networkConfig) case let .contactInfo(u, contact, connectionStats_, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats_: \(String(describing: connectionStats_))\ncustomUserProfile: \(String(describing: customUserProfile))") case let .groupMemberInfo(u, groupInfo, member, connectionStats_): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))") + case let .queueInfo(u, rcvMsgInfo, queueInfo): + let msgInfo = if let info = rcvMsgInfo { encodeJSON(rcvMsgInfo) } else { "none" } + return withUser(u, "rcvMsgInfo: \(msgInfo)\nqueueInfo: \(encodeJSON(queueInfo))") case let .contactSwitchStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") case let .groupMemberSwitchStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))") case let .contactSwitchAborted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") @@ -945,7 +956,7 @@ public enum ChatResponse: Decodable, Error { case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .sndStandaloneFileComplete(u, _, rcvURIs): return withUser(u, String(rcvURIs.count)) case let .sndFileCancelledXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) - case let .sndFileError(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileError(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") case let .callInvitation(inv): return String(describing: inv) case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))") case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))") @@ -2170,3 +2181,42 @@ public enum UserNetworkType: String, Codable { } } } + +public struct RcvMsgInfo: Codable { + var msgId: Int64 + var msgDeliveryId: Int64 + var msgDeliveryStatus: String + var agentMsgId: Int64 + var agentMsgMeta: String +} + +public struct QueueInfo: Codable { + var qiSnd: Bool + var qiNtf: Bool + var qiSub: QSub? + var qiSize: Int + var qiMsg: MsgInfo? +} + +public struct QSub: Codable { + var qSubThread: QSubThread + var qDelivered: String? +} + +public enum QSubThread: String, Codable { + case noSub + case subPending + case subThread + case prohibitSub +} + +public struct MsgInfo: Codable { + var msgId: String + var msgTs: Date + var msgType: MsgType +} + +public enum MsgType: String, Codable { + case message + case quota +} diff --git a/apps/multiplatform/android/build.gradle.kts b/apps/multiplatform/android/build.gradle.kts index 4e279d3ccb..5c2c786a21 100644 --- a/apps/multiplatform/android/build.gradle.kts +++ b/apps/multiplatform/android/build.gradle.kts @@ -88,6 +88,7 @@ android { "cs", "de", "es", + "fa", "fi", "fr", "hu", diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index b0557e2f67..1f7f55fb53 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -931,6 +931,20 @@ object ChatController { return null } + suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair? { + val r = sendCmd(rh, CC.APIContactQueueInfo(contactId)) + if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo) + apiErrorAlert("apiContactQueueInfo", generalGetString(MR.strings.error), r) + return null + } + + suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair? { + val r = sendCmd(rh, CC.APIGroupMemberQueueInfo(groupId, groupMemberId)) + if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo) + apiErrorAlert("apiGroupMemberQueueInfo", generalGetString(MR.strings.error), r) + return null + } + suspend fun apiSwitchContact(rh: Long?, contactId: Long): ConnectionStats? { val r = sendCmd(rh, CC.APISwitchContact(contactId)) if (r is CR.ContactSwitchStarted) return r.connectionStats @@ -2507,6 +2521,8 @@ sealed class CC { class ApiSetMemberSettings(val groupId: Long, val groupMemberId: Long, val memberSettings: GroupMemberSettings): CC() class APIContactInfo(val contactId: Long): CC() class APIGroupMemberInfo(val groupId: Long, val groupMemberId: Long): CC() + class APIContactQueueInfo(val contactId: Long): CC() + class APIGroupMemberQueueInfo(val groupId: Long, val groupMemberId: Long): CC() class APISwitchContact(val contactId: Long): CC() class APISwitchGroupMember(val groupId: Long, val groupMemberId: Long): CC() class APIAbortSwitchContact(val contactId: Long): CC() @@ -2652,6 +2668,8 @@ sealed class CC { is ApiSetMemberSettings -> "/_member settings #$groupId $groupMemberId ${json.encodeToString(memberSettings)}" is APIContactInfo -> "/_info @$contactId" is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId" + is APIContactQueueInfo -> "/_queue info @$contactId" + is APIGroupMemberQueueInfo -> "/_queue info #$groupId $groupMemberId" is APISwitchContact -> "/_switch @$contactId" is APISwitchGroupMember -> "/_switch #$groupId $groupMemberId" is APIAbortSwitchContact -> "/_abort switch @$contactId" @@ -2790,6 +2808,8 @@ sealed class CC { is ApiSetMemberSettings -> "apiSetMemberSettings" is APIContactInfo -> "apiContactInfo" is APIGroupMemberInfo -> "apiGroupMemberInfo" + is APIContactQueueInfo -> "apiContactQueueInfo" + is APIGroupMemberQueueInfo -> "apiGroupMemberQueueInfo" is APISwitchContact -> "apiSwitchContact" is APISwitchGroupMember -> "apiSwitchGroupMember" is APIAbortSwitchContact -> "apiAbortSwitchContact" @@ -4197,6 +4217,7 @@ sealed class CR { @Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR() @Serializable @SerialName("contactInfo") class ContactInfo(val user: UserRef, val contact: Contact, val connectionStats_: ConnectionStats? = null, val customUserProfile: Profile? = null): CR() @Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats? = null): CR() + @Serializable @SerialName("queueInfo") class QueueInfoR(val user: UserRef, val rcvMsgInfo: RcvMsgInfo?, val queueInfo: QueueInfo): CR() @Serializable @SerialName("contactSwitchStarted") class ContactSwitchStarted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR() @Serializable @SerialName("groupMemberSwitchStarted") class GroupMemberSwitchStarted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR() @Serializable @SerialName("contactSwitchAborted") class ContactSwitchAborted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR() @@ -4368,6 +4389,7 @@ sealed class CR { is NetworkConfig -> "networkConfig" is ContactInfo -> "contactInfo" is GroupMemberInfo -> "groupMemberInfo" + is QueueInfoR -> "queueInfo" is ContactSwitchStarted -> "contactSwitchStarted" is GroupMemberSwitchStarted -> "groupMemberSwitchStarted" is ContactSwitchAborted -> "contactSwitchAborted" @@ -4529,6 +4551,7 @@ sealed class CR { is NetworkConfig -> json.encodeToString(networkConfig) is ContactInfo -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats_)}") is GroupMemberInfo -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats_)}") + is QueueInfoR -> withUser(user, "rcvMsgInfo: ${json.encodeToString(rcvMsgInfo)}\nqueueInfo: ${json.encodeToString(queueInfo)}\n") is ContactSwitchStarted -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}") is GroupMemberSwitchStarted -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats)}") is ContactSwitchAborted -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}") @@ -5786,3 +5809,52 @@ enum class UserNetworkType { OTHER -> generalGetString(MR.strings.network_type_other) } } + +@Serializable +data class RcvMsgInfo ( + val msgId: Long, + val msgDeliveryId: Long, + val msgDeliveryStatus: String, + val agentMsgId: Long, + val agentMsgMeta: String +) + +@Serializable +data class QueueInfo ( + val qiSnd: Boolean, + val qiNtf: Boolean, + val qiSub: QSub? = null, + val qiSize: Int, + val qiMsg: MsgInfo? = null +) + +@Serializable +data class QSub ( + val qSubThread: QSubThread, + val qDelivered: String? = null +) + +enum class QSubThread { + @SerialName("noSub") + NO_SUB, + @SerialName("subPending") + SUB_PENDING, + @SerialName("subThread") + SUB_THREAD, + @SerialName("prohibitSub") + PROHIBIT_SUB +} + +@Serializable +data class MsgInfo ( + val msgId: String, + val msgTs: Instant, + val msgType: MsgType, +) + +enum class MsgType { + @SerialName("message") + MESSAGE, + @SerialName("quota") + QUOTA +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 12b5747787..48ed0570a7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.datetime.Clock +import kotlinx.serialization.encodeToString import java.io.File @Composable @@ -418,6 +419,19 @@ fun ChatInfoLayout( SectionView(title = stringResource(MR.strings.section_title_for_console)) { InfoRow(stringResource(MR.strings.info_row_local_name), chat.chatInfo.localDisplayName) InfoRow(stringResource(MR.strings.info_row_database_id), chat.chatInfo.apiId.toString()) + SectionItemView({ + withBGApi { + val info = controller.apiContactQueueInfo(chat.remoteHostId, chat.chatInfo.apiId) + if (info != null) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.message_queue_info), + text = queueInfoText(info) + ) + } + } + }) { + Text(stringResource(MR.strings.info_row_debug_delivery)) + } } } SectionBottomSpacer() @@ -798,6 +812,12 @@ fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) { ) } +fun queueInfoText(info: Pair): String { + val (rcvMsgInfo, qInfo) = info + val msgInfo: String = if (rcvMsgInfo != null) json.encodeToString(rcvMsgInfo) else generalGetString(MR.strings.message_queue_info_none) + return generalGetString(MR.strings.message_queue_info_server_info).format(json.encodeToString(qInfo), msgInfo) +} + @Preview @Composable fun PreviewChatInfoLayout() { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 03d4e30a6c..71d82b5691 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.saveable.mapSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.graphics.* import androidx.compose.ui.platform.* import dev.icerock.moko.resources.compose.painterResource @@ -620,7 +621,7 @@ fun ChatLayout( .fillMaxSize() .background(MaterialTheme.colors.background) .then(if (wallpaperImage != null) - Modifier.drawBehind { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) } + Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) } else Modifier) .padding(contentPadding) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index d546b51a93..931cc17872 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -86,7 +86,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List { .map { it.chatInfo } .filterIsInstance() .map { it.contact } - .filter { c -> c.ready && c.active && c.contactId !in memberContactIds && c.chatViewName.lowercase().contains(s) } + .filter { c -> c.sendMsgEnabled && !c.nextSendGrpInv && c.contactId !in memberContactIds && c.chatViewName.lowercase().contains(s) } .sortedBy { it.displayName.lowercase() } .toList() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index e90efa7d1b..2799904b71 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -3,6 +3,7 @@ package chat.simplex.common.views.chat.group import InfoRow import SectionBottomSpacer import SectionDividerSpaced +import SectionItemView import SectionSpacer import SectionTextFooter import SectionView @@ -27,6 +28,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* @@ -58,6 +60,7 @@ fun GroupMemberInfoView( if (chat != null) { val newRole = remember { mutableStateOf(member.memberRole) } GroupMemberInfoLayout( + rhId = rhId, groupInfo, member, connStats, @@ -219,6 +222,7 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c @Composable fun GroupMemberInfoLayout( + rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connStats: MutableState, @@ -397,6 +401,19 @@ fun GroupMemberInfoLayout( SectionView(title = stringResource(MR.strings.section_title_for_console)) { InfoRow(stringResource(MR.strings.info_row_local_name), member.localDisplayName) InfoRow(stringResource(MR.strings.info_row_database_id), member.groupMemberId.toString()) + SectionItemView({ + withBGApi { + val info = controller.apiGroupMemberQueueInfo(rhId, groupInfo.apiId, member.groupMemberId) + if (info != null) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.message_queue_info), + text = queueInfoText(info) + ) + } + } + }) { + Text(stringResource(MR.strings.info_row_debug_delivery)) + } } } SectionBottomSpacer() @@ -644,6 +661,7 @@ fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocke fun PreviewGroupMemberInfoLayout() { SimpleXTheme { GroupMemberInfoLayout( + rhId = null, groupInfo = GroupInfo.sampleData, member = GroupMember.sampleData, connStats = remember { mutableStateOf(null) }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index a48bb2bb12..0ce9ba32fc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -154,13 +154,7 @@ fun CIFileView( FileProtocol.SMP -> progressIndicator() FileProtocol.LOCAL -> {} } - is CIFileStatus.SndComplete -> { - if ((file.forwardingAllowed() || (chatModel.connectedToRemote() && CIFile.cachedRemoteFileRequests[file.fileSource] == true))) { - fileIcon() - } else { - fileIcon(innerIcon = painterResource(MR.images.ic_check_filled)) - } - } + is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled)) is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.RcvInvitation -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 330003b743..8549f6abe2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -537,6 +537,7 @@ suspend fun exportChatArchive( if (!m.chatDbChanged.value) { controller.apiSaveAppSettings(AppSettings.current.prepareForExport()) } + wallpapersDir.mkdirs() m.controller.apiExportArchive(config) if (storagePath == null) { deleteOldArchive(m) @@ -592,6 +593,7 @@ private fun importArchive( withLongRunningApi { try { m.controller.apiDeleteStorage() + wallpapersDir.mkdirs() try { val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) val archiveErrors = m.controller.apiImportArchive(config) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt index 89796baf4e..8921685cd6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt @@ -1,12 +1,13 @@ package chat.simplex.common.views.helpers +import androidx.compose.ui.draw.CacheDrawScope +import androidx.compose.ui.draw.DrawResult +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.clipRect +import androidx.compose.ui.graphics.drawscope.* import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* @@ -352,9 +353,17 @@ sealed class WallpaperType { } } -fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, background: Color, tint: Color) = clipRect { - val quality = FilterQuality.High - fun repeat(imageScale: Float) { +private fun drawToBitmap(image: ImageBitmap, imageScale: Float, tint: Color, size: Size, density: Float, layoutDirection: LayoutDirection): ImageBitmap { + val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low + val drawScope = CanvasDrawScope() + val bitmap = ImageBitmap(size.width.toInt(), size.height.toInt()) + val canvas = Canvas(bitmap) + drawScope.draw( + density = Density(density), + layoutDirection = layoutDirection, + canvas = canvas, + size = size, + ) { val scale = imageScale * density for (h in 0..(size.height / image.height / scale).roundToInt()) { for (w in 0..(size.width / image.width / scale).roundToInt()) { @@ -368,50 +377,71 @@ fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, b } } } + return bitmap +} - drawRect(background) - when (imageType) { - is WallpaperType.Preset -> repeat((imageType.scale ?: 1f) * imageType.predefinedImageScale) - is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) { - WallpaperScaleType.REPEAT -> repeat(imageType.scale ?: 1f) - WallpaperScaleType.FILL, WallpaperScaleType.FIT -> { - val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height)) - val scaledWidth = (image.width * scale.scaleX).roundToInt() - val scaledHeight = (image.height * scale.scaleY).roundToInt() - // Large image will cause freeze - if (image.width > 4320 || image.height > 4320) return@clipRect +fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, background: Color, tint: Color): DrawResult { + val imageScale = if (imageType is WallpaperType.Preset) { + (imageType.scale ?: 1f) * imageType.predefinedImageScale + } else if (imageType is WallpaperType.Image && imageType.scaleType == WallpaperScaleType.REPEAT) { + imageType.scale ?: 1f + } else { + 1f + } + val image = if (imageType is WallpaperType.Preset || (imageType is WallpaperType.Image && imageType.scaleType == WallpaperScaleType.REPEAT)) { + drawToBitmap(image, imageScale, tint, size, density, layoutDirection) + } else { + image + } - drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - if (scaleType == WallpaperScaleType.FIT) { - if (scaledWidth < size.width) { - // has black lines at left and right sides - var x = (size.width - scaledWidth) / 2 - while (x > 0) { - drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - x -= scaledWidth - } - x = size.width - (size.width - scaledWidth) / 2 - while (x < size.width) { - drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - x += scaledWidth - } - } else { - // has black lines at top and bottom sides - var y = (size.height - scaledHeight) / 2 - while (y > 0) { - drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - y -= scaledHeight - } - y = size.height - (size.height - scaledHeight) / 2 - while (y < size.height) { - drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) - y += scaledHeight + return onDrawBehind { + val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low + drawRect(background) + when (imageType) { + is WallpaperType.Preset -> drawImage(image) + is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) { + WallpaperScaleType.REPEAT -> drawImage(image) + WallpaperScaleType.FILL, WallpaperScaleType.FIT -> { + clipRect { + val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height)) + val scaledWidth = (image.width * scale.scaleX).roundToInt() + val scaledHeight = (image.height * scale.scaleY).roundToInt() + // Large image will cause freeze + if (image.width > 4320 || image.height > 4320) return@clipRect + + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + if (scaleType == WallpaperScaleType.FIT) { + if (scaledWidth < size.width) { + // has black lines at left and right sides + var x = (size.width - scaledWidth) / 2 + while (x > 0) { + drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + x -= scaledWidth + } + x = size.width - (size.width - scaledWidth) / 2 + while (x < size.width) { + drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + x += scaledWidth + } + } else { + // has black lines at top and bottom sides + var y = (size.height - scaledHeight) / 2 + while (y > 0) { + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + y -= scaledHeight + } + y = size.height - (size.height - scaledHeight) / 2 + while (y < size.height) { + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + y += scaledHeight + } + } } } + drawRect(tint) } - drawRect(tint) } + is WallpaperType.Empty -> {} } - is WallpaperType.Empty -> {} } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt index c96a277fb8..94affad0e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt @@ -1,6 +1,5 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -13,12 +12,15 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* +import chat.simplex.common.model.NotificationsMode import chat.simplex.common.platform.onRightClick import chat.simplex.common.platform.windowWidth import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.SelectableCard import chat.simplex.common.views.usersettings.SettingsActionItemWithContent import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.stringResource @Composable fun SectionView(title: String? = null, padding: PaddingValues = PaddingValues(), content: (@Composable ColumnScope.() -> Unit)) { @@ -76,6 +78,26 @@ fun SectionViewSelectable( SectionTextFooter(values.first { it.value == currentValue.value }.description) } +@Composable +fun SectionViewSelectableCards( + title: String?, + currentValue: State, + values: List>, + onSelected: (T) -> Unit, +) { + SectionView(title) { + Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { + if (title != null) { + Text(title, Modifier.fillMaxWidth(), textAlign = TextAlign.Center) + Spacer(Modifier.height(DEFAULT_PADDING * 2f)) + } + values.forEach { item -> + SelectableCard(currentValue, item.value, item.title, item.description, onSelected) + } + } + } +} + @Composable fun SectionItemView( click: (() -> Unit)? = null, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt index f3a992d451..1a94676f79 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -594,6 +594,7 @@ private fun MutableState.importArchive(archivePath: String, n chatInitControllerRemovingDatabases() } controller.apiDeleteStorage() + wallpapersDir.mkdirs() try { val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) val archiveErrors = controller.apiImportArchive(config) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt index 9124808959..61ecee74e4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -35,9 +36,15 @@ fun SetNotificationsMode(m: ChatModel) { Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) { Text(stringResource(MR.strings.onboarding_notifications_mode_subtitle), Modifier.fillMaxWidth(), textAlign = TextAlign.Center) Spacer(Modifier.height(DEFAULT_PADDING * 2f)) - NotificationButton(currentMode, NotificationsMode.OFF, MR.strings.onboarding_notifications_mode_off, MR.strings.onboarding_notifications_mode_off_desc) - NotificationButton(currentMode, NotificationsMode.PERIODIC, MR.strings.onboarding_notifications_mode_periodic, MR.strings.onboarding_notifications_mode_periodic_desc) - NotificationButton(currentMode, NotificationsMode.SERVICE, MR.strings.onboarding_notifications_mode_service, MR.strings.onboarding_notifications_mode_service_desc) + SelectableCard(currentMode, NotificationsMode.OFF, stringResource(MR.strings.onboarding_notifications_mode_off), annotatedStringResource(MR.strings.onboarding_notifications_mode_off_desc)) { + currentMode.value = NotificationsMode.OFF + } + SelectableCard(currentMode, NotificationsMode.PERIODIC, stringResource(MR.strings.onboarding_notifications_mode_periodic), annotatedStringResource(MR.strings.onboarding_notifications_mode_periodic_desc)){ + currentMode.value = NotificationsMode.PERIODIC + } + SelectableCard(currentMode, NotificationsMode.SERVICE, stringResource(MR.strings.onboarding_notifications_mode_service), annotatedStringResource(MR.strings.onboarding_notifications_mode_service_desc)){ + currentMode.value = NotificationsMode.SERVICE + } } Spacer(Modifier.fillMaxHeight().weight(1f)) Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), contentAlignment = Alignment.Center) { @@ -54,22 +61,22 @@ fun SetNotificationsMode(m: ChatModel) { expect fun SetNotificationsModeAdditions() @Composable -private fun NotificationButton(currentMode: MutableState, mode: NotificationsMode, title: StringResource, description: StringResource) { +fun SelectableCard(currentValue: State, newValue: T, title: String, description: AnnotatedString, onSelected: (T) -> Unit) { TextButton( - onClick = { currentMode.value = mode }, - border = BorderStroke(1.dp, color = if (currentMode.value == mode) MaterialTheme.colors.primary else MaterialTheme.colors.secondary.copy(alpha = 0.5f)), + onClick = { onSelected(newValue) }, + border = BorderStroke(1.dp, color = if (currentValue.value == newValue) MaterialTheme.colors.primary else MaterialTheme.colors.secondary.copy(alpha = 0.5f)), shape = RoundedCornerShape(35.dp), ) { - Column(Modifier.padding(horizontal = 10.dp).padding(top = 4.dp, bottom = 8.dp)) { + Column(Modifier.padding(horizontal = 10.dp).padding(top = 4.dp, bottom = 8.dp).fillMaxWidth()) { Text( - stringResource(title), + title, style = MaterialTheme.typography.h3, fontWeight = FontWeight.Medium, - color = if (currentMode.value == mode) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + color = if (currentValue.value == newValue) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, modifier = Modifier.padding(bottom = 8.dp).align(Alignment.CenterHorizontally), textAlign = TextAlign.Center ) - Text(annotatedStringResource(description), + Text(description, Modifier.align(Alignment.CenterHorizontally), fontSize = 15.sp, color = MaterialTheme.colors.onBackground, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index df6c66deb4..caf021a381 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -271,7 +271,6 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v4_5_italian_interface, descrId = MR.strings.v4_5_italian_interface_descr, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -308,7 +307,6 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v4_6_chinese_spanish_interface, descrId = MR.strings.v4_6_chinese_spanish_interface_descr, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -330,7 +328,6 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v5_0_polish_interface, descrId = MR.strings.v5_0_polish_interface_descr, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -362,7 +359,6 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v5_1_japanese_portuguese_interface, descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -427,7 +423,6 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v5_3_new_interface_languages, descrId = MR.strings.v5_3_new_interface_languages_descr, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -491,7 +486,6 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v5_5_new_interface_languages, descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -554,7 +548,37 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v5_7_new_interface_languages, descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" + ) + ) + ), + VersionDescription( + version = "v5.8", + post = "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html", + features = listOf( + FeatureDescription( + icon = MR.images.ic_settings_ethernet, + titleId = MR.strings.v5_8_private_routing, + descrId = MR.strings.v5_8_private_routing_descr + ), + FeatureDescription( + icon = MR.images.ic_palette, + titleId = MR.strings.v5_8_chat_themes, + descrId = MR.strings.v5_8_chat_themes_descr + ), + FeatureDescription( + icon = MR.images.ic_security, + titleId = MR.strings.v5_8_safe_files, + descrId = MR.strings.v5_8_safe_files_descr + ), + FeatureDescription( + icon = MR.images.ic_battery_3_bar, + titleId = MR.strings.v5_8_message_delivery, + descrId = MR.strings.v5_8_message_delivery_descr + ), + FeatureDescription( + icon = MR.images.ic_translate, + titleId = MR.strings.v5_8_persian_ui, + descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate ) ) ), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt index 1de2c7c489..84fe333ba8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt @@ -95,11 +95,13 @@ object AppearanceScope { val backgroundColor = backgroundColor ?: wallpaperType?.defaultBackgroundColor(theme, MaterialTheme.colors.background) val tintColor = tintColor ?: wallpaperType?.defaultTintColor(theme) Column(Modifier - .drawBehind { + .drawWithCache { if (wallpaperImage != null && wallpaperType != null && backgroundColor != null && tintColor != null) { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) } else { - drawRect(themeBackgroundColor) + onDrawBehind { + drawRect(themeBackgroundColor) + } } } .padding(DEFAULT_PADDING_HALF) @@ -887,6 +889,7 @@ object AppearanceScope { "cs" to "Čeština", "de" to "Deutsch", "es" to "Español", + "fa" to "فارسی", "fi" to "Suomi", "fr" to "Français", "hu" to "Magyar", diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index d07fad8623..5898ccb657 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -7,6 +7,7 @@ import SectionItemView import SectionItemWithValue import SectionView import SectionViewSelectable +import SectionViewSelectableCards import TextIconSpaced import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -555,7 +556,7 @@ private fun SMPProxyModePicker( Modifier.fillMaxWidth(), ) { AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing)) - SectionViewSelectable(null, smpProxyMode, values, updateSMPProxyMode) + SectionViewSelectableCards(null, smpProxyMode, values, updateSMPProxyMode) } } } @@ -592,7 +593,7 @@ private fun SMPProxyFallbackPicker( Modifier.fillMaxWidth(), ) { AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade)) - SectionViewSelectable(null, smpProxyFallback, values, updateSMPProxyFallback) + SectionViewSelectableCards(null, smpProxyFallback, values, updateSMPProxyFallback) } } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 35e3591aea..df43d1459f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1401,6 +1401,7 @@ FOR CONSOLE Local name Database ID + Debug delivery Record updated at Sent at Created at @@ -1462,6 +1463,9 @@ Connection direct indirect (%1$s) + Message queue info + none + server queue info: %1$s\n\nlast received msg: %2$s Welcome message @@ -1850,6 +1854,15 @@ Network management More reliable network connection. Lithuanian UI + Private message routing 🚀 + Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings. + New chat themes + Make your chats look different! + Safely receive files + Confirm files from unknown servers. + Improved message delivery + With reduced battery usage. + Persian UI seconds diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_palette.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_palette.svg new file mode 100644 index 0000000000..f36ef054e0 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_palette.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 36149c8248..4128982f78 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -115,6 +115,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState) { false } }, title = "SimpleX") { +// val hardwareAccelerationDisabled = remember { listOf(GraphicsApi.SOFTWARE_FAST, GraphicsApi.SOFTWARE_COMPAT, GraphicsApi.UNKNOWN).contains(window.renderApi) } simplexWindowState.window = window AppScreen() if (simplexWindowState.openDialog.isAwaiting) { diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index 9925a6346b..f69cf817e5 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -3,7 +3,6 @@ package chat.simplex.desktop import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.foundation.* -import androidx.compose.foundation.interaction.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi @@ -17,6 +16,8 @@ import kotlinx.coroutines.* import java.io.File fun main() { + // Disable hardware acceleration + //System.setProperty("skiko.renderApi", "SOFTWARE") initHaskell() runMigrations() initApp() diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index c90510566a..309369811f 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -26,11 +26,11 @@ android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=5.8-beta.3 -android.version_code=214 +android.version_name=5.8-beta.4 +android.version_code=215 -desktop.version_name=5.8-beta.3 -desktop.version_code=49 +desktop.version_name=5.8-beta.4 +desktop.version_code=50 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 diff --git a/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md b/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md new file mode 100644 index 0000000000..b9c4e74237 --- /dev/null +++ b/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md @@ -0,0 +1,18 @@ +--- +layout: layouts/article.html +title: "SimpleX network: private message routing, v5.8 released with IP address protection and chat themes" +date: 2024-06-04 +# previewBody: blog_previews/20240426.html +draft: true +# image: images/20240426-profile.png +# imageBottom: true +permalink: "/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html" +--- + +# SimpleX network: private message routing, v5.8 released with IP address protection and chat themes + +**Published:** June 4, 2024 + +TODO + +This is a permalink for release announcement. diff --git a/cabal.project b/cabal.project index e04a03b210..6bd63f3bb2 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 0f663bd569f5d282fc13cd969391f30d873d73a0 + tag: 2e4f507919dd3a08e5c4106fde193fee4414de3c source-repository-package type: git diff --git a/package.yaml b/package.yaml index accab5f0c4..2e73355d6f 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 5.8.0.4 +version: 5.8.0.5 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index a719aa9629..6b74da3798 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."0f663bd569f5d282fc13cd969391f30d873d73a0" = "0xv3fjq6acr25hkkpb4grmdj0hls9z5qnq0dx7h7lxck60q0v9w8"; + "https://github.com/simplex-chat/simplexmq.git"."2e4f507919dd3a08e5c4106fde193fee4414de3c" = "1s035v77rpk8xs8gcfqm2ddp1mf5n35zrkq68gxqr634r2r9vbaj"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index b70c2dd71b..5c4e9cd53f 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 5.8.0.4 +version: 5.8.0.5 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 07f8586f33..5eb75148cc 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -94,7 +94,7 @@ import qualified Simplex.FileTransfer.Description as FD import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.Messaging.Agent as Agent -import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, ipAddressProtected, temporaryAgentError, withLockMap) +import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getNetworkConfig', ipAddressProtected, temporaryAgentError, withLockMap) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol @@ -103,7 +103,7 @@ import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), Migrati import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations -import Simplex.Messaging.Client (ProxyClientError (..), defaultNetworkConfig) +import Simplex.Messaging.Client (ProxyClientError (..), NetworkConfig (..), defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF @@ -218,7 +218,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote} - ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} + ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} backgroundMode = do let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable} @@ -303,7 +303,7 @@ newChatController let DefaultAgentServers {smp = defSmp, xftp = defXftp} = defaultServers smp' = fromMaybe defSmp (nonEmpty smpServers) xftp' = fromMaybe defXftp (nonEmpty xftpServers) - in defaultServers {smp = smp', xftp = xftp', netCfg = networkConfig} + in defaultServers {smp = smp', xftp = xftp', netCfg = updateNetworkConfig defaultNetworkConfig simpleNetCfg} agentServers :: ChatConfig -> IO InitialAgentServers agentServers config@ChatConfig {defaultServers = defServers@DefaultAgentServers {ntf, netCfg}} = do users <- withTransaction chatStore getUsers @@ -321,6 +321,13 @@ newChatController userServers :: User -> IO (NonEmpty (ProtoServerWithAuth p)) userServers user' = activeAgentServers config protocol <$> withTransaction chatStore (`getProtocolServers` user') +updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig +updateNetworkConfig cfg SimpleNetCfg {socksProxy, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors} = + let cfg1 = maybe cfg (\smpProxyMode -> cfg {smpProxyMode}) smpProxyMode_ + cfg2 = maybe cfg1 (\smpProxyFallback -> cfg1 {smpProxyFallback}) smpProxyFallback_ + cfg3 = maybe cfg2 (\tcpTimeout -> cfg2 {tcpTimeout, tcpConnectTimeout = (tcpTimeout * 3) `div` 2}) tcpTimeout_ + in cfg3 {socksProxy, logTLSErrors} + withChatLock :: String -> CM a -> CM a withChatLock name action = asks chatLock >>= \l -> withLock l name action @@ -1342,7 +1349,11 @@ processChatCommand' vr = \case processChatCommand $ APIGetChatItemTTL userId APISetNetworkConfig cfg -> withUser' $ \_ -> lift (withAgent' (`setNetworkConfig` cfg)) >> ok_ APIGetNetworkConfig -> withUser' $ \_ -> - lift $ CRNetworkConfig <$> withAgent' getNetworkConfig + CRNetworkConfig <$> lift getNetworkConfig + SetNetworkConfig netCfg -> do + cfg <- lift getNetworkConfig + void . processChatCommand $ APISetNetworkConfig $ updateNetworkConfig cfg netCfg + pure $ CRNetworkConfig cfg APISetNetworkInfo info -> lift (withAgent' (`setUserNetworkInfo` info)) >> ok_ ReconnectAllServers -> withUser' $ \_ -> lift (withAgent' reconnectAllServers) >> ok_ APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user -> case cType of @@ -1379,6 +1390,11 @@ processChatCommand' vr = \case forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) connectionStats <- mapM (withAgent . flip getConnectionServers) (contactConnId ct) pure $ CRContactInfo user ct connectionStats (fmap fromLocalProfile incognitoProfile) + APIContactQueueInfo contactId -> withUser $ \user -> do + ct@Contact {activeConn} <- withStore $ \db -> getContact db vr user contactId + case activeConn of + Just conn -> getConnQueueInfo user conn + Nothing -> throwChatError $ CEContactNotActive ct APIGroupInfo gId -> withUser $ \user -> do (g, s) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> liftIO (getGroupSummary db user gId) pure $ CRGroupInfo user g s @@ -1386,6 +1402,11 @@ processChatCommand' vr = \case (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m) pure $ CRGroupMemberInfo user g m connectionStats + APIGroupMemberQueueInfo gId gMemberId -> withUser $ \user -> do + GroupMember {activeConn} <- withStore $ \db -> getGroupMember db vr user gId gMemberId + case activeConn of + Just conn -> getConnQueueInfo user conn + Nothing -> throwChatError CEGroupMemberNotActive APISwitchContact contactId -> withUser $ \user -> do ct <- withStore $ \db -> getContact db vr user contactId case contactConnId ct of @@ -1497,6 +1518,8 @@ processChatCommand' vr = \case groupId <- withStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIGroupInfo groupId GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo + ContactQueueInfo cName -> withContactName cName APIContactQueueInfo + GroupMemberQueueInfo gName mName -> withMemberName gName mName APIGroupMemberQueueInfo SwitchContact cName -> withContactName cName APISwitchContact SwitchGroupMember gName mName -> withMemberName gName mName APISwitchGroupMember AbortSwitchContact cName -> withContactName cName APIAbortSwitchContact @@ -2247,6 +2270,7 @@ processChatCommand' vr = \case SubInfo {server, subError = Just e} -> M.alter (Just . maybe [e] (e :)) server m _ -> m GetAgentSubsDetails -> lift $ CRAgentSubsDetails <$> withAgent' getAgentSubscriptions + GetAgentQueuesInfo -> lift $ CRAgentQueuesInfo <$> withAgent' getAgentQueuesInfo -- CustomChatCommand is unsupported, it can be processed in preCmdHook -- in a modified CLI app or core - the hook should return Either ChatResponse ChatCommand CustomChatCommand _cmd -> withUser $ \user -> pure $ chatCmdError (Just user) "not supported" @@ -2794,6 +2818,9 @@ processChatCommand' vr = \case pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal} let ci = mkChatItem cd ciId content ciFile_ Nothing Nothing itemForwarded Nothing False createdAt Nothing createdAt pure . CRNewChatItem user $ AChatItem SCTLocal SMDSnd (LocalChat nf) ci + getConnQueueInfo user Connection {connId, agentConnId = AgentConnId acId} = do + msgInfo <- withStore' (`getLastRcvMsgInfo` connId) + CRQueueInfo user msgInfo <$> withAgent (`getConnectionQueueInfo` acId) contactCITimed :: Contact -> CM (Maybe CITimed) contactCITimed ct = sndContactCITimed False ct Nothing @@ -3178,7 +3205,7 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} pure $ filter (`notElem` knownSrvs) srvs ipProtectedForSrvs :: [XFTPServer] -> CM Bool ipProtectedForSrvs srvs = do - netCfg <- lift $ withAgent' getNetworkConfig + netCfg <- lift getNetworkConfig pure $ all (ipAddressProtected netCfg) srvs relaysNotApproved :: [XFTPServer] -> CM () relaysNotApproved unknownSrvs = do @@ -3186,6 +3213,9 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci throwChatError $ CEFileNotApproved fileId unknownSrvs +getNetworkConfig :: CM' NetworkConfig +getNetworkConfig = withAgent' $ liftIO . getNetworkConfig' + resetRcvCIFileStatus :: User -> FileTransferId -> CIFileStatus 'MDRcv -> CM (Maybe AChatItem) resetRcvCIFileStatus user fileId ciFileStatus = do vr <- chatVersionRange @@ -7349,7 +7379,7 @@ chatCommandP = "/ttl" $> GetChatItemTTL, "/_network info " *> (APISetNetworkInfo <$> jsonP), "/_network " *> (APISetNetworkConfig <$> jsonP), - ("/network " <|> "/net ") *> (APISetNetworkConfig <$> netCfgP), + ("/network " <|> "/net ") *> (SetNetworkConfig <$> netCfgP), ("/network" <|> "/net") $> APIGetNetworkConfig, "/reconnect" $> ReconnectAllServers, "/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP), @@ -7360,6 +7390,10 @@ chatCommandP = ("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* char_ '@' <*> displayName), ("/info #" <|> "/i #") *> (ShowGroupInfo <$> displayName), ("/info " <|> "/i ") *> char_ '@' *> (ContactInfo <$> displayName), + "/_queue info #" *> (APIGroupMemberQueueInfo <$> A.decimal <* A.space <*> A.decimal), + "/_queue info @" *> (APIContactQueueInfo <$> A.decimal), + ("/queue info #" <|> "/qi #") *> (GroupMemberQueueInfo <$> displayName <* A.space <* char_ '@' <*> displayName), + ("/queue info " <|> "/qi ") *> char_ '@' *> (ContactQueueInfo <$> displayName), "/_switch #" *> (APISwitchGroupMember <$> A.decimal <* A.space <*> A.decimal), "/_switch @" *> (APISwitchContact <$> A.decimal), "/_abort switch #" *> (APIAbortSwitchGroupMember <$> A.decimal <* A.space <*> A.decimal), @@ -7536,6 +7570,7 @@ chatCommandP = "/get workers" $> GetAgentWorkers, "/get workers details" $> GetAgentWorkersDetails, "/get msgs" $> GetAgentMsgCounts, + "/get queues" $> GetAgentQueuesInfo, "//" *> (CustomChatCommand <$> A.takeByteString) ] where @@ -7657,10 +7692,12 @@ chatCommandP = <|> ("no" $> TMEDisableKeepTTL) netCfgP = do socksProxy <- "socks=" *> ("off" $> Nothing <|> "on" $> Just defaultSocksProxy <|> Just <$> strP) + smpProxyMode_ <- optional $ " smp-proxy=" *> strP + smpProxyFallback_ <- optional $ " smp-proxy-fallback=" *> strP t_ <- optional $ " timeout=" *> A.decimal - logErrors <- " log=" *> onOffP <|> pure False - let tcpTimeout = 1000000 * fromMaybe (maybe 5 (const 10) socksProxy) t_ - pure $ fullNetworkConfig socksProxy tcpTimeout logErrors + logTLSErrors <- " log=" *> onOffP <|> pure False + let tcpTimeout_ = (1000000 *) <$> t_ + pure $ SimpleNetCfg {socksProxy, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors} dbKeyP = nonEmptyKey <$?> strP nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False} diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 71c198c47c..77b14b5eac 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -68,13 +68,14 @@ import Simplex.Chat.Types.UITheme import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) -import Simplex.Messaging.Agent.Client (AgentLocks, AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, UserNetworkInfo) +import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, UserNetworkInfo) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction) import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) import qualified Simplex.Messaging.Crypto.File as CF @@ -83,9 +84,10 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, XFTPServerWithAuth, userProtocol) +import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, simplexMQVersion) -import Simplex.Messaging.Transport.Client (TransportHost) +import Simplex.Messaging.Transport.Client (SocksProxy, TransportHost) import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors', (<$$>)) import Simplex.RemoteControl.Client import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitation) @@ -350,6 +352,7 @@ data ChatCommand | GetChatItemTTL | APISetNetworkConfig NetworkConfig | APIGetNetworkConfig + | SetNetworkConfig SimpleNetCfg | APISetNetworkInfo UserNetworkInfo | ReconnectAllServers | APISetChatSettings ChatRef ChatSettings @@ -357,6 +360,8 @@ data ChatCommand | APIContactInfo ContactId | APIGroupInfo GroupId | APIGroupMemberInfo GroupId GroupMemberId + | APIContactQueueInfo ContactId + | APIGroupMemberQueueInfo GroupId GroupMemberId | APISwitchContact ContactId | APISwitchGroupMember GroupId GroupMemberId | APIAbortSwitchContact ContactId @@ -375,6 +380,8 @@ data ChatCommand | ContactInfo ContactName | ShowGroupInfo GroupName | GroupMemberInfo GroupName ContactName + | ContactQueueInfo ContactName + | GroupMemberQueueInfo GroupName ContactName | SwitchContact ContactName | SwitchGroupMember GroupName ContactName | AbortSwitchContact ContactName @@ -504,6 +511,7 @@ data ChatCommand | GetAgentWorkers | GetAgentWorkersDetails | GetAgentMsgCounts + | GetAgentQueuesInfo | -- The parser will return this command for strings that start from "//". -- This command should be processed in preCmdHook CustomChatCommand ByteString @@ -568,6 +576,7 @@ data ChatResponse | CRContactInfo {user :: User, contact :: Contact, connectionStats_ :: Maybe ConnectionStats, customUserProfile :: Maybe Profile} | CRGroupInfo {user :: User, groupInfo :: GroupInfo, groupSummary :: GroupSummary} | CRGroupMemberInfo {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats} + | CRQueueInfo {user :: User, rcvMsgInfo :: Maybe RcvMsgInfo, queueInfo :: QueueInfo} | CRContactSwitchStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} | CRGroupMemberSwitchStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} | CRContactSwitchAborted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} @@ -750,6 +759,7 @@ data ChatResponse | CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]} | CRAgentSubsDetails {agentSubs :: SubscriptionsInfo} | CRAgentMsgCounts {msgCounts :: [(Text, (Int, Int))]} + | CRAgentQueuesInfo {agentQueuesInfo :: AgentQueuesInfo} | CRContactDisabled {user :: User, contact :: Contact} | CRConnectionDisabled {connectionEntity :: ConnectionEntity} | CRConnectionInactive {connectionEntity :: ConnectionEntity, inactive :: Bool} @@ -953,6 +963,18 @@ data AppFilePathsConfig = AppFilePathsConfig } deriving (Show) +data SimpleNetCfg = SimpleNetCfg + { socksProxy :: Maybe SocksProxy, + smpProxyMode_ :: Maybe SMPProxyMode, + smpProxyFallback_ :: Maybe SMPProxyFallback, + tcpTimeout_ :: Maybe Int, + logTLSErrors :: Bool + } + deriving (Show) + +defaultSimpleNetCfg :: SimpleNetCfg +defaultSimpleNetCfg = SimpleNetCfg Nothing Nothing Nothing Nothing False + data ContactSubStatus = ContactSubStatus { contact :: Contact, contactError :: Maybe ChatError diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 6c0e93e017..ea79161e06 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -962,6 +962,15 @@ data RcvMsgDelivery = RcvMsgDelivery } deriving (Show) +data RcvMsgInfo = RcvMsgInfo + { msgId :: Int64, + msgDeliveryId :: Int64, + msgDeliveryStatus :: Text, + agentMsgId :: AgentMsgId, + agentMsgMeta :: Text + } + deriving (Show) + data MsgMetaJSON = MsgMetaJSON { integrity :: Text, rcvId :: Int64, @@ -1332,3 +1341,5 @@ $(JQ.deriveJSON defaultJSON ''MsgMetaJSON) msgMetaJson :: MsgMeta -> Text msgMetaJson = decodeLatin1 . LB.toStrict . J.encode . msgMetaToJson + +$(JQ.deriveJSON defaultJSON ''RcvMsgInfo) diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 5883c6042c..486e0d62f3 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -48,7 +48,6 @@ import Simplex.Chat.Types import Simplex.Messaging.Agent.Client (agentClientStore) import Simplex.Messaging.Agent.Env.SQLite (createAgentStore) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, closeSQLiteStore, reopenSQLiteStore) -import Simplex.Messaging.Client (defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON) @@ -192,7 +191,7 @@ mobileChatOpts dbFilePrefix = dbKey = "", -- for API database is already opened, and the key in options is not used smpServers = [], xftpServers = [], - networkConfig = defaultNetworkConfig, + simpleNetCfg = defaultSimpleNetCfg, logLevel = CLLImportant, logConnections = False, logServerHosts = True, diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 871e3358ec..747414af37 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -13,7 +13,6 @@ module Simplex.Chat.Options coreChatOptsP, getChatOpts, protocolServersP, - fullNetworkConfig, ) where @@ -22,15 +21,17 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteArray (ScrubbedBytes) import qualified Data.ByteString.Char8 as B import Data.Text (Text) +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Numeric.Natural (Natural) import Options.Applicative -import Simplex.Chat.Controller (ChatLogLevel (..), updateStr, versionNumber, versionString) +import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString) import Simplex.FileTransfer.Description (mb) -import Simplex.Messaging.Client (NetworkConfig (..), defaultNetworkConfig) +import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth) -import Simplex.Messaging.Transport.Client (SocksProxy, defaultSocksProxy) +import Simplex.Messaging.Transport.Client (defaultSocksProxy) import System.FilePath (combine) data ChatOpts = ChatOpts @@ -55,7 +56,7 @@ data CoreChatOpts = CoreChatOpts dbKey :: ScrubbedBytes, smpServers :: [SMPServerWithAuth], xftpServers :: [XFTPServerWithAuth], - networkConfig :: NetworkConfig, + simpleNetCfg :: SimpleNetCfg, logLevel :: ChatLogLevel, logConnections :: Bool, logServerHosts :: Bool, @@ -123,18 +124,34 @@ coreChatOptsP appDir defaultDbFileName = do socksProxy <- flag' (Just defaultSocksProxy) (short 'x' <> help "Use local SOCKS5 proxy at :9050") <|> option - parseSocksProxy + strParse ( long "socks-proxy" <> metavar "SOCKS5" <> help "Use SOCKS5 proxy at `ipv4:port` or `:port`" <> value Nothing ) + smpProxyMode_ <- + optional $ + option + strParse + ( long "smp-proxy" + <> metavar "SMP_PROXY_MODE" + <> help "Use private message routing: always, unknown, unprotected, never (default)" + ) + smpProxyFallback_ <- + optional $ + option + strParse + ( long "smp-proxy-fallback" + <> metavar "SMP_PROXY_FALLBACK_MODE" + <> help "Allow downgrade and connect directly: no, [when IP address is] protected, yes (default)" + ) t <- option auto ( long "tcp-timeout" <> metavar "TIMEOUT" - <> help "TCP timeout, seconds (default: 5/10 without/with SOCKS5 proxy)" + <> help "TCP timeout, seconds (default: 7/15 without/with SOCKS5 proxy)" <> value 0 ) logLevel <- @@ -149,7 +166,7 @@ coreChatOptsP appDir defaultDbFileName = do logTLSErrors <- switch ( long "log-tls-errors" - <> help "Log TLS errors (also enabled with `-l debug`)" + <> help "Log TLS errors" ) logConnections <- switch @@ -194,7 +211,7 @@ coreChatOptsP appDir defaultDbFileName = do dbKey, smpServers, xftpServers, - networkConfig = fullNetworkConfig socksProxy (useTcpTimeout socksProxy t) (logTLSErrors || logLevel == CLLDebug), + simpleNetCfg = SimpleNetCfg {socksProxy, smpProxyMode_, smpProxyFallback_, tcpTimeout_ = Just $ useTcpTimeout socksProxy t, logTLSErrors}, logLevel, logConnections = logConnections || logLevel <= CLLInfo, logServerHosts = logServerHosts || logLevel <= CLLInfo, @@ -204,7 +221,7 @@ coreChatOptsP appDir defaultDbFileName = do highlyAvailable } where - useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 5 (const 10) p + useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 7 (const 15) p defaultDbFilePath = combine appDir defaultDbFileName chatOptsP :: FilePath -> FilePath -> Parser ChatOpts @@ -321,16 +338,11 @@ chatOptsP appDir defaultDbFileName = do maintenance } -fullNetworkConfig :: Maybe SocksProxy -> Int -> Bool -> NetworkConfig -fullNetworkConfig socksProxy tcpTimeout logTLSErrors = - let tcpConnectTimeout = (tcpTimeout * 3) `div` 2 - in defaultNetworkConfig {socksProxy, tcpTimeout, tcpConnectTimeout, logTLSErrors} - parseProtocolServers :: ProtocolTypeI p => ReadM [ProtoServerWithAuth p] parseProtocolServers = eitherReader $ parseAll protocolServersP . B.pack -parseSocksProxy :: ReadM (Maybe SocksProxy) -parseSocksProxy = eitherReader $ parseAll strP . B.pack +strParse :: StrEncoding a => ReadM a +strParse = eitherReader $ parseAll strP . encodeUtf8 . T.pack parseServerPort :: ReadM (Maybe String) parseServerPort = eitherReader $ parseAll serverPortP . B.pack diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 5b98ea119c..064fcc561e 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -72,11 +72,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [5, 8, 0, 2] +minRemoteCtrlVersion = AppVersion [5, 8, 0, 4] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [5, 8, 0, 2] +minRemoteHostVersion = AppVersion [5, 8, 0, 4] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 84b2536380..853d34995a 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -23,6 +23,7 @@ module Simplex.Chat.Store.Messages createNewSndMessage, createSndMsgDelivery, createNewMessageAndRcvMsgDelivery, + getLastRcvMsgInfo, createNewRcvMessage, updateSndMsgDeliveryStatus, createPendingGroupMessage, @@ -226,6 +227,23 @@ createNewMessageAndRcvMsgDelivery db connOrGroupId newMessage sharedMsgId_ RcvMs (msgId, connId, agentMsgId, msgMetaJson agentMsgMeta, snd $ broker agentMsgMeta, currentTs, currentTs, MDSRcvAgent) pure msg +getLastRcvMsgInfo :: DB.Connection -> Int64 -> IO (Maybe RcvMsgInfo) +getLastRcvMsgInfo db connId = + maybeFirstRow rcvMsgInfo $ + DB.query + db + [sql| + SELECT message_id, msg_delivery_id, delivery_status, agent_msg_id, agent_msg_meta + FROM msg_deliveries + WHERE connection_id = ? AND delivery_status IN (?, ?) + ORDER BY created_at DESC, msg_delivery_id DESC + LIMIT 1 + |] + (connId, MDSRcvAgent, MDSRcvAcknowledged) + where + rcvMsgInfo (msgId, msgDeliveryId, msgDeliveryStatus, agentMsgId, agentMsgMeta) = + RcvMsgInfo {msgId, msgDeliveryId, msgDeliveryStatus, agentMsgId, agentMsgMeta} + createNewRcvMessage :: forall e. MsgEncodingI e => DB.Connection -> ConnOrGroupId -> NewRcvMessage e -> Maybe SharedMsgId -> Maybe GroupMemberId -> Maybe GroupMemberId -> ExceptT StoreError IO RcvMessage createNewRcvMessage db connOrGroupId NewRcvMessage {chatMsgEvent, msgBody} sharedMsgId_ authorMember forwardedByMember = case connOrGroupId of diff --git a/src/Simplex/Chat/Terminal/Main.hs b/src/Simplex/Chat/Terminal/Main.hs index 2b26bb1d66..3e7d933669 100644 --- a/src/Simplex/Chat/Terminal/Main.hs +++ b/src/Simplex/Chat/Terminal/Main.hs @@ -6,15 +6,16 @@ module Simplex.Chat.Terminal.Main where import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent.STM import Control.Monad +import Data.Maybe (fromMaybe) import Data.Time.Clock (getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) import Network.Socket -import Simplex.Chat.Controller (ChatConfig, ChatController (..), ChatResponse (..), currentRemoteHost, versionNumber, versionString) +import Simplex.Chat.Controller (ChatConfig, ChatController (..), ChatResponse (..), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString) import Simplex.Chat.Core import Simplex.Chat.Options import Simplex.Chat.Terminal -import Simplex.Chat.View (serializeChatResponse) -import Simplex.Messaging.Client (NetworkConfig (..)) +import Simplex.Chat.View (serializeChatResponse, smpProxyModeStr) +import Simplex.Messaging.Client (NetworkConfig (..), defaultNetworkConfig) import System.Directory (getAppUserDataDirectory) import System.Exit (exitFailure) import System.Terminal (withTerminal) @@ -51,7 +52,7 @@ simplexChatCLI cfg server_ = do putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r welcome :: ChatOpts -> IO () -welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} = +welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, simpleNetCfg = SimpleNetCfg {socksProxy, smpProxyMode_, smpProxyFallback_}}} = mapM_ putStrLn [ versionString versionNumber, @@ -59,6 +60,9 @@ welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} = maybe "direct network connection - use `/network` command or `-x` CLI option to connect via SOCKS5 at :9050" (("using SOCKS5 proxy " <>) . show) - (socksProxy networkConfig), + socksProxy, + smpProxyModeStr + (fromMaybe (smpProxyMode defaultNetworkConfig) smpProxyMode_) + (fromMaybe (smpProxyFallback defaultNetworkConfig) smpProxyFallback_), "type \"/help\" or \"/h\" for usage info" ] diff --git a/src/Simplex/Chat/Types/UITheme.hs b/src/Simplex/Chat/Types/UITheme.hs index ed92caea0e..ef445c5a7c 100644 --- a/src/Simplex/Chat/Types/UITheme.hs +++ b/src/Simplex/Chat/Types/UITheme.hs @@ -128,13 +128,16 @@ data UIColors = UIColors background :: Maybe UIColor, menus :: Maybe UIColor, title :: Maybe UIColor, + accentVariant2 :: Maybe UIColor, sentMessage :: Maybe UIColor, - receivedMessage :: Maybe UIColor + sentReply :: Maybe UIColor, + receivedMessage :: Maybe UIColor, + receivedReply :: Maybe UIColor } deriving (Eq, Show) defaultUIColors :: UIColors -defaultUIColors = UIColors Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing +defaultUIColors = UIColors Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing newtype UIColor = UIColor String deriving (Eq, Show) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 01519a7fcc..6c02eac991 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -13,7 +13,6 @@ module Simplex.Chat.View where import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ -import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace, toUpper) @@ -56,6 +55,7 @@ import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestSte import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..)) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) +import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.Ratchet as CR @@ -65,7 +65,7 @@ import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtoServerWithAuth, ProtocolServer (..), ProtocolTypeI, SProtocolType (..)) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport.Client (TransportHost (..)) -import Simplex.Messaging.Util (bshow, tshow) +import Simplex.Messaging.Util (bshow, safeDecodeUtf8, tshow) import Simplex.Messaging.Version hiding (version) import Simplex.RemoteControl.Types (RCCtrlAddress (..)) import System.Console.ANSI.Types @@ -90,10 +90,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChatRunning -> ["chat is running"] CRChatStopped -> ["chat stopped"] CRChatSuspended -> ["chat suspended"] - CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [plain . bshow $ J.encode chats] + CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [viewJSON chats] CRChats chats -> viewChats ts tz chats - CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [plain . bshow $ J.encode chat] - CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft] + CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] + CRApiParsedMarkdown ft -> [viewJSON ft] CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl @@ -101,6 +101,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile CRGroupInfo u g s -> ttyUser u $ viewGroupInfo g s CRGroupMemberInfo u g m cStats -> ttyUser u $ viewGroupMemberInfo g m cStats + CRQueueInfo _ msgInfo qInfo -> + [ "last received msg: " <> maybe "none" viewJSON msgInfo, + "server queue info: " <> viewJSON qInfo + ] CRContactSwitchStarted {} -> ["switch started"] CRGroupMemberSwitchStarted {} -> ["switch started"] CRContactSwitchAborted {} -> ["switch aborted"] @@ -220,7 +224,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRSndFileError u (Just ci) _ e -> ttyUser u $ uploadingFile "error" ci <> [plain e] CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} -> ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft] - CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [plain . LB.toStrict $ J.encode j]) info_ + CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [viewJSON j]) info_ CRContactConnecting u _ -> ttyUser u [] CRContactConnected u ct userCustomProfile -> ttyUser u $ viewContactConnected ct userCustomProfile testView CRContactAnotherClient u c -> ttyUser u [ttyContact' c <> ": contact is connected to another client"] @@ -322,7 +326,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe ] CRRemoteFileStored rhId (CryptoFile filePath cfArgs_) -> [plain $ "file " <> filePath <> " stored on remote host " <> show rhId] - <> maybe [] ((: []) . plain . cryptoFileArgsStr testView) cfArgs_ + <> maybe [] ((: []) . cryptoFileArgsStr testView) cfArgs_ CRRemoteCtrlList cs -> viewRemoteCtrls cs CRRemoteCtrlFound {remoteCtrl = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName}, ctrlAppInfo_, appVersion, compatible} -> [ ("remote controller " <> sShow remoteCtrlId <> " found: ") @@ -354,8 +358,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries) CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} -> [ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName, - plain $ "chat entity locks: " <> LB.unpack (J.encode chatEntityLocks), - plain $ "agent locks: " <> LB.unpack (J.encode agentLocks) + "chat entity locks: " <> viewJSON chatEntityLocks, + "agent locks: " <> viewJSON agentLocks ] CRAgentStats stats -> map (plain . intercalate ",") stats CRAgentSubs {activeSubs, pendingSubs, removedSubs} -> @@ -370,12 +374,16 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe ("active subscriptions:" : map sShow activeSubscriptions) <> ("pending subscriptions: " : map sShow pendingSubscriptions) <> ("removed subscriptions: " : map sShow removedSubscriptions) - CRAgentWorkersSummary {agentWorkersSummary} -> ["agent workers summary: " <> plain (LB.unpack $ J.encode agentWorkersSummary)] + CRAgentWorkersSummary {agentWorkersSummary} -> ["agent workers summary: " <> viewJSON agentWorkersSummary] CRAgentWorkersDetails {agentWorkersDetails} -> [ "agent workers details:", - plain . LB.unpack $ J.encode agentWorkersDetails -- this would be huge, but copypastable when has its own line + viewJSON agentWorkersDetails -- this would be huge, but copypastable when has its own line + ] + CRAgentMsgCounts {msgCounts} -> ["received messages (total, duplicates):", viewJSON msgCounts] + CRAgentQueuesInfo {agentQueuesInfo} -> + [ "agent queues info:", + plain . LB.unpack $ J.encode agentQueuesInfo ] - CRAgentMsgCounts {msgCounts} -> ["received messages (total, duplicates):", plain . LB.unpack $ J.encode msgCounts] CRContactDisabled u c -> ttyUser u ["[" <> ttyContact' c <> "] connection is disabled, to enable: " <> highlight ("/enable " <> viewContactName c) <> ", to delete: " <> highlight ("/d " <> viewContactName c)] CRConnectionDisabled entity -> viewConnectionEntityDisabled entity CRConnectionInactive entity inactive -> viewConnectionEntityInactive entity inactive @@ -393,7 +401,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChatError u e -> ttyUser' u $ viewChatError False logLevel testView e CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError False logLevel testView) errs CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)] - CRAppSettings as -> ["app settings: " <> plain (LB.unpack $ J.encode as)] + CRAppSettings as -> ["app settings: " <> viewJSON as] CRTimedAction _ _ -> [] CRCustomChatResponse u r -> ttyUser' u $ [plain r] where @@ -1206,12 +1214,17 @@ viewChatItemTTL = \case deletedAfter ttlStr = ["old messages are set to be deleted after: " <> ttlStr] viewNetworkConfig :: NetworkConfig -> [StyledString] -viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} = +viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout, smpProxyMode, smpProxyFallback} = [ plain $ maybe "direct network connection" (("using SOCKS5 proxy " <>) . show) socksProxy, "TCP timeout: " <> sShow tcpTimeout, - "use " <> highlight' "/network socks=[ timeout=]" <> " to change settings" + plain $ smpProxyModeStr smpProxyMode smpProxyFallback, + "use " <> highlight' "/network socks=[ timeout=][ smp-proxy=always/unknown/unprotected/never][ smp-proxy-fallback=no/protected/yes]" <> " to change settings" ] +smpProxyModeStr :: SMPProxyMode -> SMPProxyFallback -> String +smpProxyModeStr SPMNever _ = "private message routing disabled." +smpProxyModeStr mode fallback = T.unpack $ safeDecodeUtf8 $ "private message routing mode: " <> strEncode mode <> ", fallback: " <> strEncode fallback + viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString] viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, uiThemes, customData} stats incognitoProfile = ["contact ID: " <> sShow contactId] @@ -1237,10 +1250,10 @@ viewGroupInfo GroupInfo {groupId, uiThemes, customData} s = <> viewCustomData customData viewUITheme :: Maybe UIThemeEntityOverrides -> [StyledString] -viewUITheme = maybe [] (\uiThemes -> ["UI themes: " <> plain (LB.toStrict $ J.encode uiThemes)]) +viewUITheme = maybe [] (\uiThemes -> ["UI themes: " <> viewJSON uiThemes]) viewCustomData :: Maybe CustomData -> [StyledString] -viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> plain (LB.toStrict . J.encode $ J.Object v)]) +viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> viewJSON (J.Object v)]) viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString] viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias, contactLink}, activeConn} stats = @@ -1682,7 +1695,7 @@ receivingFile_' :: (Maybe RemoteHostId, Maybe User) -> Bool -> String -> AChatIt receivingFile_' hu testView status (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileName, fileSource = Just f@(CryptoFile _ cfArgs_)}, chatDir}) = [plain status <> " receiving " <> fileTransferStr fileId fileName <> fileFrom chat chatDir] <> cfArgsStr cfArgs_ <> getRemoteFileStr where - cfArgsStr (Just cfArgs) = [plain (cryptoFileArgsStr testView cfArgs) | status == "completed"] + cfArgsStr (Just cfArgs) = [cryptoFileArgsStr testView cfArgs | status == "completed"] cfArgsStr _ = [] getRemoteFileStr = case hu of (Just rhId, Just User {userId}) @@ -1703,10 +1716,10 @@ viewLocalFile to CIFile {fileId, fileSource} ts tz = case fileSource of Just (CryptoFile fPath _) -> sentWithTime_ ts tz [to <> fileTransferStr fileId fPath] _ -> const [] -cryptoFileArgsStr :: Bool -> CryptoFileArgs -> ByteString +cryptoFileArgsStr :: Bool -> CryptoFileArgs -> StyledString cryptoFileArgsStr testView cfArgs@(CFArgs key nonce) - | testView = LB.toStrict $ J.encode cfArgs - | otherwise = "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce + | testView = viewJSON cfArgs + | otherwise = plain $ "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce fileFrom :: ChatInfo c -> CIDirection c d -> StyledString fileFrom (DirectChat ct) CIDirectRcv = " from " <> ttyContact' ct @@ -1824,7 +1837,7 @@ viewCallAnswer ct WebRTCSession {rtcSession = answer, rtcIceCandidates = iceCand [ ttyContact' ct <> " continued the WebRTC call", "To connect, please paste the data below in your browser window you opened earlier and click Connect button", "", - plain . LB.toStrict . J.encode $ WCCallAnswer {answer, iceCandidates} + viewJSON WCCallAnswer {answer, iceCandidates} ] callMediaStr :: CallType -> StyledString @@ -2082,6 +2095,9 @@ viewConnectionEntityInactive entity inactive | inactive = ["[" <> connEntityLabel entity <> "] connection is marked as inactive"] | otherwise = ["[" <> connEntityLabel entity <> "] inactive connection is marked as active"] +viewJSON :: J.ToJSON a => a -> StyledString +viewJSON = plain . LB.toStrict . J.encode + connEntityLabel :: ConnectionEntity -> StyledString connEntityLabel = \case RcvDirectMsgConnection _ (Just Contact {localDisplayName = c}) -> plain c diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 8f7bd1dc4e..589d880e8f 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -25,7 +25,7 @@ import Data.Maybe (isNothing) import qualified Data.Text as T import Network.Socket import Simplex.Chat -import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..)) +import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), defaultSimpleNetCfg) import Simplex.Chat.Core import Simplex.Chat.Options import Simplex.Chat.Protocol (currentChatVersion, pqEncryptionCompressionVersion) @@ -44,7 +44,7 @@ import Simplex.Messaging.Agent.Protocol (currentSMPAgentVersion, duplexHandshake import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), closeSQLiteStore) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB -import Simplex.Messaging.Client (ProtocolClientConfig (..), defaultNetworkConfig) +import Simplex.Messaging.Client (ProtocolClientConfig (..)) import Simplex.Messaging.Client.Agent (defaultSMPClientAgentConfig) import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange) import qualified Simplex.Messaging.Crypto.Ratchet as CR @@ -94,7 +94,7 @@ testCoreOpts = -- dbKey = "this is a pass-phrase to encrypt the database", smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], - networkConfig = defaultNetworkConfig, + simpleNetCfg = defaultSimpleNetCfg, logLevel = CLLImportant, logConnections = False, logServerHosts = False, @@ -432,7 +432,8 @@ smpServerCfg = controlPort = Nothing, smpAgentCfg = defaultSMPClientAgentConfig, allowSMPProxy = False, - serverClientConcurrency = 16 + serverClientConcurrency = 16, + information = Nothing } withSmpServer :: IO () -> IO () @@ -472,7 +473,8 @@ xftpServerConfig = serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log", serverStatsBackupFile = Nothing, controlPort = Nothing, - transportConfig = defaultTransportServerConfig + transportConfig = defaultTransportServerConfig, + responseDelay = 0 } withXFTPServer :: IO () -> IO ()