diff --git a/README.md b/README.md index 52f753a5ab..ad70c350e4 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,7 @@ You can use SimpleX with your own servers and still communicate with people usin Recent and important updates: -[Nov 25, 2025. Servers operated by Flux - true privacy and decentralization for all users](./20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md) +[Dec 10, 2024. SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps](./20241210-simplex-network-v6-2-servers-by-flux-business-chats.md) [Oct 14, 2024. SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience.](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md) @@ -243,20 +243,14 @@ Recent and important updates: [Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) -[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md) - [Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md). -[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md). - [Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md). [Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md). [Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). -[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md). - [All updates](./blog) ## :zap: Quick installation of a terminal app @@ -384,9 +378,11 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A - ✅ Improve sending videos (including encryption of locally stored videos). - ✅ Post-quantum resistant key exchange in double ratchet protocol. - ✅ Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic). +- ✅ Support multiple network operators in the app. +- 🏗 Large groups, communities and public channels. +- 🏗 Short links to connect and join groups. - 🏗 Improve stability and reduce battery usage. - 🏗 Improve experience for the new users. -- 🏗 Large groups, communities and public channels. - Privacy & security slider - a simple way to set all settings at once. - SMP queue redundancy and rotation (manual is supported). - Include optional message into connection request sent via contact address. diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index ea9daa74bc..9b6b9b73e8 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -156,8 +156,8 @@ struct ChatInfoView: View { HStack(alignment: .center, spacing: 8) { let buttonWidth = g.size.width / 4 searchButton(width: buttonWidth) - AudioCallButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) } - VideoButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) } + AudioCallButton(chat: chat, contact: contact, connectionStats: $connectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) } + VideoButton(chat: chat, contact: contact, connectionStats: $connectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) } muteButton(width: buttonWidth) } } @@ -314,7 +314,15 @@ struct ChatInfoView: View { case .networkStatusAlert: return networkStatusAlert() case .switchAddressAlert: return switchAddressAlert(switchContactAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress) - case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) }) + case .syncConnectionForceAlert: + return syncConnectionForceAlert({ + Task { + if let stats = await syncContactConnection(contact, force: true, showAlert: { alert = .someAlert(alert: $0) }) { + connectionStats = stats + dismiss() + } + } + }) case let .queueInfo(info): return queueInfoAlert(info) case let .someAlert(a): return a.alert case let .error(title, error): return mkAlert(title: title, message: error) @@ -493,7 +501,12 @@ struct ChatInfoView: View { private func synchronizeConnectionButton() -> some View { Button { - syncContactConnection(force: false) + Task { + if let stats = await syncContactConnection(contact, force: false, showAlert: { alert = .someAlert(alert: $0) }) { + connectionStats = stats + dismiss() + } + } } label: { Label("Fix connection", systemImage: "exclamationmark.arrow.triangle.2.circlepath") .foregroundColor(.orange) @@ -612,25 +625,6 @@ struct ChatInfoView: View { } } - private func syncContactConnection(force: Bool) { - Task { - do { - let stats = try apiSyncContactRatchet(contact.apiId, force) - connectionStats = stats - await MainActor.run { - chatModel.updateContactConnectionStats(contact, stats) - dismiss() - } - } catch let error { - logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))") - let a = getErrorAlert(error, "Error synchronizing connection") - await MainActor.run { - alert = .error(title: a.title, error: a.message) - } - } - } - } - private func savePreferences() { Task { do { @@ -649,9 +643,32 @@ struct ChatInfoView: View { } } +func syncContactConnection(_ contact: Contact, force: Bool, showAlert: (SomeAlert) -> Void) async -> ConnectionStats? { + do { + let stats = try apiSyncContactRatchet(contact.apiId, force) + await MainActor.run { + ChatModel.shared.updateContactConnectionStats(contact, stats) + } + return stats + } catch let error { + logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))") + let a = getErrorAlert(error, "Error synchronizing connection") + await MainActor.run { + showAlert( + SomeAlert( + alert: mkAlert(title: a.title, message: a.message), + id: "syncContactConnection error" + ) + ) + } + return nil + } +} + struct AudioCallButton: View { var chat: Chat var contact: Contact + @Binding var connectionStats: ConnectionStats? var width: CGFloat var showAlert: (SomeAlert) -> Void @@ -659,6 +676,7 @@ struct AudioCallButton: View { CallButton( chat: chat, contact: contact, + connectionStats: $connectionStats, image: "phone.fill", title: "call", mediaType: .audio, @@ -671,6 +689,7 @@ struct AudioCallButton: View { struct VideoButton: View { var chat: Chat var contact: Contact + @Binding var connectionStats: ConnectionStats? var width: CGFloat var showAlert: (SomeAlert) -> Void @@ -678,6 +697,7 @@ struct VideoButton: View { CallButton( chat: chat, contact: contact, + connectionStats: $connectionStats, image: "video.fill", title: "video", mediaType: .video, @@ -690,6 +710,7 @@ struct VideoButton: View { private struct CallButton: View { var chat: Chat var contact: Contact + @Binding var connectionStats: ConnectionStats? var image: String var title: LocalizedStringKey var mediaType: CallMediaType @@ -701,12 +722,40 @@ private struct CallButton: View { InfoViewButton(image: image, title: title, disabledLook: !canCall, width: width) { if canCall { - if CallController.useCallKit() { - CallController.shared.startCall(contact, mediaType) - } else { - // When CallKit is not used, colorscheme will be changed and it will be visible if not hiding sheets first - dismissAllSheets(animated: true) { - CallController.shared.startCall(contact, mediaType) + if let connStats = connectionStats { + if connStats.ratchetSyncState == .ok { + if CallController.useCallKit() { + CallController.shared.startCall(contact, mediaType) + } else { + // When CallKit is not used, colorscheme will be changed and it will be visible if not hiding sheets first + dismissAllSheets(animated: true) { + CallController.shared.startCall(contact, mediaType) + } + } + } else if connStats.ratchetSyncAllowed { + showAlert(SomeAlert( + alert: Alert( + title: Text("Fix connection?"), + message: Text("Connection requires encryption renegotiation."), + primaryButton: .default(Text("Fix")) { + Task { + if let stats = await syncContactConnection(contact, force: false, showAlert: showAlert) { + connectionStats = stats + } + } + }, + secondaryButton: .cancel() + ), + id: "can't call contact, fix connection" + )) + } else { + showAlert(SomeAlert( + alert: mkAlert( + title: "Can't call contact", + message: "Encryption renegotiation in progress." + ), + id: "can't call contact, encryption renegotiation in progress" + )) } } } else if contact.nextSendGrpInv { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 0c5a458930..90c277ce76 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -440,6 +440,7 @@ struct ChatView: View { maxWidth: maxWidth, composeState: $composeState, selectedMember: $selectedMember, + showChatInfoSheet: $showChatInfoSheet, revealedChatItem: $revealedChatItem, selectedChatItems: $selectedChatItems, forwardedChatItems: $forwardedChatItems @@ -893,12 +894,14 @@ struct ChatView: View { private struct ChatItemWithMenu: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner @Binding @ObservedObject var chat: Chat @ObservedObject var dummyModel: ChatItemDummyModel = .shared let chatItem: ChatItem let maxWidth: CGFloat @Binding var composeState: ComposeState @Binding var selectedMember: GMember? + @Binding var showChatInfoSheet: Bool @Binding var revealedChatItem: ChatItem? @State private var deletingItem: ChatItem? = nil @@ -1255,16 +1258,22 @@ struct ChatView: View { setReaction(ci, add: !r.userReacted, reaction: r.reaction) } } - if case let .group(groupInfo) = chat.chatInfo { + switch chat.chatInfo { + case let .group(groupInfo): v.contextMenu { ReactionContextMenu( groupInfo: groupInfo, itemId: ci.id, reactionCount: r, - selectedMember: $selectedMember + selectedMember: $selectedMember, + profileRadius: profileRadius ) } - } else { + case let .direct(contact): + v.contextMenu { + contactReactionMenu(contact, r) + } + default: v } } @@ -1767,6 +1776,20 @@ struct ChatView: View { } } } + + @ViewBuilder private func contactReactionMenu(_ contact: Contact, _ r: CIReactionCount) -> some View { + if !r.userReacted || r.totalReacted > 1 { + Button { showChatInfoSheet = true } label: { + profileMenuItem(Text(contact.displayName), contact.image, radius: profileRadius) + } + } + if r.userReacted { + Button {} label: { + profileMenuItem(Text("you"), m.currentUser?.profile.image, radius: profileRadius) + } + .disabled(true) + } + } private struct SelectedChatItem: View { @EnvironmentObject var theme: AppTheme @@ -1859,13 +1882,12 @@ struct ReactionContextMenu: View { var itemId: Int64 var reactionCount: CIReactionCount @Binding var selectedMember: GMember? + var profileRadius: CGFloat @State private var memberReactions: [MemberReaction] = [] - @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner var body: some View { groupMemberReactionList() .task { - logger.debug("ReactionContextMenu task \(radius)") await loadChatItemReaction() } } @@ -1889,27 +1911,12 @@ struct ReactionContextMenu: View { selectedMember = member } } label: { - HStack { - Text(mem.displayName) - if let img = cropImage(mem.image) { - Image(uiImage: img) - } else { - Image(systemName: "person.crop.circle") - } - } + profileMenuItem(Text(mem.displayName), mem.image, radius: profileRadius) } .disabled(userMember) } } } - - private func cropImage(_ img: String?) -> UIImage? { - return if let originalImage = imageFromBase64(img) { - maskToCustomShape(originalImage, size: 30, radius: radius) - } else { - nil - } - } private func loadChatItemReaction() async { do { @@ -1927,6 +1934,17 @@ struct ReactionContextMenu: View { } } +func profileMenuItem(_ nameText: Text, _ image: String?, radius: CGFloat) -> some View { + HStack { + nameText + if let image, let img = imageFromBase64(image) { + Image(uiImage: maskToCustomShape(img, size: 30, radius: radius)) + } else { + Image(systemName: "person.crop.circle") + } + } +} + func maskToCustomShape(_ image: UIImage, size: CGFloat, radius: CGFloat) -> UIImage { let path = Path { path in if radius >= 50 { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index b73c5e10f5..a18de1b349 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -20,6 +20,9 @@ struct GroupMemberInfoView: View { @State private var connectionStats: ConnectionStats? = nil @State private var connectionCode: String? = nil @State private var connectionLoaded: Bool = false + @State private var knownContactChat: Chat? = nil + @State private var knownContact: Contact? = nil + @State private var knownContactConnectionStats: ConnectionStats? = nil @State private var newRole: GroupMemberRole = .member @State private var alert: GroupMemberInfoViewAlert? @State private var sheet: PlanAndConnectActionSheet? @@ -119,8 +122,8 @@ struct GroupMemberInfoView: View { } label: { Label("Share address", systemImage: "square.and.arrow.up") } - if let contactId = member.memberContactId { - if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { + if member.memberContactId != nil { + if knownContactChat == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { connectViaAddressButton(contactLink) } } else { @@ -229,6 +232,18 @@ struct GroupMemberInfoView: View { } logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))") } + if let contactId = member.memberContactId, let (contactChat, contact) = knownDirectChat(contactId) { + knownContactChat = contactChat + knownContact = contact + do { + let (stats, _) = try await apiContactInfo(contactChat.chatInfo.apiId) + await MainActor.run { + knownContactConnectionStats = stats + } + } catch let error { + logger.error("apiContactInfo error: \(responseError(error))") + } + } } .onChange(of: newRole) { newRole in if newRole != member.memberRole { @@ -274,10 +289,10 @@ struct GroupMemberInfoView: View { GeometryReader { g in let buttonWidth = g.size.width / 4 HStack(alignment: .center, spacing: 8) { - if let contactId = member.memberContactId, let (chat, contact) = knownDirectChat(contactId) { + if let chat = knownContactChat, let contact = knownContact { knownDirectChatButton(chat, width: buttonWidth) - AudioCallButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) } - VideoButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) } + AudioCallButton(chat: chat, contact: contact, connectionStats: $knownContactConnectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) } + VideoButton(chat: chat, contact: contact, connectionStats: $knownContactConnectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) } } else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { if let contactId = member.memberContactId { newDirectChatButton(contactId, width: buttonWidth) @@ -366,25 +381,49 @@ struct GroupMemberInfoView: View { func createMemberContactButton(width: CGFloat) -> some View { InfoViewButton(image: "message.fill", title: "message", width: width) { - progressIndicator = true - Task { - do { - let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId) - await MainActor.run { - progressIndicator = false - chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact))) - ItemsModel.shared.loadOpenChat(memberContact.id) { - dismissAllSheets(animated: true) + if let connStats = connectionStats { + if connStats.ratchetSyncState == .ok { + progressIndicator = true + Task { + do { + let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId) + await MainActor.run { + progressIndicator = false + chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact))) + ItemsModel.shared.loadOpenChat(memberContact.id) { + dismissAllSheets(animated: true) + } + NetworkModel.shared.setContactNetworkStatus(memberContact, .connected) + } + } catch let error { + logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))") + let a = getErrorAlert(error, "Error creating member contact") + await MainActor.run { + progressIndicator = false + alert = .error(title: a.title, error: a.message) + } } - NetworkModel.shared.setContactNetworkStatus(memberContact, .connected) - } - } catch let error { - logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))") - let a = getErrorAlert(error, "Error creating member contact") - await MainActor.run { - progressIndicator = false - alert = .error(title: a.title, error: a.message) } + } else if connStats.ratchetSyncAllowed { + alert = .someAlert(alert: SomeAlert( + alert: Alert( + title: Text("Fix connection?"), + message: Text("Connection requires encryption renegotiation."), + primaryButton: .default(Text("Fix")) { + syncMemberConnection(force: false) + }, + secondaryButton: .cancel() + ), + id: "can't message member, fix connection" + )) + } else { + alert = .someAlert(alert: SomeAlert( + alert: mkAlert( + title: "Can't message member", + message: "Encryption renegotiation in progress." + ), + id: "can't message contact, encryption renegotiation in progress" + )) } } } diff --git a/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift index b87b84ebc0..aa802c1af9 100644 --- a/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift +++ b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift @@ -587,7 +587,7 @@ struct SMPStatsView: View { } header: { Text("Statistics") } footer: { - Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.") + Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is kept private on your device.") } } } @@ -703,7 +703,7 @@ struct XFTPStatsView: View { } header: { Text("Statistics") } footer: { - Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.") + Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is kept private on your device.") } } } 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 9260ac41c0..4aa1f2213f 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -731,8 +731,8 @@ Всички данни се изтриват при въвеждане. No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. No comment provided by engineer. 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 d921471f7f..668888c20e 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -712,8 +712,8 @@ Všechna data se při zadání vymažou. No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. 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 1fc614becf..e993740f1c 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -760,8 +760,8 @@ Alle Daten werden gelöscht, sobald dieser eingegeben wird. No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. Alle Daten werden nur auf Ihrem Gerät gespeichert. No comment provided by engineer. 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 004d7f0d31..cebd6c90d1 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -760,9 +760,9 @@ All data is erased when it is entered. No comment provided by engineer. - - All data is private to your device. - All data is private to your device. + + All data is kept private on your device. + All data is kept private on your device. No comment provided by engineer. 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 a96aebebae..08522cc617 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -760,8 +760,8 @@ Al introducirlo todos los datos son eliminados. No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. Todos los datos son privados y están en tu dispositivo. No comment provided by engineer. 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 2f67ee9d7d..2caa98e25b 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -707,8 +707,8 @@ Kaikki tiedot poistetaan, kun se syötetään. No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. 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 74002293d7..148156b07c 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -745,8 +745,8 @@ Toutes les données sont effacées lorsqu'il est saisi. No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. Toutes les données restent confinées dans votre appareil. No comment provided by engineer. 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 c682a02d8c..231c33523d 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -760,8 +760,8 @@ A jelkód megadása után az összes adat törlésre kerül. No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. Az összes adat biztonságban van az eszközén. No comment provided by engineer. 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 8a32fd3277..d785acda81 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -759,8 +759,8 @@ Tutti i dati vengono cancellati quando inserito. No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. Tutti i dati sono privati, nel tuo dispositivo. No comment provided by engineer. 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 43e6f24cf7..72e68cff48 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -724,8 +724,8 @@ 入力するとすべてのデータが消去されます。 No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. 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 73a9d05b73..ab3499a4dc 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -760,8 +760,8 @@ Alle gegevens worden bij het invoeren gewist. No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. Alle gegevens zijn privé op uw apparaat. No comment provided by engineer. 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 e7c9863152..8cfdf56f66 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -745,8 +745,8 @@ Wszystkie dane są usuwane po jego wprowadzeniu. No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. Wszystkie dane są prywatne na Twoim urządzeniu. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff index 9badf9c2e4..93ba6f357b 100644 --- a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff +++ b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff @@ -5425,8 +5425,8 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi Advanced settings Configurações avançadas - - All data is private to your device. + + All data is kept private on your device. Toda informação é privada em seu dispositivo. 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 814b878a03..5809c65216 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -760,8 +760,8 @@ Все данные удаляются при его вводе. No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. Все данные хранятся только на вашем устройстве. No comment provided by engineer. 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 177f426c1a..4317787f67 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -699,8 +699,8 @@ ข้อมูลทั้งหมดจะถูกลบเมื่อถูกป้อน No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. 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 d88adc3235..261752aefc 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -745,8 +745,8 @@ Kullanıldığında bütün veriler silinir. No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. Tüm veriler cihazınıza özeldir. No comment provided by engineer. 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 d68b5abbe1..d7dcc58dcd 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -756,8 +756,8 @@ Всі дані стираються при введенні. No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. Всі дані є приватними для вашого пристрою. No comment provided by engineer. 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 99d4a5077f..d6e548c6be 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 @@ -739,8 +739,8 @@ 所有数据在输入后将被删除。 No comment provided by engineer. - - All data is private to your device. + + All data is kept private on your device. 所有数据都是您设备的私有数据. No comment provided by engineer. diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 30b9a27e0e..759b16b196 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -1931,7 +1931,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1956,7 +1956,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1980,7 +1980,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2005,7 +2005,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -2021,11 +2021,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2041,11 +2041,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2066,7 +2066,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2081,7 +2081,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2103,7 +2103,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2118,7 +2118,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2140,7 +2140,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2166,7 +2166,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2191,7 +2191,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2217,7 +2217,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -2242,7 +2242,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2257,7 +2257,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -2276,7 +2276,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 254; + CURRENT_PROJECT_VERSION = 255; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2291,7 +2291,7 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 6.2; + MARKETING_VERSION = 6.2.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 7d69adc1d5..cad89ed29a 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -479,7 +479,7 @@ "All data is erased when it is entered." = "Alle Daten werden gelöscht, sobald dieser eingegeben wird."; /* No comment provided by engineer. */ -"All data is private to your device." = "Alle Daten werden nur auf Ihrem Gerät gespeichert."; +"All data is kept private on your device." = "Alle Daten werden nur auf Ihrem Gerät gespeichert."; /* No comment provided by engineer. */ "All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden."; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index ce36dec953..e7570f177e 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -479,7 +479,7 @@ "All data is erased when it is entered." = "Al introducirlo todos los datos son eliminados."; /* No comment provided by engineer. */ -"All data is private to your device." = "Todos los datos son privados y están en tu dispositivo."; +"All data is kept private on your device." = "Todos los datos son privados y están en tu dispositivo."; /* No comment provided by engineer. */ "All group members will remain connected." = "Todos los miembros del grupo permanecerán conectados."; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 2de5997f07..6b973e75d0 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -431,7 +431,7 @@ "All data is erased when it is entered." = "Toutes les données sont effacées lorsqu'il est saisi."; /* No comment provided by engineer. */ -"All data is private to your device." = "Toutes les données restent confinées dans votre appareil."; +"All data is kept private on your device." = "Toutes les données restent confinées dans votre appareil."; /* No comment provided by engineer. */ "All group members will remain connected." = "Tous les membres du groupe resteront connectés."; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 594bd3a123..2ba51d1e13 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -479,7 +479,7 @@ "All data is erased when it is entered." = "A jelkód megadása után az összes adat törlésre kerül."; /* No comment provided by engineer. */ -"All data is private to your device." = "Az összes adat biztonságban van az eszközén."; +"All data is kept private on your device." = "Az összes adat biztonságban van az eszközén."; /* No comment provided by engineer. */ "All group members will remain connected." = "Az összes csoporttag kapcsolatban marad."; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 31ee4e9e18..7c3a7e05de 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -473,7 +473,7 @@ "All data is erased when it is entered." = "Tutti i dati vengono cancellati quando inserito."; /* No comment provided by engineer. */ -"All data is private to your device." = "Tutti i dati sono privati, nel tuo dispositivo."; +"All data is kept private on your device." = "Tutti i dati sono privati, nel tuo dispositivo."; /* No comment provided by engineer. */ "All group members will remain connected." = "Tutti i membri del gruppo resteranno connessi."; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index e5c3520898..7004d0d124 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -479,7 +479,7 @@ "All data is erased when it is entered." = "Alle gegevens worden bij het invoeren gewist."; /* No comment provided by engineer. */ -"All data is private to your device." = "Alle gegevens zijn privé op uw apparaat."; +"All data is kept private on your device." = "Alle gegevens zijn privé op uw apparaat."; /* No comment provided by engineer. */ "All group members will remain connected." = "Alle groepsleden blijven verbonden."; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index e48e9f2ed8..cc3bd228f9 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -431,7 +431,7 @@ "All data is erased when it is entered." = "Wszystkie dane są usuwane po jego wprowadzeniu."; /* No comment provided by engineer. */ -"All data is private to your device." = "Wszystkie dane są prywatne na Twoim urządzeniu."; +"All data is kept private on your device." = "Wszystkie dane są prywatne na Twoim urządzeniu."; /* No comment provided by engineer. */ "All group members will remain connected." = "Wszyscy członkowie grupy pozostaną połączeni."; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index f22981f80a..dcd3de19d1 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -479,7 +479,7 @@ "All data is erased when it is entered." = "Все данные удаляются при его вводе."; /* No comment provided by engineer. */ -"All data is private to your device." = "Все данные хранятся только на вашем устройстве."; +"All data is kept private on your device." = "Все данные хранятся только на вашем устройстве."; /* No comment provided by engineer. */ "All group members will remain connected." = "Все члены группы, которые соединились через эту ссылку, останутся в группе."; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 3670e57955..b3eb5d426a 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -431,7 +431,7 @@ "All data is erased when it is entered." = "Kullanıldığında bütün veriler silinir."; /* No comment provided by engineer. */ -"All data is private to your device." = "Tüm veriler cihazınıza özeldir."; +"All data is kept private on your device." = "Tüm veriler cihazınıza özeldir."; /* No comment provided by engineer. */ "All group members will remain connected." = "Tüm grup üyeleri bağlı kalacaktır."; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 4e2b1680fd..ce8184272d 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -464,7 +464,7 @@ "All data is erased when it is entered." = "Всі дані стираються при введенні."; /* No comment provided by engineer. */ -"All data is private to your device." = "Всі дані є приватними для вашого пристрою."; +"All data is kept private on your device." = "Всі дані є приватними для вашого пристрою."; /* No comment provided by engineer. */ "All group members will remain connected." = "Всі учасники групи залишаться на зв'язку."; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index c40833b67b..62ff2088c2 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -413,7 +413,7 @@ "All data is erased when it is entered." = "所有数据在输入后将被删除。"; /* No comment provided by engineer. */ -"All data is private to your device." = "所有数据都是您设备的私有数据."; +"All data is kept private on your device." = "所有数据都是您设备的私有数据."; /* No comment provided by engineer. */ "All group members will remain connected." = "所有群组成员将保持连接。"; diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt index aca8efcb6f..9255584deb 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt @@ -1,10 +1,11 @@ package chat.simplex.common.platform import android.util.Log +import chat.simplex.common.model.ChatController.appPrefs actual object Log { - actual fun d(tag: String, text: String) = Log.d(tag, text).run{} - actual fun e(tag: String, text: String) = Log.e(tag, text).run{} - actual fun i(tag: String, text: String) = Log.i(tag, text).run{} - actual fun w(tag: String, text: String) = Log.w(tag, text).run{} + actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) Log.d(tag, text) } + actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) Log.e(tag, text) } + actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) Log.i(tag, text) } + actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) Log.w(tag, text) } } 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 6d13ff191f..e95fdb446f 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 @@ -132,6 +132,7 @@ class AppPreferences { val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null) val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false) val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false) + val logLevel = mkEnumPreference(SHARED_PREFS_LOG_LEVEL, LogLevel.WARNING) { LogLevel.entries.firstOrNull { it.name == this } } val showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false) val showSlowApiCalls = mkBoolPreference(SHARED_PREFS_SHOW_SLOW_API_CALLS, false) val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false) @@ -393,6 +394,7 @@ class AppPreferences { private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart" private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped" private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools" + private const val SHARED_PREFS_LOG_LEVEL = "LogLevel" private const val SHARED_PREFS_SHOW_INTERNAL_ERRORS = "ShowInternalErrors" private const val SHARED_PREFS_SHOW_SLOW_API_CALLS = "ShowSlowApiCalls" private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt index a1b39527d1..1c393d19ed 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt @@ -2,6 +2,10 @@ package chat.simplex.common.platform const val TAG = "SIMPLEX" +enum class LogLevel { + DEBUG, INFO, WARNING, ERROR +} + expect object Log { fun d(tag: String, text: String) fun e(tag: String, text: String) 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 df13368900..ed661245a3 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 @@ -131,26 +131,14 @@ fun ChatInfoView( }, syncContactConnection = { withBGApi { - val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) - connStats.value = cStats - if (cStats != null) { - withChats { - updateContactConnectionStats(chatRh, contact, cStats) - } - } + syncContactConnection(chatRh, contact, connStats, force = false) close.invoke() } }, syncContactConnectionForce = { showSyncConnectionForceAlert(syncConnectionForce = { withBGApi { - val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true) - connStats.value = cStats - if (cStats != null) { - withChats { - updateContactConnectionStats(chatRh, contact, cStats) - } - } + syncContactConnection(chatRh, contact, connStats, force = true) close.invoke() } }) @@ -189,6 +177,16 @@ fun ChatInfoView( } } +suspend fun syncContactConnection(rhId: Long?, contact: Contact, connectionStats: MutableState, force: Boolean) { + val cStats = chatModel.controller.apiSyncContactRatchet(rhId, contact.contactId, force = force) + connectionStats.value = cStats + if (cStats != null) { + withChats { + updateContactConnectionStats(rhId, contact, cStats) + } + } +} + sealed class SendReceipts { object Yes: SendReceipts() object No: SendReceipts() @@ -505,7 +503,7 @@ fun ChatInfoLayout( currentUser: User, sendReceipts: State, setSendReceipts: (SendReceipts) -> Unit, - connStats: State, + connStats: MutableState, contactNetworkStatus: NetworkStatus, customUserProfile: Profile?, localAlias: String, @@ -553,8 +551,8 @@ fun ChatInfoLayout( verticalAlignment = Alignment.CenterVertically ) { SearchButton(modifier = Modifier.fillMaxWidth(0.25f), chat, contact, close, onSearchClicked) - AudioCallButton(modifier = Modifier.fillMaxWidth(0.33f), chat, contact) - VideoButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact) + AudioCallButton(modifier = Modifier.fillMaxWidth(0.33f), chat, contact, connStats) + VideoButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact, connStats) MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, contact) } } @@ -825,12 +823,14 @@ fun MuteButton( fun AudioCallButton( modifier: Modifier, chat: Chat, - contact: Contact + contact: Contact, + connectionStats: MutableState ) { CallButton( modifier = modifier, chat, contact, + connectionStats, icon = painterResource(MR.images.ic_call), title = generalGetString(MR.strings.info_view_call_button), mediaType = CallMediaType.Audio @@ -841,12 +841,14 @@ fun AudioCallButton( fun VideoButton( modifier: Modifier, chat: Chat, - contact: Contact + contact: Contact, + connectionStats: MutableState ) { CallButton( modifier = modifier, chat, contact, + connectionStats, icon = painterResource(MR.images.ic_videocam), title = generalGetString(MR.strings.info_view_video_button), mediaType = CallMediaType.Video @@ -858,6 +860,7 @@ fun CallButton( modifier: Modifier, chat: Chat, contact: Contact, + connectionStats: MutableState, icon: Painter, title: String, mediaType: CallMediaType @@ -879,7 +882,23 @@ fun CallButton( disabledLook = !canCall, onClick = when { - canCall -> { { startChatCall(chat.remoteHostId, chat.chatInfo, mediaType) } } + canCall -> { { + val connStats = connectionStats.value + if (connStats != null) { + if (connStats.ratchetSyncState == RatchetSyncState.Ok) { + startChatCall(chat.remoteHostId, chat.chatInfo, mediaType) + } else if (connStats.ratchetSyncAllowed) { + showFixConnectionAlert(syncConnection = { + withBGApi { syncContactConnection(chat.remoteHostId, contact, connectionStats, force = false) } + }) + } else { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.cant_call_contact_alert_title), + generalGetString(MR.strings.encryption_renegotiation_in_progress) + ) + } + } + } } contact.nextSendGrpInv -> { { showCantCallContactSendMessageAlert() } } !contact.active -> { { showCantCallContactDeletedAlert() } } !contact.ready -> { { showCantCallContactConnectingAlert() } } @@ -1265,6 +1284,15 @@ fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) { ) } +fun showFixConnectionAlert(syncConnection: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.sync_connection_question), + text = generalGetString(MR.strings.sync_connection_desc), + confirmText = generalGetString(MR.strings.sync_connection_confirm), + onConfirm = syncConnection, + ) +} + 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) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index d6744a0a0d..e62235bd7c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -296,6 +296,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } } SectionBottomSpacer() + SectionBottomSpacer() } } @@ -309,6 +310,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools QuotedMsgView(qi) } SectionBottomSpacer() + SectionBottomSpacer() } } @@ -324,6 +326,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools ForwardedFromView(forwardedFromItem) } SectionBottomSpacer() + SectionBottomSpacer() } } @@ -395,6 +398,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } } SectionBottomSpacer() + SectionBottomSpacer() } } @@ -433,12 +437,11 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools Column { if (numTabs() > 1) { - Column( + Box( Modifier - .fillMaxHeight(), - verticalArrangement = Arrangement.SpaceBetween + .fillMaxHeight() ) { - Column(Modifier.weight(1f)) { + Column { when (val sel = selection.value) { is CIInfoTab.Delivery -> { DeliveryTab(sel.memberDeliveryStatuses) @@ -479,7 +482,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } } val oneHandUI = remember { appPrefs.oneHandUI.state } - Box(Modifier.offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) { + Box(Modifier.align(Alignment.BottomCenter).navigationBarsPadding().offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) { TabRow( selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class }, Modifier.height(AppBarHeight * fontSizeSqrtMultiplier), 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 913ea87c98..3c0f1f7769 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 @@ -665,13 +665,18 @@ fun ChatLayout( AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) { if (chatInfo != null) { Box(Modifier.fillMaxSize()) { - ChatItemsList( - remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, - useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadMessages, deleteMessage, deleteMessages, - receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, - updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, - setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy, - ) + // disables scrolling to top of chat item on click inside the bubble + CompositionLocalProvider(LocalBringIntoViewSpec provides object : BringIntoViewSpec { + override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f + }) { + ChatItemsList( + remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, + useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, + receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, + updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, + setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy, + ) + } } } Box( @@ -939,6 +944,7 @@ fun BoxScope.ChatItemsList( linkMode: SimplexLinkMode, selectedChatItems: MutableState?>, showMemberInfo: (GroupInfo, GroupMember) -> Unit, + showChatInfo: () -> Unit, loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, @@ -984,6 +990,7 @@ fun BoxScope.ChatItemsList( }) val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } } val loadingMoreItems = remember { mutableStateOf(false) } + val animatedScrollingInProgress = remember { mutableStateOf(false) } val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf() } if (!loadingMoreItems.value) { PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination -> @@ -1004,7 +1011,7 @@ fun BoxScope.ChatItemsList( val chatInfoUpdated = rememberUpdatedState(chatInfo) val highlightedItems = remember { mutableStateOf(setOf()) } val scope = rememberCoroutineScope() - val scrollToItem: (Long) -> Unit = remember { scrollToItem(searchValue, loadingMoreItems, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) } + val scrollToItem: (Long) -> Unit = remember { scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) } val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) } LoadLastItems(loadingMoreItems, remoteHostId, chatInfo) @@ -1065,7 +1072,7 @@ fun BoxScope.ChatItemsList( highlightedItems.value = setOf() } } - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } @@ -1314,7 +1321,7 @@ fun BoxScope.ChatItemsList( } } } - FloatingButtons(loadingMoreItems, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, markChatRead, listState) + FloatingButtons(loadingMoreItems, animatedScrollingInProgress, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, markChatRead, listState) FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent(true)).align(Alignment.TopCenter), mergedItems, listState) LaunchedEffect(Unit) { @@ -1323,6 +1330,15 @@ fun BoxScope.ChatItemsList( chatViewScrollState.value = it } } + LaunchedEffect(Unit) { + snapshotFlow { listState.value.isScrollInProgress } + .filter { !it } + .collect { + if (animatedScrollingInProgress.value) { + animatedScrollingInProgress.value = false + } + } + } } @Composable @@ -1400,6 +1416,7 @@ private fun NotifyChatListOnFinishingComposition( @Composable fun BoxScope.FloatingButtons( loadingMoreItems: MutableState, + animatedScrollingInProgress: MutableState, mergedItems: State, unreadCount: State, maxHeight: State, @@ -1439,8 +1456,14 @@ fun BoxScope.FloatingButtons( bottomUnreadCount, showBottomButtonWithCounter, showBottomButtonWithArrow, + animatedScrollingInProgress, composeViewHeight, - onClick = { scope.launch { tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) } } } + onClick = { + scope.launch { + animatedScrollingInProgress.value = true + tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) } + } + } ) // Don't show top FAB if is in search if (searchValue.value.isNotEmpty()) return @@ -1451,11 +1474,15 @@ fun BoxScope.FloatingButtons( TopEndFloatingButton( Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent(true)).align(Alignment.TopEnd), topUnreadCount, + animatedScrollingInProgress, onClick = { val index = mergedItems.value.items.indexOfLast { it.hasUnread() } if (index != -1) { // scroll to the top unread item - scope.launch { tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(index + 1, -maxHeight.value) } } + scope.launch { + animatedScrollingInProgress.value = true + tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(index + 1, -maxHeight.value) } + } } }, onLongClick = { showDropDown.value = true } @@ -1595,10 +1622,11 @@ fun MemberImage(member: GroupMember) { private fun TopEndFloatingButton( modifier: Modifier = Modifier, unreadCount: State, + animatedScrollingInProgress: State, onClick: () -> Unit, onLongClick: () -> Unit ) { - if (unreadCount.value > 0) { + if (remember { derivedStateOf { unreadCount.value > 0 && !animatedScrollingInProgress.value } }.value) { val interactionSource = interactionSourceWithDetection(onClick, onLongClick) FloatingActionButton( {}, // no action here @@ -1839,6 +1867,7 @@ private fun lastFullyVisibleIemInListState(topPaddingToContentPx: State, de private fun scrollToItem( searchValue: State, loadingMoreItems: MutableState, + animatedScrollingInProgress: MutableState, highlightedItems: MutableState>, chatInfo: State, maxHeight: State, @@ -1876,6 +1905,7 @@ private fun scrollToItem( highlightedItems.value = setOf(itemId) } else { withContext(scope.coroutineContext) { + animatedScrollingInProgress.value = true listState.value.animateScrollToItem(min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value) highlightedItems.value = setOf(itemId) } @@ -1937,10 +1967,11 @@ private fun BoxScope.BottomEndFloatingButton( unreadCount: State, showButtonWithCounter: State, showButtonWithArrow: State, + animatedScrollingInProgress: State, composeViewHeight: State, onClick: () -> Unit ) = when { - showButtonWithCounter.value -> { + showButtonWithCounter.value && !animatedScrollingInProgress.value -> { FloatingActionButton( onClick = onClick, elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), @@ -1954,7 +1985,7 @@ private fun BoxScope.BottomEndFloatingButton( ) } } - showButtonWithArrow.value -> { + showButtonWithArrow.value && !animatedScrollingInProgress.value -> { FloatingActionButton( onClick = onClick, elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp), 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 7f0d5f088e..c9ac464438 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 @@ -8,8 +8,6 @@ import SectionSpacer import SectionTextFooter import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview -import java.net.URI -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent @@ -58,6 +56,19 @@ fun GroupMemberInfoView( val developerTools = chatModel.controller.appPrefs.developerTools.get() var progressIndicator by remember { mutableStateOf(false) } + fun syncMemberConnection() { + withBGApi { + val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false) + if (r != null) { + connStats.value = r.second + withChats { + updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } + close.invoke() + } + } + } + if (chat != null) { val newRole = remember { mutableStateOf(member.memberRole) } GroupMemberInfoLayout( @@ -78,19 +89,30 @@ fun GroupMemberInfoView( } }, createMemberContact = { - withBGApi { - progressIndicator = true - val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId) - if (memberContact != null) { - val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf()) - withChats { - addChat(memberChat) - openLoadedChat(memberChat) + if (connectionStats != null) { + if (connectionStats.ratchetSyncState == RatchetSyncState.Ok) { + withBGApi { + progressIndicator = true + val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId) + if (memberContact != null) { + val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf()) + withChats { + addChat(memberChat) + openLoadedChat(memberChat) + } + closeAll() + chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected()) + } + progressIndicator = false } - closeAll() - chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected()) + } else if (connectionStats.ratchetSyncAllowed) { + showFixConnectionAlert(syncConnection = { syncMemberConnection() }) + } else { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.cant_send_message_to_member_alert_title), + generalGetString(MR.strings.encryption_renegotiation_in_progress) + ) } - progressIndicator = false } }, connectViaAddress = { connReqUri -> @@ -149,16 +171,7 @@ fun GroupMemberInfoView( }) }, syncMemberConnection = { - withBGApi { - val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false) - if (r != null) { - connStats.value = r.second - withChats { - updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) - } - close.invoke() - } - } + syncMemberConnection() }, syncMemberConnectionForce = { showSyncConnectionForceAlert(syncConnectionForce = { @@ -335,9 +348,20 @@ fun GroupMemberInfoLayout( val knownChat = if (contactId != null) knownDirectChat(contactId) else null if (knownChat != null) { val (chat, contact) = knownChat + val knownContactConnectionStats: MutableState = remember { mutableStateOf(null) } + + LaunchedEffect(contact.contactId) { + withBGApi { + val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, chat.chatInfo.apiId) + if (contactInfo != null) { + knownContactConnectionStats.value = contactInfo.first + } + } + } + OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contact.contactId) }) - AudioCallButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact) - VideoButton(modifier = Modifier.fillMaxWidth(1f), chat, contact) + AudioCallButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact, knownContactConnectionStats) + VideoButton(modifier = Modifier.fillMaxWidth(1f), chat, contact, knownContactConnectionStats) } else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) { if (contactId != null) { OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contactId) }) // legacy - only relevant for direct contacts created when joining group diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index bf871ab626..22842eb350 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -24,12 +24,12 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.currentUser import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR -import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlin.math.* @@ -51,6 +51,12 @@ fun chatEventText(eventText: String, ts: String): AnnotatedString = withStyle(chatEventStyle) { append("$eventText $ts") } } +data class ChatItemReactionMenuItem ( + val name: String, + val image: String?, + val onClick: (() -> Unit)? +) + @Composable fun ChatItemView( rhId: Long?, @@ -87,6 +93,7 @@ fun ChatItemView( showItemDetails: (ChatInfo, ChatItem) -> Unit, reveal: (Boolean) -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit, + showChatInfo: () -> Unit, developerTools: Boolean, showViaProxy: Boolean, showTimestamp: Boolean, @@ -120,7 +127,7 @@ fun ChatItemView( Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.chatItemOffset(cItem, itemSeparation.largeGap, inverted = true, revealed = true)) { cItem.reactions.forEach { r -> val showReactionMenu = remember { mutableStateOf(false) } - val reactionMembers = remember { mutableStateOf(emptyList()) } + val reactionMenuItems = remember { mutableStateOf(emptyList()) } val interactionSource = remember { MutableInteractionSource() } val enterInteraction = remember { HoverInteraction.Enter() } KeyChangeEffect(highlighted.value) { @@ -134,18 +141,39 @@ fun ChatItemView( var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp)) if (cInfo.featureEnabled(ChatFeature.Reactions)) { fun showReactionsMenu() { - if (cInfo is ChatInfo.Group) { - withBGApi { - try { - val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction) - if (members != null) { - showReactionMenu.value = true - reactionMembers.value = members + when (cInfo) { + is ChatInfo.Group -> { + withBGApi { + try { + val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction) + if (members != null) { + showReactionMenu.value = true + reactionMenuItems.value = members.map { + val enabled = cInfo.groupInfo.membership.groupMemberId != it.groupMember.groupMemberId + val click = if (enabled) ({ showMemberInfo(cInfo.groupInfo, it.groupMember) }) else null + ChatItemReactionMenuItem(it.groupMember.displayName, it.groupMember.image, click) + } + } + } catch (e: Exception) { + Log.d(TAG, "chatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}") } - } catch (e: Exception) { - Log.d(TAG, "hatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}") } } + is ChatInfo.Direct -> { + showReactionMenu.value = true + val reactions = mutableListOf() + + if (!r.userReacted || r.totalReacted > 1) { + val contact = cInfo.contact + reactions.add(ChatItemReactionMenuItem(contact.displayName, contact.image, showChatInfo)) + } + + if (r.userReacted) { + reactions.add(ChatItemReactionMenuItem(generalGetString(MR.strings.sender_you_pronoun), currentUser.value?.image, null)) + } + reactionMenuItems.value = reactions + } + else -> {} } } modifier = modifier @@ -166,19 +194,19 @@ fun ChatItemView( Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) { ReactionIcon(r.reaction.text, fontSize = 12.sp) DefaultDropdownMenu(showMenu = showReactionMenu) { - reactionMembers.value.forEach { m -> + reactionMenuItems.value.forEach { m -> ItemAction( - text = m.groupMember.displayName, - composable = { ProfileImage(44.dp, m.groupMember.image) }, + text = m.name, + composable = { ProfileImage(44.dp, m.image) }, onClick = { - if (cInfo is ChatInfo.Group && cInfo.groupInfo.membership.groupMemberId != m.groupMember.groupMemberId) { - showMemberInfo(cInfo.groupInfo, m.groupMember) - showReactionMenu.value = false - } else { + val click = m.onClick + if (click != null) { + click() showReactionMenu.value = false } }, - lineLimit = 1 + lineLimit = 1, + color = if (m.onClick == null) MaterialTheme.colors.secondary else MenuTextColor ) } } @@ -1188,6 +1216,7 @@ fun PreviewChatItemView( showItemDetails = { _, _ -> }, reveal = {}, showMemberInfo = { _, _ ->}, + showChatInfo = {}, developerTools = false, showViaProxy = false, showTimestamp = true, @@ -1233,6 +1262,7 @@ fun PreviewChatItemViewDeletedContent() { showItemDetails = { _, _ -> }, reveal = {}, showMemberInfo = { _, _ ->}, + showChatInfo = {}, developerTools = false, showViaProxy = false, preview = true, 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 ab908e4c5f..bf59524a06 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 @@ -30,6 +30,7 @@ import kotlinx.datetime.* import java.io.* import java.net.URI import java.nio.file.Files +import java.nio.file.StandardCopyOption import java.text.SimpleDateFormat import java.util.* import kotlin.collections.ArrayList @@ -44,11 +45,14 @@ fun DatabaseView() { val chatArchiveFile = remember { mutableStateOf(null) } val stopped = remember { m.chatRunning }.value == false val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? -> - val file = chatArchiveFile.value - if (file != null && to != null) { - copyFileToFile(File(file), to) { - chatArchiveFile.value = null - } + val archive = chatArchiveFile.value + if (archive != null && to != null) { + copyFileToFile(File(archive), to) {} + } + // delete no matter the database was exported or canceled the export process + if (archive != null) { + File(archive).delete() + chatArchiveFile.value = null } } val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) } @@ -680,6 +684,8 @@ suspend fun importArchive( } finally { File(archivePath).delete() } + } else { + progressIndicator.value = false } return false } @@ -691,14 +697,15 @@ private fun saveArchiveFromURI(importedArchiveURI: URI): String? { if (inputStream != null && archiveName != null) { val archivePath = "$databaseExportDir${File.separator}$archiveName" val destFile = File(archivePath) - Files.copy(inputStream, destFile.toPath()) + Files.copy(inputStream, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING) archivePath } else { Log.e(TAG, "saveArchiveFromURI null inputStream") null } } catch (e: Exception) { - Log.e(TAG, "saveArchiveFromURI error: ${e.message}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_database), e.stackTraceToString()) + Log.e(TAG, "saveArchiveFromURI error: ${e.stackTraceToString()}") null } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index 87770e9ffd..c5a4ae5f70 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -44,6 +45,12 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) -> if (devTools.value) { SectionDividerSpaced(maxTopPadding = true) SectionView(stringResource(MR.strings.developer_options_section).uppercase()) { + SettingsActionItemWithContent(painterResource(MR.images.ic_breaking_news), stringResource(MR.strings.debug_logs)) { + DefaultSwitch( + checked = remember { appPrefs.logLevel.state }.value <= LogLevel.DEBUG, + onCheckedChange = { appPrefs.logLevel.set(if (it) LogLevel.DEBUG else LogLevel.WARNING) } + ) + } SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades) if (appPlatform.isDesktop) { TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt index dcb1bc9de1..cc72387875 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/OperatorView.kt @@ -735,7 +735,10 @@ private fun ConditionsAppliedToOtherOperatorsText(userServers: ListRenegotiate encryption? The encryption is working and the new encryption agreement is not required. It may result in connection errors! Renegotiate + Fix connection? + Connection requires encryption renegotiation. + Fix + Encryption renegotiation in progress. View security code Verify security code @@ -903,6 +907,7 @@ Show: Hide: Show developer options + Enable logs Database IDs and Transport isolation option. Developer options Show internal errors @@ -1334,6 +1339,7 @@ You may migrate the exported database. Some file(s) were not exported Continue + Error saving database Save passphrase in Keystore @@ -2402,7 +2408,7 @@ Messages sent Messages received Details - Starting from %s.\nAll data is private to your device. + Starting from %s.\nAll data is kept private on your device.. Message reception Active connections Pending diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_breaking_news.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_breaking_news.svg new file mode 100644 index 0000000000..4688932ef9 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_breaking_news.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt index 3bd1506b4f..ee415ae82b 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt @@ -67,7 +67,7 @@ object NtfManager { ntf.second.close() } catch (e: Exception) { // Can be java.lang.UnsupportedOperationException, for example. May do nothing - println("Failed to close notification: ${e.stackTraceToString()}") + Log.e(TAG, "Failed to close notification: ${e.stackTraceToString()}") }*/ } } @@ -85,7 +85,8 @@ object NtfManager { } fun cancelAllNotifications() { -// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { println("Failed to close notification: ${e.stackTraceToString()}") } } +// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { Log.e(TAG, "Failed to close notification: ${e + // .stackTraceToString()}") } } withBGApi { prevNtfsMutex.withLock { prevNtfs.clear() @@ -153,7 +154,7 @@ object NtfManager { ImageIO.write(icon.toAwtImage(), "PNG", newFile.outputStream()) newFile.absolutePath } catch (e: Exception) { - println("Failed to write an icon to tmpDir: ${e.stackTraceToString()}") + Log.e(TAG, "Failed to write an icon to tmpDir: ${e.stackTraceToString()}") null } } else null diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt index 395754c51b..2b75a41cd3 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt @@ -1,8 +1,10 @@ package chat.simplex.common.platform +import chat.simplex.common.model.ChatController.appPrefs + actual object Log { - actual fun d(tag: String, text: String) = println("D: $text") - actual fun e(tag: String, text: String) = println("E: $text") - actual fun i(tag: String, text: String) = println("I: $text") - actual fun w(tag: String, text: String) = println("W: $text") + actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) println("D: $text") } + actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) println("E: $text") } + actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) println("I: $text") } + actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) println("W: $text") } } diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index c392be6b0e..cde173f24c 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,11 +24,11 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.2 -android.version_code=259 +android.version_name=6.2.1 +android.version_code=261 -desktop.version_name=6.2 -desktop.version_code=82 +desktop.version_name=6.2.1 +desktop.version_code=83 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 diff --git a/blog/20220404-simplex-chat-instant-notifications.md b/blog/20220404-simplex-chat-instant-notifications.md index ce7dfd613c..7d88a47fa7 100644 --- a/blog/20220404-simplex-chat-instant-notifications.md +++ b/blog/20220404-simplex-chat-instant-notifications.md @@ -68,7 +68,7 @@ So, for Android we can now deliver instant message notifications without comprom Please let us know what needs to be improved - it's only the first version of instant notifications for Android! -## Our iOS approach has one trade-off +## iOS notifications require a server iOS is much more protective of what apps are allowed to run on the devices, and the solution that worked on Android is not viable on iOS. diff --git a/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md b/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md index 55de82df47..502a42c559 100644 --- a/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md +++ b/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.md @@ -1,23 +1,88 @@ --- layout: layouts/article.html -title: "Servers operated by Flux - true privacy and decentralization for all users" +title: "SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps" date: 2024-12-10 -# previewBody: blog_previews/20241210.html -# image: images/simplexonflux.png -# imageWide: true -draft: true +previewBody: blog_previews/20241210.html +image: images/20241210-operators-1.png permalink: "/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html" --- # SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps -**Will be published:** Dec 10, 2024 +**Published:** Dec 10, 2024 -This is a placeholder page for the upcoming v6.2 release announcement! +What's new in v6.2: -- Preset servers are now operated by two companies - SimpleX Chat and Flux. Read [this post](./20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md). -- Business chats to provide support from your business to users of SimpleX network. Read [this page](../docs/BUSINESS.md). -- and more! +- [SimpleX Chat and Flux](#simplex-chat-and-flux-improve-metadata-privacy-in-simplex-network) improve metadata privacy in SimpleX network. +- [Business chats](#business-chats) to provide support from your business to users of SimpleX network. +- [Better user experience](#better-user-experience): open on the first unread, jump to quoted messages, see who reacted. +- [Improving notifications in iOS app](#improving-notifications-in-ios-app). + +## What's new in v6.2 + +### SimpleX Chat and Flux improve metadata privacy in SimpleX network + + + +SimpleX Chat and [Flux](https://runonflux.com) (Influx Technology Limited) made an agreement to include messaging and file servers operated by Flux into the app. + +SimpleX network is decentralized by design, but in the users of the previous app versions had to find other servers online or host servers themselves to use any other servers than operated by us. + +Now all users can choose between servers of two companies, use both of them, and continue using any other servers they host or available online. + +To use Flux servers enable them when the app offers it, or at any point later via Network & servers settings in the app. + +When both SimpleX Chat and Flux servers are enabled, the app will use servers of both operators in each connection to receive messages and for [private message routing](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md), increasing metadata privacy for all users. + +Read more about why SimpleX network benefits from multiple operators in [our previous post](./20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md). + +You can also read about our plan [how network operators will make money](https://github.com/simplex-chat/simplex-chat/blob/stable/docs/rfcs/2024-04-26-commercial-model.md), while continuing to protect users privacy, based on network design rather than on trust to operators, and without any cryptocurrency emission. + +### Business chats + + + +We use SimpleX Chat to provide support to SimpleX Chat users, and we also see some other companies offering SimpleX Chat as a support channel. + +One of the problem of providing support via general purpose messengers is that the customers don't see who they talk to, as they can in all dedicated support systems. + +It is not possible in most messengers, including SimpleX Chat prior to v6.2 - every new customer joins a one-to-one conversation, where the customers see that they talk to a company, not knowing who they talk to, and if it's a bot or a human. + +The new business chats in SimpleX Chat solve this problem: to use them enable the toggle under the contact address in your chat profile. It is safe to do, and you can always toggle it off, if needed - the address itself does not change. + +Once you do it, the app will be creating a new business chat with each connecting customer where multiple people can participate. Business chat is a hybrid of one-to-one and group conversation. In the list of chats you will see customer names and avatars, and the customer will see your business name and avatar, like with one-to-one conversations. But inside it works as a group, allowing customer to see who sent the message, and allowing you to add other participants from the business side, for delegation and escalation of customer questions. + +This can be done manually, or you can automate these conversations using bots that can answer some customer questions and then add a human to the conversation when appropriate or requested by the customer. We will be offering more bot-related features to the app and a simpler way to program bots very soon - watch our announcements. + +### Better user experience + + + +**Chat navigation** + +This has been a long-standing complaint from the users: *why does the app opens conversations on the last message, and not on the first unread message*? + +Android and desktop apps now open the chat on the first unread message. It will soon be done in the iOS app too. + +Also, the app can scroll to the replied message anywhere in the conversation (when you tap it), even if it was sent a very long time ago. + +**See who reacted!** + +This is a small but important change - you can now see who reacted to your messages! + +### Improving notifications in iOS app + +iOS notifications in a decentralized network is a complex problems. We [support iOS notifications](./20220404-simplex-chat-instant-notifications.md#ios-notifications-require-a-server) from early versions of the app, focussing on preserving privacy as much as possible. But the reliability of notifications was not good enough. + +We solved several problems of notification delivery in this release: +- messaging servers no longer lose notifications while notification servers are restarted. +- Apple can drop notifications while your device is offline - about 15-20% of notifications are dropped because of it. The servers and the new version of the app work around this problem by delivering several last notifications, to show notifications correctly even when Apple drops them. + +With these changes the iOS notifications remained as private and secure as before. The notifications only contain metadata, without the actual messages, and even the metadata is end-to-end encrypted between SimpleX notification servers and the client device, inaccessible to Apple push notification servers. + +There are two remaining problems we will solve soon: +- iOS only allows to use 25mb of device memory when processing notifications in the background. This limit didn't change for many years, and it is challenging for decentralized design. If the app uses more memory, iOS kills it and the notification is not shown – approximately 10% of notifications can be lost because of that. +- for notifications to work, the app communicates with the notification server. If the user puts the app in background too quickly, the app may fail to enable notification for the new contacts. We plan to change clients and servers to delegate this task to messaging servers, to remove the need for this additional communication entirely, without any impact on privacy and security. This will happen early next year. ## SimpleX network diff --git a/blog/README.md b/blog/README.md index 97ccffda9a..1432d95de5 100644 --- a/blog/README.md +++ b/blog/README.md @@ -1,6 +1,15 @@ # Blog -Nov 25, 2025 [Servers operated by Flux - true privacy and decentralization for all users](./20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md) +Dec 10, 2024 [SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps](./20241210-simplex-network-v6-2-servers-by-flux-business-chats.md) + +- SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app to improve metadata privacy in SimpleX network. +- Business chats for better privacy and support of your customers. +- Better user experience: open on the first unread, jump to quoted messages, see who reacted. +- Improving notifications in iOS app. + +-- + +Nov 25, 2024 [Servers operated by Flux - true privacy and decentralization for all users](./20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md) - Welcome, Flux - the new servers in v6.2-beta.1! - What's the problem? diff --git a/blog/images/20241210-business.png b/blog/images/20241210-business.png new file mode 100644 index 0000000000..407ed66a94 Binary files /dev/null and b/blog/images/20241210-business.png differ diff --git a/blog/images/20241210-operators-1.png b/blog/images/20241210-operators-1.png new file mode 100644 index 0000000000..863bd00822 Binary files /dev/null and b/blog/images/20241210-operators-1.png differ diff --git a/blog/images/20241210-operators-2.png b/blog/images/20241210-operators-2.png new file mode 100644 index 0000000000..85e599c827 Binary files /dev/null and b/blog/images/20241210-operators-2.png differ diff --git a/blog/images/20241210-reactions.png b/blog/images/20241210-reactions.png new file mode 100644 index 0000000000..6de8ba8f07 Binary files /dev/null and b/blog/images/20241210-reactions.png differ diff --git a/scripts/desktop/make-appimage-linux.sh b/scripts/desktop/make-appimage-linux.sh index 5084a0276d..6cc7aac011 100755 --- a/scripts/desktop/make-appimage-linux.sh +++ b/scripts/desktop/make-appimage-linux.sh @@ -40,10 +40,10 @@ if [ ! -f ../appimagetool-x86_64.AppImage ]; then wget --secure-protocol=TLSv1_3 https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage chmod +x ../appimagetool-x86_64.AppImage fi -if [ ! -f ../runtime-fuse3-x86_64 ]; then - wget --secure-protocol=TLSv1_3 https://github.com/AppImage/type2-runtime/releases/download/old/runtime-fuse3-x86_64 -O ../runtime-fuse3-x86_64 - chmod +x ../runtime-fuse3-x86_64 +if [ ! -f ../runtime-x86_64 ]; then + wget --secure-protocol=TLSv1_3 https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64 -O ../runtime-x86_64 + chmod +x ../runtime-x86_64 fi -../appimagetool-x86_64.AppImage --runtime-file ../runtime-fuse3-x86_64 . +../appimagetool-x86_64.AppImage --runtime-file ../runtime-x86_64 . mv *imple*.AppImage ../../ diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index bc90e4e041..9570f4bbca 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,17 @@ + + https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html + +

