Compare commits

...

15 Commits

Author SHA1 Message Date
Avently 0cc1fc2dbf rename 2024-12-12 19:01:18 +07:00
Avently 7ba960021f changes 2024-12-12 18:57:57 +07:00
Avently 46d7b8ddd9 unused 2024-12-12 18:05:49 +07:00
Avently eb4290ef72 enhancements 2024-12-12 17:49:01 +07:00
Avently 507b0c5f2a Merge branch 'master' into av/multiplatform-saving-prefs 2024-12-12 15:12:33 +07:00
spaced4ndy 708368c9c5 android, desktop: fix operators text (#5371) 2024-12-12 07:26:17 +00:00
Evgeny Poberezkin 5d8e11c3a2 blog: typo in screenshot 2024-12-10 18:39:14 +00:00
Evgeny Poberezkin df8ce2b06d blog: fix image 2024-12-10 18:13:11 +00:00
Evgeny 6ff5f31bee blog: v6.2 announcement (#5359)
* blog: v6.2 announcement

* update, images

* update

* readme

* correction

* update
2024-12-10 17:16:34 +00:00
Stanislav Dmitrenko 5e29cda27b desktop: start using continuous runtime for AppImage again (#5366) 2024-12-10 11:09:01 +00:00
Stanislav Dmitrenko 2e573662d5 android, desktop: hiding counters while scrolling (#5363)
* android, desktop: hiding counters while scrolling

* change
2024-12-10 10:07:49 +00:00
spaced4ndy 4075c26dd2 ui: offer to fix connection on call and message buttons (#5358) 2024-12-09 21:03:56 +04:00
Evgeny Poberezkin d64351b760 ui: update label in server statistics 2024-12-08 15:17:32 +00:00
sh b06211bd4e flatpak: update metainfo (#5353)
* flatpak: update metainfo

* make notes like in release

* simpler

* space

---------

Co-authored-by: Evgeny <evgeny@poberezkin.com>
2024-12-08 10:39:31 +00:00
Avently 7122721af7 desktop: saving settings in a safer way to handle process death 2024-08-14 21:50:09 +09:00
57 changed files with 513 additions and 205 deletions
+4 -8
View File
@@ -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.
+78 -29
View File
@@ -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 {
@@ -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"
))
}
}
}
@@ -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.")
}
}
}
@@ -731,8 +731,8 @@
<target>Всички данни се изтриват при въвеждане.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
@@ -712,8 +712,8 @@
<target>Všechna data se při zadání vymažou.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
@@ -760,8 +760,8 @@
<target>Alle Daten werden gelöscht, sobald dieser eingegeben wird.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Alle Daten werden nur auf Ihrem Gerät gespeichert.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -760,9 +760,9 @@
<target>All data is erased when it is entered.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<target>All data is private to your device.</target>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>All data is kept private on your device.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
@@ -760,8 +760,8 @@
<target>Al introducirlo todos los datos son eliminados.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Todos los datos son privados y están en tu dispositivo.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -707,8 +707,8 @@
<target>Kaikki tiedot poistetaan, kun se syötetään.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
@@ -745,8 +745,8 @@
<target>Toutes les données sont effacées lorsqu'il est saisi.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Toutes les données restent confinées dans votre appareil.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -760,8 +760,8 @@
<target>A jelkód megadása után az összes adat törlésre kerül.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Az összes adat biztonságban van az eszközén.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -759,8 +759,8 @@
<target>Tutti i dati vengono cancellati quando inserito.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Tutti i dati sono privati, nel tuo dispositivo.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -724,8 +724,8 @@
<target>入力するとすべてのデータが消去されます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
@@ -760,8 +760,8 @@
<target>Alle gegevens worden bij het invoeren gewist.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Alle gegevens zijn privé op uw apparaat.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -745,8 +745,8 @@
<target>Wszystkie dane są usuwane po jego wprowadzeniu.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Wszystkie dane są prywatne na Twoim urządzeniu.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -5425,8 +5425,8 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi
<source>Advanced settings</source>
<target state="translated">Configurações avançadas</target>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve" approved="no">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve" approved="no">
<source>All data is kept private on your device.</source>
<target state="translated">Toda informação é privada em seu dispositivo.</target>
</trans-unit>
<trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve" approved="no">
@@ -760,8 +760,8 @@
<target>Все данные удаляются при его вводе.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Все данные хранятся только на вашем устройстве.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -699,8 +699,8 @@
<target>ข้อมูลทั้งหมดจะถูกลบเมื่อถูกป้อน</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
@@ -745,8 +745,8 @@
<target>Kullanıldığında bütün veriler silinir.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Tüm veriler cihazınıza özeldir.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -756,8 +756,8 @@
<target>Всі дані стираються при введенні.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Всі дані є приватними для вашого пристрою.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -739,8 +739,8 @@
<target>所有数据在输入后将被删除。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>所有数据都是您设备的私有数据.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
+1 -1
View File
@@ -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.";
+1 -1
View File
@@ -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.";
+1 -1
View File
@@ -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.";
+1 -1
View File
@@ -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.";
+1 -1
View File
@@ -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.";
+1 -1
View File
@@ -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.";
+1 -1
View File
@@ -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.";
+1 -1
View File
@@ -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." = "Все члены группы, которые соединились через эту ссылку, останутся в группе.";
+1 -1
View File
@@ -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.";
+1 -1
View File
@@ -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." = "Всі учасники групи залишаться на зв'язку.";
+1 -1
View File
@@ -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." = "所有群组成员将保持连接。";
@@ -19,6 +19,8 @@ actual val wallpapersDir: File = File(filesDir.absolutePath + File.separator + "
actual val coreTmpDir: File = File(filesDir.absolutePath + File.separator + "temp_files")
actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "files"
actual val preferencesDir = File(dataDir.absolutePath + File.separator + "shared_prefs")
actual val preferencesTmpDir = File(tmpDir, "prefs_tmp")
.also { it.deleteRecursively() }
actual val chatDatabaseFileName: String = "files_chat.db"
actual val agentDatabaseFileName: String = "files_agent.db"
@@ -3115,8 +3115,12 @@ class SharedPreference<T>(val get: () -> T, set: (T) -> Unit) {
init {
this.set = { value ->
set(value)
_state.value = value
try {
set(value)
_state.value = value
} catch (e: Exception) {
Log.e(TAG, "Error saving settings: ${e.stackTraceToString()}")
}
}
}
}
@@ -3,7 +3,7 @@ package chat.simplex.common.platform
import androidx.compose.runtime.Composable
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import com.charleskorn.kaml.*
import kotlinx.serialization.encodeToString
@@ -11,6 +11,8 @@ import java.io.*
import java.net.URI
import java.net.URLDecoder
import java.net.URLEncoder
import java.nio.file.Files
import java.nio.file.StandardCopyOption
expect val dataDir: File
expect val tmpDir: File
@@ -20,6 +22,7 @@ expect val wallpapersDir: File
expect val coreTmpDir: File
expect val dbAbsolutePrefixPath: String
expect val preferencesDir: File
expect val preferencesTmpDir: File
expect val chatDatabaseFileName: String
expect val agentDatabaseFileName: String
@@ -142,16 +145,23 @@ fun readThemeOverrides(): List<ThemeOverrides> {
}
}
private const val lock = "themesWriter"
fun writeThemeOverrides(overrides: List<ThemeOverrides>): Boolean =
try {
File(getPreferenceFilePath("themes.yaml")).outputStream().use {
val string = yaml.encodeToString(ThemesFile(themes = overrides))
it.bufferedWriter().use { it.write(string) }
synchronized(lock) {
try {
val themesFile = File(getPreferenceFilePath("themes.yaml"))
createTmpFileAndDelete(preferencesTmpDir) { tmpFile ->
val string = yaml.encodeToString(ThemesFile(themes = overrides))
tmpFile.bufferedWriter().use { it.write(string) }
themesFile.parentFile.mkdirs()
Files.move(tmpFile.toPath(), themesFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
true
} catch (e: Exception) {
Log.e(TAG, "Error writing themes file: ${e.stackTraceToString()}")
false
}
true
} catch (e: Throwable) {
Log.e(TAG, "Error while writing themes file: ${e.stackTraceToString()}")
false
}
private fun fileReady(file: CIFile, filePath: String) =
@@ -102,7 +102,9 @@ object ThemeManager {
}
fun applyTheme(theme: String) {
appPrefs.currentTheme.set(theme)
if (appPrefs.currentTheme.get() != theme) {
appPrefs.currentTheme.set(theme)
}
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
platform.androidSetNightModeIfSupported()
val c = CurrentColors.value.colors
@@ -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<ConnectionStats?>, 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<SendReceipts>,
setSendReceipts: (SendReceipts) -> Unit,
connStats: State<ConnectionStats?>,
connStats: MutableState<ConnectionStats?>,
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<ConnectionStats?>
) {
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<ConnectionStats?>
) {
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<ConnectionStats?>,
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<RcvMsgInfo?, ServerQueueInfo>): String {
val (rcvMsgInfo, qInfo) = info
val msgInfo: String = if (rcvMsgInfo != null) json.encodeToString(rcvMsgInfo) else generalGetString(MR.strings.message_queue_info_none)
@@ -984,6 +984,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<Long>() }
if (!loadingMoreItems.value) {
PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination ->
@@ -1004,7 +1005,7 @@ fun BoxScope.ChatItemsList(
val chatInfoUpdated = rememberUpdatedState(chatInfo)
val highlightedItems = remember { mutableStateOf(setOf<Long>()) }
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)
@@ -1314,7 +1315,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 +1324,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 +1410,7 @@ private fun NotifyChatListOnFinishingComposition(
@Composable
fun BoxScope.FloatingButtons(
loadingMoreItems: MutableState<Boolean>,
animatedScrollingInProgress: MutableState<Boolean>,
mergedItems: State<MergedItems>,
unreadCount: State<Int>,
maxHeight: State<Int>,
@@ -1439,8 +1450,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 +1468,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 +1616,11 @@ fun MemberImage(member: GroupMember) {
private fun TopEndFloatingButton(
modifier: Modifier = Modifier,
unreadCount: State<Int>,
animatedScrollingInProgress: State<Boolean>,
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 +1861,7 @@ private fun lastFullyVisibleIemInListState(topPaddingToContentPx: State<Int>, de
private fun scrollToItem(
searchValue: State<String>,
loadingMoreItems: MutableState<Boolean>,
animatedScrollingInProgress: MutableState<Boolean>,
highlightedItems: MutableState<Set<Long>>,
chatInfo: State<ChatInfo>,
maxHeight: State<Int>,
@@ -1876,6 +1899,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 +1961,11 @@ private fun BoxScope.BottomEndFloatingButton(
unreadCount: State<Int>,
showButtonWithCounter: State<Boolean>,
showButtonWithArrow: State<Boolean>,
animatedScrollingInProgress: State<Boolean>,
composeViewHeight: State<Dp>,
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 +1979,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),
@@ -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<ConnectionStats?> = 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
@@ -316,8 +316,9 @@ fun removeWallpaperFile(fileName: String? = null) {
WallpaperType.cachedImages.remove(fileName)
}
fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T {
val tmpFile = File(tmpDir, UUID.randomUUID().toString())
fun <T> createTmpFileAndDelete(dir: File = tmpDir, onCreated: (File) -> T): T {
val tmpFile = File(dir, UUID.randomUUID().toString())
tmpFile.parentFile.mkdirs()
tmpFile.deleteOnExit()
ChatModel.filesToDelete.add(tmpFile)
try {
@@ -16,6 +16,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.setConditionsNotified
import chat.simplex.common.model.ServerOperator.Companion.dummyOperatorInfo
@@ -766,7 +768,9 @@ private val versionDescriptions: List<VersionDescription> = listOf(
private val lastVersion = versionDescriptions.last().version
fun setLastVersionDefault(m: ChatModel) {
m.controller.appPrefs.whatsNewVersion.set(lastVersion)
if (appPrefs.whatsNewVersion.get() != lastVersion) {
appPrefs.whatsNewVersion.set(lastVersion)
}
}
fun shouldShowWhatsNew(m: ChatModel): Boolean {
@@ -735,7 +735,10 @@ private fun ConditionsAppliedToOtherOperatorsText(userServers: List<UserOperator
}
if (otherOperatorsToApply.value.isNotEmpty()) {
ReadableText(MR.strings.operator_conditions_will_be_applied)
ReadableText(
MR.strings.operator_conditions_will_be_applied,
args = otherOperatorsToApply.value.joinToString(", ") { it.legalName_ }
)
}
}
@@ -524,6 +524,10 @@
<string name="sync_connection_force_question">Renegotiate encryption?</string>
<string name="sync_connection_force_desc">The encryption is working and the new encryption agreement is not required. It may result in connection errors!</string>
<string name="sync_connection_force_confirm">Renegotiate</string>
<string name="sync_connection_question">Fix connection?</string>
<string name="sync_connection_desc">Connection requires encryption renegotiation.</string>
<string name="sync_connection_confirm">Fix</string>
<string name="encryption_renegotiation_in_progress">Encryption renegotiation in progress.</string>
<string name="view_security_code">View security code</string>
<string name="verify_security_code">Verify security code</string>
@@ -909,6 +913,7 @@
<string name="show_slow_api_calls">Show slow API calls</string>
<string name="shutdown_alert_question">Shutdown?</string>
<string name="shutdown_alert_desc">Notifications will stop working until you re-launch the app</string>
<string name="prefs_error_saving_settings">Error saving settings</string>
<!-- Address Items - UserAddressView.kt -->
<string name="create_address">Create address</string>
@@ -2402,7 +2407,7 @@
<string name="servers_info_messages_sent">Messages sent</string>
<string name="servers_info_messages_received">Messages received</string>
<string name="servers_info_details">Details</string>
<string name="servers_info_private_data_disclaimer">Starting from %s.\nAll data is private to your device.</string>
<string name="servers_info_private_data_disclaimer">Starting from %s.\nAll data is kept private on your device..</string>
<string name="servers_info_subscriptions_section_header">Message reception</string>
<string name="servers_info_subscriptions_connections_subscribed">Active connections</string>
<string name="servers_info_subscriptions_connections_pending">Pending</string>
@@ -17,6 +17,8 @@ actual val wallpapersDir: File = File(dataDir.absolutePath + File.separator + "s
actual val coreTmpDir: File = File(dataDir.absolutePath + File.separator + "tmp")
actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "simplex_v1"
actual val preferencesDir = File(desktopPlatform.configPath).also { it.parentFile.mkdirs() }
actual val preferencesTmpDir = File(desktopPlatform.configPath, "tmp")
.also { it.deleteRecursively() }
actual val chatDatabaseFileName: String = "simplex_v1_chat.db"
actual val agentDatabaseFileName: String = "simplex_v1_agent.db"
@@ -9,15 +9,16 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.*
import chat.simplex.common.simplexWindowState
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import com.jthemedetecor.OsThemeDetector
import com.russhwolf.settings.*
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.desc.desc
import kotlinx.coroutines.*
import java.io.File
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.*
import java.util.concurrent.Executors
@Composable
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font =
@@ -37,10 +38,8 @@ catch (e: Exception) {
private val settingsFile =
File(desktopPlatform.configPath + File.separator + "settings.properties")
.also { it.parentFile.mkdirs() }
private val settingsThemesFile =
File(desktopPlatform.configPath + File.separator + "themes.properties")
.also { it.parentFile.mkdirs() }
private val settingsProps =
Properties()
@@ -61,11 +60,35 @@ private val settingsThemesProps =
Properties()
.also { props -> try { settingsThemesFile.reader().use { props.load(it) } } catch (e: Exception) { /**/ } }
private val settingsWriterThread = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
actual val settings: Settings = PropertiesSettings(settingsProps) { CoroutineScope(settingsWriterThread).launch { settingsFile.writer().use { settingsProps.store(it, "") } } }
actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { CoroutineScope(settingsWriterThread).launch { settingsThemesFile.writer().use { settingsThemesProps.store(it, "") } } }
private const val lock = "settingsSaver"
actual val settings: Settings = PropertiesSettings(settingsProps) {
synchronized(lock) {
try {
createTmpFileAndDelete(preferencesTmpDir) { tmpFile ->
tmpFile.writer().use { settingsProps.store(it, "") }
settingsFile.parentFile.mkdirs()
Files.move(tmpFile.toPath(), settingsFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.prefs_error_saving_settings), e.stackTraceToString())
throw e
}
}
}
actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) {
synchronized(lock) {
try {
createTmpFileAndDelete(preferencesTmpDir) { tmpFile ->
tmpFile.writer().use { settingsThemesProps.store(it, "") }
settingsThemesFile.parentFile.mkdirs()
Files.move(tmpFile.toPath(), settingsThemesFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.prefs_error_saving_settings), e.stackTraceToString())
throw e
}
}
}
actual fun windowOrientation(): WindowOrientation =
if (simplexWindowState.windowState.size.width > simplexWindowState.windowState.size.height) {
@@ -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.
@@ -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
<img src="./images/20241210-operators-1.png" width=288 class="float-to-right"> <img src="./images/20241210-operators-2.png" width=288 class="float-to-right">
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
<img src="./images/20241210-business.png" width=288 class="float-to-right">
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
<img src="./images/20241210-reactions.png" width=288 class="float-to-right">
**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
+10 -1
View File
@@ -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?
Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

+4 -4
View File
@@ -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 ../../
@@ -38,6 +38,17 @@
</description>
<releases>
<release version="6.2.0" date="2024-12-08">
<url type="details">https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html</url>
<description>
<p>New in v6.2:</p>
<ul>
<li>SimpleX Chat and Flux made an agreement to include servers operated by Flux into the app to improve metadata privacy.</li>
<li>Business chats your customers privacy.</li>
<li>Improved user experience in chats: open on the first unread, jump to quoted messages, see who reacted.</li>
</ul>
</description>
</release>
<release version="6.1.1" date="2024-10-18">
<url type="details">https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html</url>
<description>
@@ -1,5 +1,3 @@
<p></p>
<ul>
<li><strong>Welcome, Flux</strong> &mdash; the new servers in <strong>v6.2-beta.1!</strong></li>
<li>What's the problem?</li>
@@ -0,0 +1,8 @@
<p><strong>v6.2 is released:</strong></p>
<ul>
<li>SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app to improve metadata privacy in SimpleX network.</li>
<li>Business chats for better privacy and support of your customers.</li>
<li>Better user experience: open on the first unread, jump to quoted messages, see who reacted.</li>
<li>Improving notifications in iOS app.</li>
</ul>