New in v6.2:

+
    +
  • SimpleX Chat and Flux made an agreement to include servers operated by Flux into the app – to improve metadata privacy.
  • +
  • Business chats – your customers privacy.
  • +
  • Improved user experience in chats: open on the first unread, jump to quoted messages, see who reacted.
  • +
+
+
https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html diff --git a/website/src/_includes/blog_previews/20241125.html b/website/src/_includes/blog_previews/20241125.html index 2a73e9ec9b..8e71807c0b 100644 --- a/website/src/_includes/blog_previews/20241125.html +++ b/website/src/_includes/blog_previews/20241125.html @@ -1,5 +1,3 @@ -

-
  • Welcome, Flux — the new servers in v6.2-beta.1!
  • What's the problem?
  • diff --git a/website/src/_includes/blog_previews/20241210.html b/website/src/_includes/blog_previews/20241210.html new file mode 100644 index 0000000000..48962be17a --- /dev/null +++ b/website/src/_includes/blog_previews/20241210.html @@ -0,0 +1,8 @@ +

    v6.2 is released:

    + +
      +
    • SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app to improve metadata privacy in SimpleX network.
    • +
    • Business chats for better privacy and support of your customers.
    • +
    • Better user experience: open on the first unread, jump to quoted messages, see who reacted.
    • +
    • Improving notifications in iOS app.
    • +