Compare commits

...

41 Commits

Author SHA1 Message Date
shum 4e5ee981e5 Revert "ui: comment smp proxy ui (#4204)"
This reverts commit a0d6ae15ab.
2024-05-21 14:04:40 +00:00
Evgeny Poberezkin c59eb06b8a 5.8-beta.2: ios 218, android 210, desktop 47 2024-05-20 21:23:40 +01:00
Evgeny Poberezkin 423fc96638 core: 5.8.0.2 (simplexmq 5.8.0.2) 2024-05-20 18:38:46 +01:00
spaced4ndy ba203faad4 android: check known relays before file reception, support user approval of unknown relays (#4196) 2024-05-20 17:58:30 +04:00
spaced4ndy ec7b35adb9 ios: check known relays before file reception, support user approval of unknown relays (#4044)
* ios: check known relays before file reception, support user approval of unknown relays

* add privacyAskToApproveRelays to AppSettings

* less text

* ui improvements, fixes

* fix ntf

* shorter alert

* simplify settings

* rename

* icon

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-05-20 17:49:19 +04:00
spaced4ndy d2d450d1d7 core: check known relays before file reception, support user approval of unknown relays (#4043)
* core: check known relays before file reception, support user approval of unknown relays

* comment

* reset on not approved agent error

* add privacyAskToApproveRelays to AppSettings

* filter distinct servers

* update simplexmq

* remember user_approved_relays

* refactor

* rename

* update simplexmq

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-05-20 17:23:29 +04:00
Evgeny Poberezkin b4caefb17c 5.8-beta.1: ios 217, android 209, desktop 46 2024-05-18 08:20:38 +01:00
Evgeny Poberezkin fd791f3fb8 ios: update core library 2024-05-17 15:54:51 +01:00
Evgeny Poberezkin 0720292ebf core: 5.8.0.1 (simplexmq: 5.8.0.1, faster reconnection when switching networks) 2024-05-17 15:39:58 +01:00
spaced4ndy a0d6ae15ab ui: comment smp proxy ui (#4204) 2024-05-17 16:36:25 +04:00
Evgeny Poberezkin 3e76a2d49c blog: image 2024-05-16 19:47:24 +01:00
Evgeny Poberezkin ed3365bb6d website: fix links 2024-05-16 18:16:00 +01:00
Evgeny Poberezkin b2a8797ed4 blog: Redefining Privacy by Making Hard Choices (#4195)
* blog: Redefining Privacy by Making Hard Choices

* update

* update

* revert

* update

* corrections

Co-authored-by: Esra'a Al Shafei <alshafei@users.noreply.github.com>

* update

---------

Co-authored-by: Esra'a Al Shafei <alshafei@users.noreply.github.com>
2024-05-16 18:02:37 +01:00
spaced4ndy d3e522e169 ui: disable send field for disabled connection (based on authErrCounter) (#4193) 2024-05-16 17:08:31 +04:00
spaced4ndy 49e09ca449 core: contact disabled event (#4194) 2024-05-16 17:08:13 +04:00
spaced4ndy d1dde3d0c8 ui: support message forwarding with custom ttl (#4192) 2024-05-16 14:12:58 +04:00
spaced4ndy ef75eceb40 core: support message forwarding with custom ttl (#4191) 2024-05-16 14:11:55 +04:00
spaced4ndy 96ce59f330 multiplatform: SMP proxy configuration buttons (#4188)
* multiplatform: SMP proxy configuration buttons

* fix

* icons

* icon

* icon
2024-05-15 20:09:51 +01:00
spaced4ndy 98a67fa2e4 multiplatform: SMP proxy support (#4179)
* android: SMP proxy support

* android types

* show in meta

* reserve

* remove comment

* texts
2024-05-15 13:56:29 +01:00
spaced4ndy 4b13512950 ios: SMP proxy support (#4178)
* ios: SMP proxy support

* statuses

* group statuses

* error texts

* update

* change icon

* texts

* texts

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-05-15 13:09:42 +01:00
spaced4ndy 4c0d47bbd4 core: message statuses for sending proxies (#4161)
* core: delivery path

* update simplexmq

* via proxy snd flags

* error statuses

* rework errors

* proxy expired errors

* corrections

* move backwards compatibile parser to new type

* update simplexmq

* names

* refactor, style

* simplexmq

* refactor

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-05-15 12:30:05 +01:00
Evgeny Poberezkin 3129602a78 Merge branch 'stable' 2024-05-15 11:44:22 +01:00
Stanislav Dmitrenko a0c257962e android, desktop: fix file menu and icon (#4185)
* android, desktop: fix file menu and icon

* simplify

* space
2024-05-15 11:43:20 +01:00
Evgeny Poberezkin 93ae1145bc core: update simplexmq (use MVar for better DB connection concurrency) (#4183)
* core: update simplexmq (use MVar for better DB connection concurrency)

* focus failing tests

* add timeouts to test

* fix tests

* more delays

* increase timeouts

* prints

* delay

* delay

* empty

* more delays

* enable all

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2024-05-15 11:16:38 +01:00
Evgeny Poberezkin d02668386f directory: debug logging (#4104) 2024-05-14 21:32:53 +01:00
M. Sarmad Qadeer bd8c8b5a8c web: fix the links of faq & some other (#4144)
* web: fix the links of faq & some other

* web: fix links

* web: update .eleventy.js to fix links

* web: fix some links and improve the logic

* web: fix links & improve the logic

* remove semicolon

* update links

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-05-14 19:59:51 +01:00
Evgeny Poberezkin e359ec02f5 5.8-beta.0: ios 216, android 208, desktop 45 2024-05-13 23:34:14 +01:00
Evgeny Poberezkin a1948d549b ios: update core library 2024-05-13 22:24:10 +01:00
Evgeny Poberezkin 396b73b480 core: 5.8.0.0 2024-05-13 21:13:07 +01:00
Evgeny Poberezkin 331b4633fc core: platform independent encoding for delete command (#4176) 2024-05-13 20:39:13 +01:00
Evgeny Poberezkin 29db2b4e9b core: command to show duplicate message count (#4172)
* core: command to show duplicate message count

* update

* updated

* update

* update

* simplexmq
2024-05-13 19:55:57 +01:00
spaced4ndy f091384aa7 ui: apply model updates only for active user for switch and ratchet sync events (#4175) 2024-05-13 17:25:24 +01:00
spaced4ndy f40ba6f04d core: api to differentiate contacts and conversations (#4111) 2024-05-13 16:51:54 +04:00
Evgeny Poberezkin 06d61ea73e core: update simplexmq 2024-05-13 08:19:40 +01:00
Evgeny Poberezkin 84d7a77a9f core: update simplexmq (better subscription management) (#4131)
* core: update simplexmq (better subscription management)

* ui: update settings

* update simplexmq

* simplexmq

* simplexmq

* test output

* delay

* disable test
2024-05-13 08:04:12 +01:00
Evgeny Poberezkin 878eea774d core: save app themes as map with any text key (#4159)
* core: save app themes as map with any text key

* remove theme preset name

* list of themes

* theme id

* theme IDs

* moar

* colors

* default to dark

* fix
2024-05-12 23:35:14 +01:00
Narasimha-sc 4575dc6e30 langs: change 1-2 days to several days on fdroid (#4170) 2024-05-12 18:19:27 +01:00
Evgeny Poberezkin 554ef4eefc 5.7.3: ios 215, android 206, desktop 44 2024-05-12 11:36:30 +01:00
Evgeny Poberezkin 567e9981be core: 5.7.3.0 2024-05-11 23:48:39 +01:00
Evgeny Poberezkin 1183fe427f Merge branch 'stable' 2024-05-10 20:30:42 +01:00
Narasimha-sc 4f173215c6 docs: fix broken simplex group invite links (#4155)
* Fix non-working #simplex-devs invite

* Fix #simplex-devs link in pl

* replace SimpleX-Group-2

* replace simplex-group-2 link

* Update README.md
2024-05-10 17:43:06 +01:00
114 changed files with 2241 additions and 813 deletions
+4 -3
View File
@@ -1,5 +1,6 @@
--- ---
layout: layouts/privacy.html layout: layouts/privacy.html
permalink: /privacy/index.html
--- ---
# SimpleX Chat Privacy Policy and Conditions of Use # SimpleX Chat Privacy Policy and Conditions of Use
@@ -10,7 +11,7 @@ SimpleX Chat communication protocol is the first protocol that has no user profi
Double ratchet algorithm has such important properties as [forward secrecy](/docs/GLOSSARY.md#forward-secrecy), sender [repudiation](/docs/GLOSSARY.md#) and break-in recovery (also known as [post-compromise security](/docs/GLOSSARY.md#post-compromise-security)). Double ratchet algorithm has such important properties as [forward secrecy](/docs/GLOSSARY.md#forward-secrecy), sender [repudiation](/docs/GLOSSARY.md#) and break-in recovery (also known as [post-compromise security](/docs/GLOSSARY.md#post-compromise-security)).
If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
## Privacy Policy ## Privacy Policy
@@ -78,7 +79,7 @@ When you choose to use instant push notifications in SimpleX iOS app, because th
Preset notification server cannot observe the actual addresses of these queues, as a separate address is used to subscribe to the notifications. It also cannot observe who sends messages to you. Apple push notifications servers can only observe how many notifications are sent to you, but not from how many contacts, or from which messaging relays, as notifications are delivered to your device end-to-end encrypted by one of the preset notification servers - these notifications only contain end-to-end encrypted metadata, not even encrypted message content, and they look completely random to Apple push notification servers. Preset notification server cannot observe the actual addresses of these queues, as a separate address is used to subscribe to the notifications. It also cannot observe who sends messages to you. Apple push notifications servers can only observe how many notifications are sent to you, but not from how many contacts, or from which messaging relays, as notifications are delivered to your device end-to-end encrypted by one of the preset notification servers - these notifications only contain end-to-end encrypted metadata, not even encrypted message content, and they look completely random to Apple push notification servers.
You can read more about the design of iOS push notifications [here](https://simplex.chat/blog/20220404-simplex-chat-instant-notifications.html#our-ios-approach-has-one-trade-off). You can read more about the design of iOS push notifications [here](./blog/20220404-simplex-chat-instant-notifications.md#our-ios-approach-has-one-trade-off).
#### Another information stored on the servers #### Another information stored on the servers
@@ -115,7 +116,7 @@ We will update this Privacy Policy as needed so that it is current, accurate, an
Please also read our Conditions of Use of Software and Infrastructure below. Please also read our Conditions of Use of Software and Infrastructure below.
If you have questions about our Privacy Policy please contact us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). If you have questions about our Privacy Policy please contact us via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
## Conditions of Use of Software and Infrastructure ## Conditions of Use of Software and Infrastructure
+1 -1
View File
@@ -74,7 +74,7 @@ Messages not following these rules will be deleted, the right to send messages m
You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D) You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) for developers who build on SimpleX platform: There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform:
- chat bots and automations - chat bots and automations
- integrations with other apps - integrations with other apps
+73 -23
View File
@@ -341,8 +341,8 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
throw r throw r
} }
func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64) async -> ChatItem? { func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?) async -> ChatItem? {
let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId) let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId, ttl: ttl)
return await processSendMessageCmd(toChatType: toChatType, cmd: cmd) return await processSendMessageCmd(toChatType: toChatType, cmd: cmd)
} }
@@ -927,14 +927,19 @@ func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationF
} }
} }
func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async { func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async {
if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) { if let chatItem = await apiReceiveFile(
fileId: fileId,
userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(),
encrypted: privacyEncryptLocalFilesGroupDefault.get(),
auto: auto
) {
await chatItemSimpleUpdate(user, chatItem) await chatItemSimpleUpdate(user, chatItem)
} }
} }
func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? { func apiReceiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
let r = await chatSendCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline)) let r = await chatSendCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
let am = AlertManager.shared let am = AlertManager.shared
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
if case .rcvFileAcceptedSndCancelled = r { if case .rcvFileAcceptedSndCancelled = r {
@@ -947,19 +952,50 @@ func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: B
} }
} else if let networkErrorAlert = networkErrorAlert(r) { } else if let networkErrorAlert = networkErrorAlert(r) {
logger.error("apiReceiveFile network error: \(String(describing: r))") logger.error("apiReceiveFile network error: \(String(describing: r))")
am.showAlert(networkErrorAlert) if !auto {
am.showAlert(networkErrorAlert)
}
} else { } else {
switch chatError(r) { switch chatError(r) {
case .fileCancelled: case .fileCancelled:
logger.debug("apiReceiveFile ignoring fileCancelled error") logger.debug("apiReceiveFile ignoring fileCancelled error")
case .fileAlreadyReceiving: case .fileAlreadyReceiving:
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error") logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
case let .fileNotApproved(fileId, unknownServers):
logger.debug("apiReceiveFile fileNotApproved error")
if !auto {
let srvs = unknownServers.map { s in
if let srv = parseServerAddress(s), !srv.hostnames.isEmpty {
srv.hostnames[0]
} else {
serverHost(s)
}
}
am.showAlert(Alert(
title: Text("Unknown servers!"),
message: Text("Without Tor or VPN, your IP address will be visible to these XFTP relays: \(srvs.sorted().joined(separator: ", "))."),
primaryButton: .default(
Text("Download"),
action: {
Task {
logger.debug("apiReceiveFile fileNotApproved alert - in Task")
if let user = ChatModel.shared.currentUser {
await receiveFile(user: user, fileId: fileId, userApprovedRelays: true)
}
}
}
),
secondaryButton: .cancel()
))
}
default: default:
logger.error("apiReceiveFile error: \(String(describing: r))") logger.error("apiReceiveFile error: \(String(describing: r))")
am.showAlertMsg( if !auto {
title: "Error receiving file", am.showAlertMsg(
message: "Error: \(String(describing: r))" title: "Error receiving file",
) message: "Error: \(String(describing: r))"
)
}
} }
} }
return nil return nil
@@ -1849,21 +1885,35 @@ func processReceivedMsg(_ res: ChatResponse) async {
} }
case .chatSuspended: case .chatSuspended:
chatSuspended() chatSuspended()
case let .contactSwitch(_, contact, switchProgress): case let .contactSwitch(user, contact, switchProgress):
await MainActor.run { if active(user) {
m.updateContactConnectionStats(contact, switchProgress.connectionStats) await MainActor.run {
m.updateContactConnectionStats(contact, switchProgress.connectionStats)
}
} }
case let .groupMemberSwitch(_, groupInfo, member, switchProgress): case let .groupMemberSwitch(user, groupInfo, member, switchProgress):
await MainActor.run { if active(user) {
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats) await MainActor.run {
m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats)
}
} }
case let .contactRatchetSync(_, contact, ratchetSyncProgress): case let .contactRatchetSync(user, contact, ratchetSyncProgress):
await MainActor.run { if active(user) {
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats) await MainActor.run {
m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats)
}
} }
case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress): case let .groupMemberRatchetSync(user, groupInfo, member, ratchetSyncProgress):
await MainActor.run { if active(user) {
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats) await MainActor.run {
m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats)
}
}
case let .contactDisabled(user, contact):
if active(user) {
await MainActor.run {
m.updateContact(contact)
}
} }
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible):
await MainActor.run { await MainActor.run {
@@ -49,7 +49,7 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey)
} }
} }
private func serverHost(_ s: String) -> String { func serverHost(_ s: String) -> String {
if let i = s.range(of: "@")?.lowerBound { if let i = s.range(of: "@")?.lowerBound {
return String(s[i...].dropFirst()) return String(s[i...].dropFirst())
} else { } else {
@@ -60,6 +60,7 @@ struct CIFileView: View {
case .rcvInvitation: return true case .rcvInvitation: return true
case .rcvAccepted: return true case .rcvAccepted: return true
case .rcvTransfer: return false case .rcvTransfer: return false
case .rcvAborted: return true
case .rcvComplete: return true case .rcvComplete: return true
case .rcvCancelled: return false case .rcvCancelled: return false
case .rcvError: return false case .rcvError: return false
@@ -73,10 +74,10 @@ struct CIFileView: View {
logger.debug("CIFileView fileAction") logger.debug("CIFileView fileAction")
if let file = file { if let file = file {
switch (file.fileStatus) { switch (file.fileStatus) {
case .rcvInvitation: case .rcvInvitation, .rcvAborted:
if fileSizeValid(file) { if fileSizeValid(file) {
Task { Task {
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task") logger.debug("CIFileView fileAction - in .rcvInvitation, .rcvAborted, in Task")
if let user = m.currentUser { if let user = m.currentUser {
await receiveFile(user: user, fileId: file.fileId) await receiveFile(user: user, fileId: file.fileId)
} }
@@ -148,6 +149,8 @@ struct CIFileView: View {
} else { } else {
progressView() progressView()
} }
case .rcvAborted:
fileIcon("doc.fill", color: .accentColor, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12)
case .rcvComplete: fileIcon("doc.fill") case .rcvComplete: fileIcon("doc.fill")
case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
@@ -21,6 +21,8 @@ struct CIGroupInvitationView: View {
@State private var inProgress = false @State private var inProgress = false
@State private var progressByTimeout = false @State private var progressByTimeout = false
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
var body: some View { var body: some View {
let action = !chatItem.chatDir.sent && groupInvitation.status == .pending let action = !chatItem.chatDir.sent && groupInvitation.status == .pending
let v = ZStack(alignment: .bottomTrailing) { let v = ZStack(alignment: .bottomTrailing) {
@@ -43,7 +45,7 @@ struct CIGroupInvitationView: View {
.foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor) .foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor)
.font(.callout) .font(.callout)
+ Text(" ") + Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false) + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
) )
.overlay(DetermineWidth()) .overlay(DetermineWidth())
} }
@@ -51,7 +53,7 @@ struct CIGroupInvitationView: View {
( (
groupInvitationText() groupInvitationText()
+ Text(" ") + Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false) + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy)
) )
.overlay(DetermineWidth()) .overlay(DetermineWidth())
} }
@@ -17,7 +17,6 @@ struct CIImageView: View {
let maxWidth: CGFloat let maxWidth: CGFloat
@Binding var imgWidth: CGFloat? @Binding var imgWidth: CGFloat?
@State var scrollProxy: ScrollViewProxy? @State var scrollProxy: ScrollViewProxy?
@State var metaColor: Color
@State private var showFullScreenImage = false @State private var showFullScreenImage = false
var body: some View { var body: some View {
@@ -38,7 +37,7 @@ struct CIImageView: View {
.onTapGesture { .onTapGesture {
if let file = file { if let file = file {
switch file.fileStatus { switch file.fileStatus {
case .rcvInvitation: case .rcvInvitation, .rcvAborted:
Task { Task {
if let user = m.currentUser { if let user = m.currentUser {
await receiveFile(user: user, fileId: file.fileId) await receiveFile(user: user, fileId: file.fileId)
@@ -103,6 +102,7 @@ struct CIImageView: View {
case .rcvInvitation: fileIcon("arrow.down", 10, 13) case .rcvInvitation: fileIcon("arrow.down", 10, 13)
case .rcvAccepted: fileIcon("ellipsis", 14, 11) case .rcvAccepted: fileIcon("ellipsis", 14, 11)
case .rcvTransfer: progressView() case .rcvTransfer: progressView()
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
case .rcvCancelled: fileIcon("xmark", 10, 13) case .rcvCancelled: fileIcon("xmark", 10, 13)
case .rcvError: fileIcon("xmark", 10, 13) case .rcvError: fileIcon("xmark", 10, 13)
case .invalid: fileIcon("questionmark", 10, 13) case .invalid: fileIcon("questionmark", 10, 13)
@@ -116,7 +116,7 @@ struct CIImageView: View {
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: size, height: size) .frame(width: size, height: size)
.foregroundColor(metaColor) .foregroundColor(.white)
.padding(padding) .padding(padding)
} }
@@ -17,6 +17,8 @@ struct CIMetaView: View {
var showStatus = true var showStatus = true
var showEdited = true var showEdited = true
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
var body: some View { var body: some View {
if chatItem.isDeletedContent { if chatItem.isDeletedContent {
chatItem.timestampText.font(.caption).foregroundColor(metaColor) chatItem.timestampText.font(.caption).foregroundColor(metaColor)
@@ -27,24 +29,24 @@ struct CIMetaView: View {
switch meta.itemStatus { switch meta.itemStatus {
case let .sndSent(sndProgress): case let .sndSent(sndProgress):
switch sndProgress { switch sndProgress {
case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited) case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited) case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
} }
case let .sndRcvd(_, sndProgress): case let .sndRcvd(_, sndProgress):
switch sndProgress { switch sndProgress {
case .complete: case .complete:
ZStack { ZStack {
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited) ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited) ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
} }
case .partial: case .partial:
ZStack { ZStack {
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited) ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited) ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
} }
} }
default: default:
ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited) ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy)
} }
} }
} }
@@ -64,7 +66,8 @@ func ciMetaText(
transparent: Bool = false, transparent: Bool = false,
sent: SentCheckmark? = nil, sent: SentCheckmark? = nil,
showStatus: Bool = true, showStatus: Bool = true,
showEdited: Bool = true showEdited: Bool = true,
showViaProxy: Bool
) -> Text { ) -> Text {
var r = Text("") var r = Text("")
if showEdited, meta.itemEdited { if showEdited, meta.itemEdited {
@@ -78,6 +81,9 @@ func ciMetaText(
} }
r = r + Text(" ") r = r + Text(" ")
} }
if showViaProxy, meta.sentViaProxy == true {
r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
}
if showStatus { if showStatus {
if let (icon, statusColor) = meta.statusIcon(color) { if let (icon, statusColor) = meta.statusIcon(color) {
let t = Text(Image(systemName: icon)).font(.caption2) let t = Text(Image(systemName: icon)).font(.caption2)
@@ -19,6 +19,8 @@ struct CIRcvDecryptionError: View {
var chatItem: ChatItem var chatItem: ChatItem
@State private var alert: CIRcvDecryptionErrorAlert? @State private var alert: CIRcvDecryptionErrorAlert?
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
enum CIRcvDecryptionErrorAlert: Identifiable { enum CIRcvDecryptionErrorAlert: Identifiable {
case syncAllowedAlert(_ syncConnection: () -> Void) case syncAllowedAlert(_ syncConnection: () -> Void)
case syncNotSupportedContactAlert case syncNotSupportedContactAlert
@@ -119,7 +121,7 @@ struct CIRcvDecryptionError: View {
.foregroundColor(syncSupported ? .accentColor : .secondary) .foregroundColor(syncSupported ? .accentColor : .secondary)
.font(.callout) .font(.callout)
+ Text(" ") + Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true) + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
) )
} }
.padding(.horizontal, 12) .padding(.horizontal, 12)
@@ -140,7 +142,7 @@ struct CIRcvDecryptionError: View {
.foregroundColor(.red) .foregroundColor(.red)
.italic() .italic()
+ Text(" ") + Text(" ")
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true) + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
} }
.padding(.horizontal, 12) .padding(.horizontal, 12)
CIMetaView(chat: chat, chatItem: chatItem) CIMetaView(chat: chat, chatItem: chatItem)
@@ -69,7 +69,7 @@ struct CIVideoView: View {
.onTapGesture { .onTapGesture {
if let file = file { if let file = file {
switch file.fileStatus { switch file.fileStatus {
case .rcvInvitation: case .rcvInvitation, .rcvAborted:
receiveFileIfValidSize(file: file, receiveFile: receiveFile) receiveFileIfValidSize(file: file, receiveFile: receiveFile)
case .rcvAccepted: case .rcvAccepted:
switch file.fileProtocol { switch file.fileProtocol {
@@ -95,7 +95,7 @@ struct CIVideoView: View {
} }
durationProgress() durationProgress()
} }
if let file = file, case .rcvInvitation = file.fileStatus { if let file = file, showDownloadButton(file.fileStatus) {
Button { Button {
receiveFileIfValidSize(file: file, receiveFile: receiveFile) receiveFileIfValidSize(file: file, receiveFile: receiveFile)
} label: { } label: {
@@ -105,6 +105,14 @@ struct CIVideoView: View {
} }
} }
private func showDownloadButton(_ fileStatus: CIFileStatus) -> Bool {
switch fileStatus {
case .rcvInvitation: true
case .rcvAborted: true
default: false
}
}
private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View { private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View {
return ZStack(alignment: .topTrailing) { return ZStack(alignment: .topTrailing) {
ZStack(alignment: .center) { ZStack(alignment: .center) {
@@ -280,6 +288,7 @@ struct CIVideoView: View {
} else { } else {
progressView() progressView()
} }
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
case .rcvCancelled: fileIcon("xmark", 10, 13) case .rcvCancelled: fileIcon("xmark", 10, 13)
case .rcvError: fileIcon("xmark", 10, 13) case .rcvError: fileIcon("xmark", 10, 13)
case .invalid: fileIcon("questionmark", 10, 13) case .invalid: fileIcon("questionmark", 10, 13)
@@ -318,10 +327,10 @@ struct CIVideoView: View {
} }
// TODO encrypt: where file size is checked? // TODO encrypt: where file size is checked?
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool) async -> Void) { private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) {
Task { Task {
if let user = m.currentUser { if let user = m.currentUser {
await receiveFile(user, file.fileId, false) await receiveFile(user, file.fileId, false, false)
} }
} }
} }
@@ -139,9 +139,10 @@ struct VoiceMessagePlayer: View {
case .sndComplete: playbackButton() case .sndComplete: playbackButton()
case .sndCancelled: playbackButton() case .sndCancelled: playbackButton()
case .sndError: playbackButton() case .sndError: playbackButton()
case .rcvInvitation: downloadButton(recordingFile) case .rcvInvitation: downloadButton(recordingFile, "play.fill")
case .rcvAccepted: loadingIcon() case .rcvAccepted: loadingIcon()
case .rcvTransfer: loadingIcon() case .rcvTransfer: loadingIcon()
case .rcvAborted: downloadButton(recordingFile, "exclamationmark.arrow.circlepath")
case .rcvComplete: playbackButton() case .rcvComplete: playbackButton()
case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
@@ -217,7 +218,7 @@ struct VoiceMessagePlayer: View {
} }
} }
private func downloadButton(_ recordingFile: CIFile) -> some View { private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View {
Button { Button {
Task { Task {
if let user = chatModel.currentUser { if let user = chatModel.currentUser {
@@ -225,7 +226,7 @@ struct VoiceMessagePlayer: View {
} }
} }
} label: { } label: {
playPauseIcon("play.fill") playPauseIcon(icon)
} }
} }
@@ -87,12 +87,17 @@ struct FramedItemView: View {
.cornerRadius(18) .cornerRadius(18)
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
switch chatItem.meta.itemStatus { if let (title, text) = chatItem.meta.itemStatus.statusInfo {
case .sndErrorAuth: v.onTapGesture {
v.onTapGesture { msgDeliveryError("Most likely this contact has deleted the connection with you.") } AlertManager.shared.showAlert(
case let .sndError(agentError): Alert(
v.onTapGesture { msgDeliveryError("Unexpected error: \(agentError)") } title: Text(title),
default: v message: Text(text)
)
)
}
} else {
v
} }
} }
@@ -110,7 +115,7 @@ struct FramedItemView: View {
} else { } else {
switch (chatItem.content.msgContent) { switch (chatItem.content.msgContent) {
case let .image(text, image): case let .image(text, image):
CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy, metaColor: metaColor) CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy)
.overlay(DetermineWidth()) .overlay(DetermineWidth())
if text == "" && !chatItem.meta.isLive { if text == "" && !chatItem.meta.isLive {
Color.clear Color.clear
@@ -157,13 +162,6 @@ struct FramedItemView: View {
} }
} }
} }
private func msgDeliveryError(_ err: LocalizedStringKey) {
AlertManager.shared.showAlertMsg(
title: "Message delivery error",
message: err
)
}
@ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text, pad: Bool = false) -> some View { @ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text, pad: Bool = false) -> some View {
let v = HStack(spacing: 6) { let v = HStack(spacing: 6) {
@@ -35,6 +35,8 @@ struct MsgContentView: View {
@State private var typingIdx = 0 @State private var typingIdx = 0
@State private var timer: Timer? @State private var timer: Timer?
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
var body: some View { var body: some View {
if meta?.isLive == true { if meta?.isLive == true {
msgContentView() msgContentView()
@@ -81,7 +83,7 @@ struct MsgContentView: View {
} }
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text { private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true) (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy)
} }
} }
@@ -383,7 +383,7 @@ struct ChatItemInfoView: View {
let mss = membersStatuses(memberDeliveryStatuses) let mss = membersStatuses(memberDeliveryStatuses)
if !mss.isEmpty { if !mss.isEmpty {
ForEach(mss, id: \.0.groupMemberId) { memberStatus in ForEach(mss, id: \.0.groupMemberId) { memberStatus in
memberDeliveryStatusView(memberStatus.0, memberStatus.1) memberDeliveryStatusView(memberStatus.0, memberStatus.1, memberStatus.2)
} }
} else { } else {
Text("No delivery information") Text("No delivery information")
@@ -392,23 +392,27 @@ struct ChatItemInfoView: View {
} }
} }
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] { private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus, Bool?)] {
memberDeliveryStatuses.compactMap({ mds in memberDeliveryStatuses.compactMap({ mds in
if let mem = chatModel.getGroupMember(mds.groupMemberId) { if let mem = chatModel.getGroupMember(mds.groupMemberId) {
return (mem.wrapped, mds.memberDeliveryStatus) return (mem.wrapped, mds.memberDeliveryStatus, mds.sentViaProxy)
} else { } else {
return nil return nil
} }
}) })
} }
private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus) -> some View { private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus, _ sentViaProxy: Bool?) -> some View {
HStack{ HStack{
ProfileImage(imageStr: member.image, size: 30) ProfileImage(imageStr: member.image, size: 30)
.padding(.trailing, 2) .padding(.trailing, 2)
Text(member.chatViewName) Text(member.chatViewName)
.lineLimit(1) .lineLimit(1)
Spacer() Spacer()
if sentViaProxy == true {
Image(systemName: "arrow.forward")
.foregroundColor(.secondary).opacity(0.67)
}
let v = Group { let v = Group {
if let (icon, statusColor) = status.statusIcon(Color.secondary) { if let (icon, statusColor) = status.statusIcon(Color.secondary) {
switch status { switch status {
@@ -712,9 +712,9 @@ struct ComposeView: View {
if chat.chatInfo.contact?.nextSendGrpInv ?? false { if chat.chatInfo.contact?.nextSendGrpInv ?? false {
await sendMemberContactInvitation() await sendMemberContactInvitation()
} else if case let .forwardingItem(ci, fromChatInfo) = composeState.contextItem { } else if case let .forwardingItem(ci, fromChatInfo) = composeState.contextItem {
sent = await forwardItem(ci, fromChatInfo) sent = await forwardItem(ci, fromChatInfo, ttl)
if !composeState.message.isEmpty { if !composeState.message.isEmpty {
sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: nil) sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl)
} }
} else if case let .editingItem(ci) = composeState.contextItem { } else if case let .editingItem(ci) = composeState.contextItem {
sent = await updateMessage(ci, live: live) sent = await updateMessage(ci, live: live)
@@ -890,13 +890,14 @@ struct ComposeView: View {
return nil return nil
} }
func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo) async -> ChatItem? { func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> ChatItem? {
if let chatItem = await apiForwardChatItem( if let chatItem = await apiForwardChatItem(
toChatType: chat.chatInfo.chatType, toChatType: chat.chatInfo.chatType,
toChatId: chat.chatInfo.apiId, toChatId: chat.chatInfo.apiId,
fromChatType: fromChatInfo.chatType, fromChatType: fromChatInfo.chatType,
fromChatId: fromChatInfo.apiId, fromChatId: fromChatInfo.apiId,
itemId: forwardedItem.id itemId: forwardedItem.id,
ttl: ttl
) { ) {
await MainActor.run { await MainActor.run {
chatModel.addChatItem(chat.chatInfo, chatItem) chatModel.addChatItem(chat.chatInfo, chatItem)
@@ -224,8 +224,7 @@ struct SendMessageView: View {
@ViewBuilder private func sendButtonContextMenuItems() -> some View { @ViewBuilder private func sendButtonContextMenuItems() -> some View {
if composeState.liveMessage == nil, if composeState.liveMessage == nil,
!composeState.editing, !composeState.editing {
!composeState.forwarding {
if case .noContextItem = composeState.contextItem, if case .noContextItem = composeState.contextItem,
!composeState.voicePreview, !composeState.voicePreview,
let send = sendLiveMessage, let send = sendLiveMessage,
@@ -240,14 +240,14 @@ struct ChatPreviewView: View {
private func itemStatusMark(_ cItem: ChatItem) -> Text { private func itemStatusMark(_ cItem: ChatItem) -> Text {
switch cItem.meta.itemStatus { switch cItem.meta.itemStatus {
case .sndErrorAuth: case .sndErrorAuth, .sndError:
return Text(Image(systemName: "multiply")) return Text(Image(systemName: "multiply"))
.font(.caption) .font(.caption)
.foregroundColor(.red) + Text(" ") .foregroundColor(.red) + Text(" ")
case .sndError: case .sndWarning:
return Text(Image(systemName: "exclamationmark.triangle.fill")) return Text(Image(systemName: "exclamationmark.triangle.fill"))
.font(.caption) .font(.caption)
.foregroundColor(.yellow) + Text(" ") .foregroundColor(.orange) + Text(" ")
default: return Text("") default: return Text("")
} }
} }
@@ -51,10 +51,10 @@ struct AdvancedNetworkSettings: View {
} }
.disabled(currentNetCfg == NetCfg.proxyDefaults) .disabled(currentNetCfg == NetCfg.proxyDefaults)
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [10_000000, 15_000000, 20_000000, 25_000000, 35_000000, 50_000000], label: secondsLabel) timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [10_000000, 15_000000, 20_000000, 30_000000, 45_000000, 60_000000, 90_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel) timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [2_500, 5_000, 10_000, 15_000, 20_000, 30_000], label: secondsLabel) timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [2_500, 5_000, 10_000, 15_000, 20_000, 30_000], label: secondsLabel)
intSettingPicker("Receiving concurrency", selection: $netCfg.rcvConcurrency, values: [1, 2, 4, 8, 12, 16, 24], label: "") // intSettingPicker("Receiving concurrency", selection: $netCfg.rcvConcurrency, values: [1, 2, 4, 8, 12, 16, 24], label: "")
timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel) timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel)
intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "") intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "")
Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive) Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive)
@@ -23,6 +23,7 @@ extension AppSettings {
setNetCfg(val) setNetCfg(val)
} }
if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) } if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) }
if let val = privacyAskToApproveRelays { privacyAskToApproveRelaysGroupDefault.set(val) }
if let val = privacyAcceptImages { if let val = privacyAcceptImages {
privacyAcceptImagesGroupDefault.set(val) privacyAcceptImagesGroupDefault.set(val)
def.setValue(val, forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES) def.setValue(val, forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)
@@ -50,6 +51,7 @@ extension AppSettings {
var c = AppSettings.defaults var c = AppSettings.defaults
c.networkConfig = getNetCfg() c.networkConfig = getNetCfg()
c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get() c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get()
c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get()
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get() c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS)
@@ -12,12 +12,16 @@ import SimpleXChat
private enum NetworkAlert: Identifiable { private enum NetworkAlert: Identifiable {
case updateOnionHosts(hosts: OnionHosts) case updateOnionHosts(hosts: OnionHosts)
case updateSessionMode(mode: TransportSessionMode) case updateSessionMode(mode: TransportSessionMode)
case updateSMPProxyMode(proxyMode: SMPProxyMode)
case updateSMPProxyFallback(proxyFallback: SMPProxyFallback)
case error(err: String) case error(err: String)
var id: String { var id: String {
switch self { switch self {
case let .updateOnionHosts(hosts): return "updateOnionHosts \(hosts)" case let .updateOnionHosts(hosts): return "updateOnionHosts \(hosts)"
case let .updateSessionMode(mode): return "updateSessionMode \(mode)" case let .updateSessionMode(mode): return "updateSessionMode \(mode)"
case let .updateSMPProxyMode(proxyMode): return "updateSMPProxyMode \(proxyMode)"
case let .updateSMPProxyFallback(proxyFallback): return "updateSMPProxyFallback \(proxyFallback)"
case let .error(err): return "error \(err)" case let .error(err): return "error \(err)"
} }
} }
@@ -26,11 +30,14 @@ private enum NetworkAlert: Identifiable {
struct NetworkAndServers: View { struct NetworkAndServers: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = true
@State private var cfgLoaded = false @State private var cfgLoaded = false
@State private var currentNetCfg = NetCfg.defaults @State private var currentNetCfg = NetCfg.defaults
@State private var netCfg = NetCfg.defaults @State private var netCfg = NetCfg.defaults
@State private var onionHosts: OnionHosts = .no @State private var onionHosts: OnionHosts = .no
@State private var sessionMode: TransportSessionMode = .user @State private var sessionMode: TransportSessionMode = .user
@State private var proxyMode: SMPProxyMode = .never
@State private var proxyFallback: SMPProxyFallback = .allow
@State private var alert: NetworkAlert? @State private var alert: NetworkAlert?
var body: some View { var body: some View {
@@ -75,6 +82,30 @@ struct NetworkAndServers: View {
Text("Using .onion hosts requires compatible VPN provider.") Text("Using .onion hosts requires compatible VPN provider.")
} }
Section {
Picker("Private routing", selection: $proxyMode) {
ForEach(SMPProxyMode.values, id: \.self) { Text($0.text) }
}
.frame(height: 36)
Picker("Allow downgrade", selection: $proxyFallback) {
ForEach(SMPProxyFallback.values, id: \.self) { Text($0.text) }
}
.disabled(proxyMode == .never)
.frame(height: 36)
Toggle("Show message status", isOn: $showSentViaProxy)
} header: {
Text("Private message routing")
} footer: {
VStack(alignment: .leading) {
Text("To protect your IP address, private routing uses your SMP servers to deliver messages.")
if showSentViaProxy {
Text("Show → on messages sent via private routing.")
}
}
}
Section("Calls") { Section("Calls") {
NavigationLink { NavigationLink {
RTCServers() RTCServers()
@@ -99,14 +130,24 @@ struct NetworkAndServers: View {
currentNetCfg = getNetCfg() currentNetCfg = getNetCfg()
resetNetCfgView() resetNetCfgView()
} }
.onChange(of: onionHosts) { _ in .onChange(of: onionHosts) { hosts in
if onionHosts != OnionHosts(netCfg: currentNetCfg) { if hosts != OnionHosts(netCfg: currentNetCfg) {
alert = .updateOnionHosts(hosts: onionHosts) alert = .updateOnionHosts(hosts: hosts)
} }
} }
.onChange(of: sessionMode) { _ in .onChange(of: sessionMode) { mode in
if sessionMode != netCfg.sessionMode { if mode != netCfg.sessionMode {
alert = .updateSessionMode(mode: sessionMode) alert = .updateSessionMode(mode: mode)
}
}
.onChange(of: proxyMode) { mode in
if mode != netCfg.smpProxyMode {
alert = .updateSMPProxyMode(proxyMode: mode)
}
}
.onChange(of: proxyFallback) { fallbackMode in
if fallbackMode != netCfg.smpProxyFallback {
alert = .updateSMPProxyFallback(proxyFallback: fallbackMode)
} }
} }
.alert(item: $alert) { a in .alert(item: $alert) { a in
@@ -137,6 +178,30 @@ struct NetworkAndServers: View {
resetNetCfgView() resetNetCfgView()
} }
) )
case let .updateSMPProxyMode(proxyMode):
return Alert(
title: Text("Message routing mode"),
message: Text(proxyModeInfo(proxyMode)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
primaryButton: .default(Text("Ok")) {
netCfg.smpProxyMode = proxyMode
saveNetCfg()
},
secondaryButton: .cancel() {
resetNetCfgView()
}
)
case let .updateSMPProxyFallback(proxyFallback):
return Alert(
title: Text("Message routing fallback"),
message: Text(proxyFallbackInfo(proxyFallback)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
primaryButton: .default(Text("Ok")) {
netCfg.smpProxyFallback = proxyFallback
saveNetCfg()
},
secondaryButton: .cancel() {
resetNetCfgView()
}
)
case let .error(err): case let .error(err):
return Alert( return Alert(
title: Text("Error updating settings"), title: Text("Error updating settings"),
@@ -166,6 +231,8 @@ struct NetworkAndServers: View {
netCfg = currentNetCfg netCfg = currentNetCfg
onionHosts = OnionHosts(netCfg: netCfg) onionHosts = OnionHosts(netCfg: netCfg)
sessionMode = netCfg.sessionMode sessionMode = netCfg.sessionMode
proxyMode = netCfg.smpProxyMode
proxyFallback = netCfg.smpProxyFallback
} }
private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey { private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey {
@@ -182,6 +249,23 @@ struct NetworkAndServers: View {
case .entity: return "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." case .entity: return "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail."
} }
} }
private func proxyModeInfo(_ mode: SMPProxyMode) -> LocalizedStringKey {
switch mode {
case .always: return "Always use private routing."
case .unknown: return "Use private routing with unknown servers."
case .unprotected: return "Use private routing with unknown servers when IP address is not protected."
case .never: return "Do NOT use private routing."
}
}
private func proxyFallbackInfo(_ proxyFallback: SMPProxyFallback) -> LocalizedStringKey {
switch proxyFallback {
case .allow: return "Send messages directly when your or destination server does not support private routing."
case .allowProtected: return "Send messages directly when IP address is protected and your or destination server does not support private routing."
case .prohibit: return "Do NOT send messages directly, even if your or destination server does not support private routing."
}
}
} }
struct NetworkServersView_Previews: PreviewProvider { struct NetworkServersView_Previews: PreviewProvider {
@@ -16,6 +16,7 @@ struct PrivacySettings: View {
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
@AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true @AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true
@AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get() @State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@@ -64,18 +65,6 @@ struct PrivacySettings: View {
} }
Section { Section {
settingsRow("lock.doc") {
Toggle("Encrypt local files", isOn: $encryptLocalFiles)
.onChange(of: encryptLocalFiles) {
setEncryptLocalFiles($0)
}
}
settingsRow("photo") {
Toggle("Auto-accept images", isOn: $autoAcceptImages)
.onChange(of: autoAcceptImages) {
privacyAcceptImagesGroupDefault.set($0)
}
}
settingsRow("network") { settingsRow("network") {
Toggle("Send link previews", isOn: $useLinkPreviews) Toggle("Send link previews", isOn: $useLinkPreviews)
} }
@@ -108,6 +97,32 @@ struct PrivacySettings: View {
Text("Chats") Text("Chats")
} }
Section {
settingsRow("lock.doc") {
Toggle("Encrypt local files", isOn: $encryptLocalFiles)
.onChange(of: encryptLocalFiles) {
setEncryptLocalFiles($0)
}
}
settingsRow("photo") {
Toggle("Auto-accept images", isOn: $autoAcceptImages)
.onChange(of: autoAcceptImages) {
privacyAcceptImagesGroupDefault.set($0)
}
}
settingsRow("network.badge.shield.half.filled") {
Toggle("Protect IP address", isOn: $askToApproveRelays)
}
} header: {
Text("Files")
} footer: {
if askToApproveRelays {
Text("The app will ask to confirm downloads from unknown file servers (except .onion).")
} else {
Text("Without Tor or VPN, your IP address will be visible to file servers.")
}
}
Section { Section {
settingsRow("person") { settingsRow("person") {
Toggle("Contacts", isOn: $contactReceipts) Toggle("Contacts", isOn: $contactReceipts)
@@ -60,6 +60,7 @@ let DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS = "deviceNameForRemoteAccess"
let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions" let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions"
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast" let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast"
let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto" let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto"
let DEFAULT_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen" let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
@@ -99,6 +100,7 @@ let appDefaults: [String: Any] = [
DEFAULT_CONFIRM_REMOTE_SESSIONS: false, DEFAULT_CONFIRM_REMOTE_SESSIONS: false,
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true, DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true,
DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true, DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true,
DEFAULT_SHOW_SENT_VIA_RPOXY: false,
ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue
] ]
@@ -696,14 +696,16 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
} }
func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? { func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? {
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline)) let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get()
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
logger.error("receiveFile error: \(responseError(r))") logger.error("receiveFile error: \(responseError(r))")
return nil return nil
} }
func apiSetFileToReceive(fileId: Int64, encrypted: Bool) { func apiSetFileToReceive(fileId: Int64, encrypted: Bool) {
let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, encrypted: encrypted)) let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get()
let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted))
if case .cmdOk = r { return } if case .cmdOk = r { return }
logger.error("setFileToReceive error: \(responseError(r))") logger.error("setFileToReceive error: \(responseError(r))")
} }
+32 -32
View File
@@ -76,6 +76,11 @@
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; }; 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; };
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; }; 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; };
5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; }; 5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; };
5C9F3DD62BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD12BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a */; };
5C9F3DD72BFBCDD90003B86B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD22BFBCDD80003B86B /* libffi.a */; };
5C9F3DD82BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD32BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a */; };
5C9F3DD92BFBCDD90003B86B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD42BFBCDD90003B86B /* libgmpxx.a */; };
5C9F3DDA2BFBCDD90003B86B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD52BFBCDD90003B86B /* libgmp.a */; };
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; }; 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; };
5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; }; 5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; };
5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; }; 5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; };
@@ -115,11 +120,6 @@
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; };
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; }; 5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; };
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; }; 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
5CE0E8A12BEE807A008D6E06 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E89C2BEE807A008D6E06 /* libffi.a */; };
5CE0E8A22BEE807A008D6E06 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E89D2BEE807A008D6E06 /* libgmpxx.a */; };
5CE0E8A32BEE807A008D6E06 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E89E2BEE807A008D6E06 /* libgmp.a */; };
5CE0E8A42BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E89F2BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc-ghc9.6.3.a */; };
5CE0E8A52BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8A02BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a */; };
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; }; 5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; };
@@ -354,6 +354,11 @@
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; }; 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; };
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; }; 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; }; 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; };
5C9F3DD12BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a"; sourceTree = "<group>"; };
5C9F3DD22BFBCDD80003B86B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C9F3DD32BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a"; sourceTree = "<group>"; };
5C9F3DD42BFBCDD90003B86B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C9F3DD52BFBCDD90003B86B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; }; 5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; }; 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; }; 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; };
@@ -420,11 +425,6 @@
5CDCAD7428188D2900503DA2 /* APITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITypes.swift; sourceTree = "<group>"; }; 5CDCAD7428188D2900503DA2 /* APITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITypes.swift; sourceTree = "<group>"; };
5CDCAD7D2818941F00503DA2 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; }; 5CDCAD7D2818941F00503DA2 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; };
5CDCAD80281A7E2700503DA2 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; }; 5CDCAD80281A7E2700503DA2 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
5CE0E89C2BEE807A008D6E06 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CE0E89D2BEE807A008D6E06 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CE0E89E2BEE807A008D6E06 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CE0E89F2BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc-ghc9.6.3.a"; sourceTree = "<group>"; };
5CE0E8A02BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a"; sourceTree = "<group>"; };
5CE1330328E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = "de.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5CE1330328E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = "de.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5CE1330428E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5CE1330428E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
5CE2BA682845308900EC33A6 /* SimpleXChat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleXChat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleXChat.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -529,13 +529,13 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5CE0E8A52BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a in Frameworks */, 5C9F3DD92BFBCDD90003B86B /* libgmpxx.a in Frameworks */,
5CE0E8A22BEE807A008D6E06 /* libgmpxx.a in Frameworks */, 5C9F3DDA2BFBCDD90003B86B /* libgmp.a in Frameworks */,
5CE0E8A32BEE807A008D6E06 /* libgmp.a in Frameworks */, 5C9F3DD82BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a in Frameworks */,
5CE0E8A42BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc-ghc9.6.3.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5CE0E8A12BEE807A008D6E06 /* libffi.a in Frameworks */, 5C9F3DD62BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C9F3DD72BFBCDD90003B86B /* libffi.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -601,11 +601,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5CE0E89C2BEE807A008D6E06 /* libffi.a */, 5C9F3DD22BFBCDD80003B86B /* libffi.a */,
5CE0E89E2BEE807A008D6E06 /* libgmp.a */, 5C9F3DD52BFBCDD90003B86B /* libgmp.a */,
5CE0E89D2BEE807A008D6E06 /* libgmpxx.a */, 5C9F3DD42BFBCDD90003B86B /* libgmpxx.a */,
5CE0E89F2BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc-ghc9.6.3.a */, 5C9F3DD12BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a */,
5CE0E8A02BEE807A008D6E06 /* libHSsimplex-chat-5.7.2.0-ApjXLu1rhEfAsjeg5vuofc.a */, 5C9F3DD32BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1552,7 +1552,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 214; CURRENT_PROJECT_VERSION = 218;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -1577,7 +1577,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
LLVM_LTO = YES_THIN; LLVM_LTO = YES_THIN;
MARKETING_VERSION = 5.7.2; MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1601,7 +1601,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 214; CURRENT_PROJECT_VERSION = 218;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -1626,7 +1626,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 5.7.2; MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1687,7 +1687,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 214; CURRENT_PROJECT_VERSION = 218;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s; GCC_OPTIMIZATION_LEVEL = s;
@@ -1702,7 +1702,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 5.7.2; MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1724,7 +1724,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 214; CURRENT_PROJECT_VERSION = 218;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO; ENABLE_CODE_COVERAGE = NO;
@@ -1739,7 +1739,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 5.7.2; MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1761,7 +1761,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 214; CURRENT_PROJECT_VERSION = 218;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1787,7 +1787,7 @@
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 5.7.2; MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1812,7 +1812,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 214; CURRENT_PROJECT_VERSION = 218;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1838,7 +1838,7 @@
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 5.7.2; MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
+61 -6
View File
@@ -48,7 +48,7 @@ public enum ChatCommand {
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64) case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64)
case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction)
case apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64) case apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?)
case apiGetNtfToken case apiGetNtfToken
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
case apiVerifyToken(token: DeviceToken, nonce: String, code: String) case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
@@ -123,8 +123,8 @@ public enum ChatCommand {
case apiGetNetworkStatuses case apiGetNetworkStatuses
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
case receiveFile(fileId: Int64, encrypted: Bool?, inline: Bool?) case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?)
case setFileToReceive(fileId: Int64, encrypted: Bool?) case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?)
case cancelFile(fileId: Int64) case cancelFile(fileId: Int64)
// remote desktop commands // remote desktop commands
case setLocalDeviceName(displayName: String) case setLocalDeviceName(displayName: String)
@@ -192,7 +192,9 @@ public enum ChatCommand {
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)" case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)" case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)"
case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))" case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))"
case let .apiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId): return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemId)" case let .apiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId, ttl):
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemId) ttl=\(ttlStr)"
case .apiGetNtfToken: return "/_ntf get " case .apiGetNtfToken: return "/_ntf get "
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
@@ -280,8 +282,8 @@ public enum ChatCommand {
case .apiGetNetworkStatuses: return "/_network_statuses" case .apiGetNetworkStatuses: return "/_network_statuses"
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)" case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))" case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
case let .receiveFile(fileId, encrypt, inline): return "/freceive \(fileId)\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))" case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))"
case let .setFileToReceive(fileId, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("encrypt", encrypt))" case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))"
case let .cancelFile(fileId): return "/fcancel \(fileId)" case let .cancelFile(fileId): return "/fcancel \(fileId)"
case let .setLocalDeviceName(displayName): return "/set device name \(displayName)" case let .setLocalDeviceName(displayName): return "/set device name \(displayName)"
case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)" case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)"
@@ -639,6 +641,7 @@ public enum ChatResponse: Decodable, Error {
case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo]) case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo) case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo)
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
case contactDisabled(user: UserRef, contact: Contact)
// remote desktop responses/events // remote desktop responses/events
case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo]) case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo])
case remoteCtrlFound(remoteCtrl: RemoteCtrlInfo, ctrlAppInfo_: CtrlAppInfo?, appVersion: String, compatible: Bool) case remoteCtrlFound(remoteCtrl: RemoteCtrlInfo, ctrlAppInfo_: CtrlAppInfo?, appVersion: String, compatible: Bool)
@@ -796,6 +799,7 @@ public enum ChatResponse: Decodable, Error {
case .ntfMessages: return "ntfMessages" case .ntfMessages: return "ntfMessages"
case .ntfMessage: return "ntfMessage" case .ntfMessage: return "ntfMessage"
case .contactConnectionDeleted: return "contactConnectionDeleted" case .contactConnectionDeleted: return "contactConnectionDeleted"
case .contactDisabled: return "contactDisabled"
case .remoteCtrlList: return "remoteCtrlList" case .remoteCtrlList: return "remoteCtrlList"
case .remoteCtrlFound: return "remoteCtrlFound" case .remoteCtrlFound: return "remoteCtrlFound"
case .remoteCtrlConnecting: return "remoteCtrlConnecting" case .remoteCtrlConnecting: return "remoteCtrlConnecting"
@@ -953,6 +957,7 @@ public enum ChatResponse: Decodable, Error {
case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))") case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))")
case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))") case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection)) case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
case let .contactDisabled(u, contact): return withUser(u, String(describing: contact))
case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls) case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls)
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)" case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)"
case let .remoteCtrlConnecting(remoteCtrl_, ctrlAppInfo, appVersion): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nctrlAppInfo:\n\(String(describing: ctrlAppInfo))\nappVersion: \(appVersion)" case let .remoteCtrlConnecting(remoteCtrl_, ctrlAppInfo, appVersion): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nctrlAppInfo:\n\(String(describing: ctrlAppInfo))\nappVersion: \(appVersion)"
@@ -1242,9 +1247,12 @@ public struct ServerAddress: Decodable {
public struct NetCfg: Codable, Equatable { public struct NetCfg: Codable, Equatable {
public var socksProxy: String? = nil public var socksProxy: String? = nil
var socksMode: SocksMode = .always
public var hostMode: HostMode = .publicHost public var hostMode: HostMode = .publicHost
public var requiredHostMode = true public var requiredHostMode = true
public var sessionMode: TransportSessionMode public var sessionMode: TransportSessionMode
public var smpProxyMode: SMPProxyMode = .never
public var smpProxyFallback: SMPProxyFallback = .allow
public var tcpConnectTimeout: Int // microseconds public var tcpConnectTimeout: Int // microseconds
public var tcpTimeout: Int // microseconds public var tcpTimeout: Int // microseconds
public var tcpTimeoutPerKb: Int // microseconds public var tcpTimeoutPerKb: Int // microseconds
@@ -1289,6 +1297,49 @@ public enum HostMode: String, Codable {
case publicHost = "public" case publicHost = "public"
} }
public enum SocksMode: String, Codable {
case always = "always"
case onion = "onion"
}
public enum SMPProxyMode: String, Codable {
case always = "always"
case unknown = "unknown"
case unprotected = "unprotected"
case never = "never"
public var text: LocalizedStringKey {
switch self {
case .always: return "always"
case .unknown: return "unknown relays"
case .unprotected: return "unprotected"
case .never: return "never"
}
}
public var id: SMPProxyMode { self }
public static let values: [SMPProxyMode] = [.always, .unknown, .unprotected, .never]
}
public enum SMPProxyFallback: String, Codable {
case allow = "allow"
case allowProtected = "allowProtected"
case prohibit = "prohibit"
public var text: LocalizedStringKey {
switch self {
case .allow: return "yes"
case .allowProtected: return "when IP hidden"
case .prohibit: return "no"
}
}
public var id: SMPProxyFallback { self }
public static let values: [SMPProxyFallback] = [.allow, .allowProtected, .prohibit]
}
public enum OnionHosts: String, Identifiable { public enum OnionHosts: String, Identifiable {
case no case no
case prefer case prefer
@@ -1709,6 +1760,7 @@ public enum ChatErrorType: Decodable {
case fileImageType(filePath: String) case fileImageType(filePath: String)
case fileImageSize(filePath: String) case fileImageSize(filePath: String)
case fileNotReceived(fileId: Int64) case fileNotReceived(fileId: Int64)
case fileNotApproved(fileId: Int64, unknownServers: [String])
// case xFTPRcvFile // case xFTPRcvFile
// case xFTPSndFile // case xFTPSndFile
case fallbackToSMPProhibited(fileId: Int64) case fallbackToSMPProhibited(fileId: Int64)
@@ -1987,6 +2039,7 @@ public struct MigrationFileLinkData: Codable {
public struct AppSettings: Codable, Equatable { public struct AppSettings: Codable, Equatable {
public var networkConfig: NetCfg? = nil public var networkConfig: NetCfg? = nil
public var privacyEncryptLocalFiles: Bool? = nil public var privacyEncryptLocalFiles: Bool? = nil
public var privacyAskToApproveRelays: Bool? = nil
public var privacyAcceptImages: Bool? = nil public var privacyAcceptImages: Bool? = nil
public var privacyLinkPreviews: Bool? = nil public var privacyLinkPreviews: Bool? = nil
public var privacyShowChatPreviews: Bool? = nil public var privacyShowChatPreviews: Bool? = nil
@@ -2010,6 +2063,7 @@ public struct AppSettings: Codable, Equatable {
let def = AppSettings.defaults let def = AppSettings.defaults
if networkConfig != def.networkConfig { empty.networkConfig = networkConfig } if networkConfig != def.networkConfig { empty.networkConfig = networkConfig }
if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
if privacyAskToApproveRelays != def.privacyAskToApproveRelays { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages } if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages }
if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews } if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews }
if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews } if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews }
@@ -2034,6 +2088,7 @@ public struct AppSettings: Codable, Equatable {
AppSettings ( AppSettings (
networkConfig: NetCfg.defaults, networkConfig: NetCfg.defaults,
privacyEncryptLocalFiles: true, privacyEncryptLocalFiles: true,
privacyAskToApproveRelays: true,
privacyAcceptImages: true, privacyAcceptImages: true,
privacyLinkPreviews: true, privacyLinkPreviews: true,
privacyShowChatPreviews: true, privacyShowChatPreviews: true,
+26
View File
@@ -23,9 +23,12 @@ public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used
public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles" public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles"
public let GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS = "privacyAskToApproveRelays"
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount" let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts" let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode" let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
let GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE = "networkSMPProxyMode"
let GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK = "networkSMPProxyFallback"
let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout" let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout"
let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout"
let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb"
@@ -53,6 +56,8 @@ public func registerGroupDefaults() {
GROUP_DEFAULT_NTF_ENABLE_PERIODIC: false, GROUP_DEFAULT_NTF_ENABLE_PERIODIC: false,
GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS: OnionHosts.no.rawValue, GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS: OnionHosts.no.rawValue,
GROUP_DEFAULT_NETWORK_SESSION_MODE: TransportSessionMode.user.rawValue, GROUP_DEFAULT_NETWORK_SESSION_MODE: TransportSessionMode.user.rawValue,
GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE: SMPProxyMode.never.rawValue,
GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK: SMPProxyFallback.allow.rawValue,
GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout, GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout,
GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout,
GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb,
@@ -69,6 +74,7 @@ public func registerGroupDefaults() {
GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true, GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false, GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false,
GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true, GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true,
GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS: true,
GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false, GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false,
GROUP_DEFAULT_CALL_KIT_ENABLED: true, GROUP_DEFAULT_CALL_KIT_ENABLED: true,
GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false, GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false,
@@ -177,6 +183,8 @@ public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults
public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES) public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES)
public let privacyAskToApproveRelaysGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS)
public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT) public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT)
public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>( public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>(
@@ -191,6 +199,18 @@ public let networkSessionModeGroupDefault = EnumDefault<TransportSessionMode>(
withDefault: .user withDefault: .user
) )
public let networkSMPProxyModeGroupDefault = EnumDefault<SMPProxyMode>(
defaults: groupDefaults,
forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE,
withDefault: .never
)
public let networkSMPProxyFallbackGroupDefault = EnumDefault<SMPProxyFallback>(
defaults: groupDefaults,
forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK,
withDefault: .allow
)
public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_STORE_DB_PASSPHRASE) public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_STORE_DB_PASSPHRASE)
public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE) public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE)
@@ -275,6 +295,8 @@ public func getNetCfg() -> NetCfg {
let onionHosts = networkUseOnionHostsGroupDefault.get() let onionHosts = networkUseOnionHostsGroupDefault.get()
let (hostMode, requiredHostMode) = onionHosts.hostMode let (hostMode, requiredHostMode) = onionHosts.hostMode
let sessionMode = networkSessionModeGroupDefault.get() let sessionMode = networkSessionModeGroupDefault.get()
let smpProxyMode = networkSMPProxyModeGroupDefault.get()
let smpProxyFallback = networkSMPProxyFallbackGroupDefault.get()
let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
@@ -295,6 +317,8 @@ public func getNetCfg() -> NetCfg {
hostMode: hostMode, hostMode: hostMode,
requiredHostMode: requiredHostMode, requiredHostMode: requiredHostMode,
sessionMode: sessionMode, sessionMode: sessionMode,
smpProxyMode: smpProxyMode,
smpProxyFallback: smpProxyFallback,
tcpConnectTimeout: tcpConnectTimeout, tcpConnectTimeout: tcpConnectTimeout,
tcpTimeout: tcpTimeout, tcpTimeout: tcpTimeout,
tcpTimeoutPerKb: tcpTimeoutPerKb, tcpTimeoutPerKb: tcpTimeoutPerKb,
@@ -309,6 +333,8 @@ public func getNetCfg() -> NetCfg {
public func setNetCfg(_ cfg: NetCfg) { public func setNetCfg(_ cfg: NetCfg) {
networkUseOnionHostsGroupDefault.set(OnionHosts(netCfg: cfg)) networkUseOnionHostsGroupDefault.set(OnionHosts(netCfg: cfg))
networkSessionModeGroupDefault.set(cfg.sessionMode) networkSessionModeGroupDefault.set(cfg.sessionMode)
networkSMPProxyModeGroupDefault.set(cfg.smpProxyMode)
networkSMPProxyFallbackGroupDefault.set(cfg.smpProxyFallback)
groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
+66 -6
View File
@@ -1510,7 +1510,12 @@ public struct Contact: Identifiable, Decodable, NamedChat {
public var ready: Bool { get { activeConn?.connStatus == .ready } } public var ready: Bool { get { activeConn?.connStatus == .ready } }
public var active: Bool { get { contactStatus == .active } } public var active: Bool { get { contactStatus == .active } }
public var sendMsgEnabled: Bool { get { public var sendMsgEnabled: Bool { get {
(ready && active && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false)) (
ready
&& active
&& !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false)
&& !(activeConn?.connDisabled ?? true)
)
|| nextSendGrpInv || nextSendGrpInv
} } } }
public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } } public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } }
@@ -1601,15 +1606,20 @@ public struct Connection: Decodable {
public var pqEncryption: Bool public var pqEncryption: Bool
public var pqSndEnabled: Bool? public var pqSndEnabled: Bool?
public var pqRcvEnabled: Bool? public var pqRcvEnabled: Bool?
public var authErrCounter: Int
public var connectionStats: ConnectionStats? = nil public var connectionStats: ConnectionStats? = nil
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter
} }
public var id: ChatId { get { ":\(connId)" } } public var id: ChatId { get { ":\(connId)" } }
public var connDisabled: Bool {
authErrCounter >= 10 // authErrDisableCount in core
}
public var connPQEnabled: Bool { public var connPQEnabled: Bool {
pqSndEnabled == true && pqRcvEnabled == true pqSndEnabled == true && pqRcvEnabled == true
} }
@@ -1622,7 +1632,8 @@ public struct Connection: Decodable {
connLevel: 0, connLevel: 0,
viaGroupLink: false, viaGroupLink: false,
pqSupport: false, pqSupport: false,
pqEncryption: false pqEncryption: false,
authErrCounter: 0
) )
} }
@@ -2621,6 +2632,7 @@ public struct CIMeta: Decodable {
public var itemTs: Date public var itemTs: Date
var itemText: String var itemText: String
public var itemStatus: CIStatus public var itemStatus: CIStatus
public var sentViaProxy: Bool?
public var createdAt: Date public var createdAt: Date
public var updatedAt: Date public var updatedAt: Date
public var itemForwarded: CIForwardedFrom? public var itemForwarded: CIForwardedFrom?
@@ -2710,7 +2722,8 @@ public enum CIStatus: Decodable {
case sndSent(sndProgress: SndCIStatusProgress) case sndSent(sndProgress: SndCIStatusProgress)
case sndRcvd(msgRcptStatus: MsgReceiptStatus, sndProgress: SndCIStatusProgress) case sndRcvd(msgRcptStatus: MsgReceiptStatus, sndProgress: SndCIStatusProgress)
case sndErrorAuth case sndErrorAuth
case sndError(agentError: String) case sndError(agentError: SndError)
case sndWarning(agentError: SndError)
case rcvNew case rcvNew
case rcvRead case rcvRead
case invalid(text: String) case invalid(text: String)
@@ -2722,6 +2735,7 @@ public enum CIStatus: Decodable {
case .sndRcvd: return "sndRcvd" case .sndRcvd: return "sndRcvd"
case .sndErrorAuth: return "sndErrorAuth" case .sndErrorAuth: return "sndErrorAuth"
case .sndError: return "sndError" case .sndError: return "sndError"
case .sndWarning: return "sndWarning"
case .rcvNew: return "rcvNew" case .rcvNew: return "rcvNew"
case .rcvRead: return "rcvRead" case .rcvRead: return "rcvRead"
case .invalid: return "invalid" case .invalid: return "invalid"
@@ -2738,7 +2752,8 @@ public enum CIStatus: Decodable {
case .badMsgHash: return ("checkmark", .red) case .badMsgHash: return ("checkmark", .red)
} }
case .sndErrorAuth: return ("multiply", .red) case .sndErrorAuth: return ("multiply", .red)
case .sndError: return ("exclamationmark.triangle.fill", .yellow) case .sndError: return ("multiply", .red)
case .sndWarning: return ("exclamationmark.triangle.fill", .orange)
case .rcvNew: return ("circlebadge.fill", Color.accentColor) case .rcvNew: return ("circlebadge.fill", Color.accentColor)
case .rcvRead: return nil case .rcvRead: return nil
case .invalid: return ("questionmark", metaColor) case .invalid: return ("questionmark", metaColor)
@@ -2756,7 +2771,11 @@ public enum CIStatus: Decodable {
) )
case let .sndError(agentError): return ( case let .sndError(agentError): return (
NSLocalizedString("Message delivery error", comment: "item status text"), NSLocalizedString("Message delivery error", comment: "item status text"),
String.localizedStringWithFormat(NSLocalizedString("Unexpected error: %@", comment: "item status description"), agentError) agentError.errorInfo
)
case let .sndWarning(agentError): return (
NSLocalizedString("Message delivery warning", comment: "item status text"),
agentError.errorInfo
) )
case .rcvNew: return nil case .rcvNew: return nil
case .rcvRead: return nil case .rcvRead: return nil
@@ -2768,6 +2787,42 @@ public enum CIStatus: Decodable {
} }
} }
public enum SndError: Decodable {
case auth
case quota
case expired
case relay(srvError: SrvError)
case proxy(proxyServer: String, srvError: SrvError)
case proxyRelay(proxyServer: String, srvError: SrvError)
case other(sndError: String)
public var errorInfo: String {
switch self {
case .auth: NSLocalizedString("Wrong key or unknown connection - most likely this connection is deleted.", comment: "snd error text")
case .quota: NSLocalizedString("Capacity exceeded - recipient did not receive previously sent messages.", comment: "snd error text")
case .expired: NSLocalizedString("Network issues - message expired after many attempts to send it.", comment: "snd error text")
case let .relay(srvError): String.localizedStringWithFormat(NSLocalizedString("Destination server error: %@", comment: "snd error text"), srvError.errorInfo)
case let .proxy(proxyServer, srvError): String.localizedStringWithFormat(NSLocalizedString("Forwarding server: %@\nError: %@", comment: "snd error text"), proxyServer, srvError.errorInfo)
case let .proxyRelay(proxyServer, srvError): String.localizedStringWithFormat(NSLocalizedString("Forwarding server: %@\nDestination server error: %@", comment: "snd error text"), proxyServer, srvError.errorInfo)
case let .other(sndError): String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "snd error text"), sndError)
}
}
}
public enum SrvError: Decodable {
case host
case version
case other(srvError: String)
public var errorInfo: String {
switch self {
case .host: NSLocalizedString("Server address is incompatible with network settings.", comment: "srv error text.")
case .version: NSLocalizedString("Server version is incompatible with network settings.", comment: "srv error text")
case let .other(srvError): srvError
}
}
}
public enum MsgReceiptStatus: String, Decodable { public enum MsgReceiptStatus: String, Decodable {
case ok case ok
case badMsgHash case badMsgHash
@@ -3119,6 +3174,7 @@ public struct CIFile: Decodable {
case .rcvInvitation: return false case .rcvInvitation: return false
case .rcvAccepted: return false case .rcvAccepted: return false
case .rcvTransfer: return false case .rcvTransfer: return false
case .rcvAborted: return false
case .rcvCancelled: return false case .rcvCancelled: return false
case .rcvComplete: return true case .rcvComplete: return true
case .rcvError: return false case .rcvError: return false
@@ -3143,6 +3199,7 @@ public struct CIFile: Decodable {
case .rcvInvitation: return nil case .rcvInvitation: return nil
case .rcvAccepted: return rcvCancelAction case .rcvAccepted: return rcvCancelAction
case .rcvTransfer: return rcvCancelAction case .rcvTransfer: return rcvCancelAction
case .rcvAborted: return nil
case .rcvCancelled: return nil case .rcvCancelled: return nil
case .rcvComplete: return nil case .rcvComplete: return nil
case .rcvError: return nil case .rcvError: return nil
@@ -3257,6 +3314,7 @@ public enum CIFileStatus: Decodable, Equatable {
case rcvInvitation case rcvInvitation
case rcvAccepted case rcvAccepted
case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64) case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64)
case rcvAborted
case rcvComplete case rcvComplete
case rcvCancelled case rcvCancelled
case rcvError case rcvError
@@ -3272,6 +3330,7 @@ public enum CIFileStatus: Decodable, Equatable {
case .rcvInvitation: return "rcvInvitation" case .rcvInvitation: return "rcvInvitation"
case .rcvAccepted: return "rcvAccepted" case .rcvAccepted: return "rcvAccepted"
case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)" case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)"
case .rcvAborted: return "rcvAborted"
case .rcvComplete: return "rcvComplete" case .rcvComplete: return "rcvComplete"
case .rcvCancelled: return "rcvCancelled" case .rcvCancelled: return "rcvCancelled"
case .rcvError: return "rcvError" case .rcvError: return "rcvError"
@@ -3886,4 +3945,5 @@ public struct ChatItemVersion: Decodable {
public struct MemberDeliveryStatus: Decodable { public struct MemberDeliveryStatus: Decodable {
public var groupMemberId: Int64 public var groupMemberId: Int64
public var memberDeliveryStatus: CIStatus public var memberDeliveryStatus: CIStatus
public var sentViaProxy: Bool?
} }
@@ -1048,9 +1048,13 @@ data class Contact(
override val apiId get() = contactId override val apiId get() = contactId
override val ready get() = activeConn?.connStatus == ConnStatus.Ready override val ready get() = activeConn?.connStatus == ConnStatus.Ready
val active get() = contactStatus == ContactStatus.Active val active get() = contactStatus == ContactStatus.Active
override val sendMsgEnabled get() = override val sendMsgEnabled get() = (
(ready && active && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?: false)) ready
|| nextSendGrpInv && active
&& !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?: false)
&& !(activeConn?.connDisabled ?: true)
)
|| nextSendGrpInv
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All
override val incognito get() = contactConnIncognito override val incognito get() = contactConnIncognito
@@ -1150,15 +1154,19 @@ data class Connection(
val pqEncryption: Boolean, val pqEncryption: Boolean,
val pqSndEnabled: Boolean? = null, val pqSndEnabled: Boolean? = null,
val pqRcvEnabled: Boolean? = null, val pqRcvEnabled: Boolean? = null,
val connectionStats: ConnectionStats? = null val connectionStats: ConnectionStats? = null,
val authErrCounter: Int
) { ) {
val id: ChatId get() = ":$connId" val id: ChatId get() = ":$connId"
val connDisabled: Boolean
get() = authErrCounter >= 10 // authErrDisableCount in core
val connPQEnabled: Boolean val connPQEnabled: Boolean
get() = pqSndEnabled == true && pqRcvEnabled == true get() = pqSndEnabled == true && pqRcvEnabled == true
companion object { companion object {
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null, pqSupport = false, pqEncryption = false) val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null, pqSupport = false, pqEncryption = false, authErrCounter = 0)
} }
} }
@@ -1900,6 +1908,7 @@ data class ChatItem (
ts: Instant = Clock.System.now(), ts: Instant = Clock.System.now(),
text: String = "hello\nthere", text: String = "hello\nthere",
status: CIStatus = CIStatus.SndNew(), status: CIStatus = CIStatus.SndNew(),
sentViaProxy: Boolean? = null,
quotedItem: CIQuote? = null, quotedItem: CIQuote? = null,
file: CIFile? = null, file: CIFile? = null,
itemForwarded: CIForwardedFrom? = null, itemForwarded: CIForwardedFrom? = null,
@@ -1911,7 +1920,7 @@ data class ChatItem (
) = ) =
ChatItem( ChatItem(
chatDir = dir, chatDir = dir,
meta = CIMeta.getSample(id, ts, text, status, itemForwarded, itemDeleted, itemEdited, itemTimed, deletable, editable), meta = CIMeta.getSample(id, ts, text, status, sentViaProxy, itemForwarded, itemDeleted, itemEdited, itemTimed, deletable, editable),
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)), content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
quotedItem = quotedItem, quotedItem = quotedItem,
reactions = listOf(), reactions = listOf(),
@@ -1993,6 +2002,7 @@ data class ChatItem (
itemTs = Clock.System.now(), itemTs = Clock.System.now(),
itemText = generalGetString(MR.strings.deleted_description), itemText = generalGetString(MR.strings.deleted_description),
itemStatus = CIStatus.RcvRead(), itemStatus = CIStatus.RcvRead(),
sentViaProxy = null,
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
updatedAt = Clock.System.now(), updatedAt = Clock.System.now(),
itemForwarded = null, itemForwarded = null,
@@ -2016,6 +2026,7 @@ data class ChatItem (
itemTs = Clock.System.now(), itemTs = Clock.System.now(),
itemText = "", itemText = "",
itemStatus = CIStatus.RcvRead(), itemStatus = CIStatus.RcvRead(),
sentViaProxy = null,
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
updatedAt = Clock.System.now(), updatedAt = Clock.System.now(),
itemForwarded = null, itemForwarded = null,
@@ -2118,6 +2129,7 @@ data class CIMeta (
val itemTs: Instant, val itemTs: Instant,
val itemText: String, val itemText: String,
val itemStatus: CIStatus, val itemStatus: CIStatus,
val sentViaProxy: Boolean?,
val createdAt: Instant, val createdAt: Instant,
val updatedAt: Instant, val updatedAt: Instant,
val itemForwarded: CIForwardedFrom?, val itemForwarded: CIForwardedFrom?,
@@ -2144,7 +2156,7 @@ data class CIMeta (
companion object { companion object {
fun getSample( fun getSample(
id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(), id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(), sentViaProxy: Boolean? = null,
itemForwarded: CIForwardedFrom? = null, itemDeleted: CIDeleted? = null, itemEdited: Boolean = false, itemForwarded: CIForwardedFrom? = null, itemDeleted: CIDeleted? = null, itemEdited: Boolean = false,
itemTimed: CITimed? = null, itemLive: Boolean = false, deletable: Boolean = true, editable: Boolean = true itemTimed: CITimed? = null, itemLive: Boolean = false, deletable: Boolean = true, editable: Boolean = true
): CIMeta = ): CIMeta =
@@ -2153,6 +2165,7 @@ data class CIMeta (
itemTs = ts, itemTs = ts,
itemText = text, itemText = text,
itemStatus = status, itemStatus = status,
sentViaProxy = sentViaProxy,
createdAt = ts, createdAt = ts,
updatedAt = ts, updatedAt = ts,
itemForwarded = itemForwarded, itemForwarded = itemForwarded,
@@ -2171,6 +2184,7 @@ data class CIMeta (
itemTs = Clock.System.now(), itemTs = Clock.System.now(),
itemText = "invalid JSON", itemText = "invalid JSON",
itemStatus = CIStatus.SndNew(), itemStatus = CIStatus.SndNew(),
sentViaProxy = null,
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
updatedAt = Clock.System.now(), updatedAt = Clock.System.now(),
itemForwarded = null, itemForwarded = null,
@@ -2227,7 +2241,8 @@ sealed class CIStatus {
@Serializable @SerialName("sndSent") class SndSent(val sndProgress: SndCIStatusProgress): CIStatus() @Serializable @SerialName("sndSent") class SndSent(val sndProgress: SndCIStatusProgress): CIStatus()
@Serializable @SerialName("sndRcvd") class SndRcvd(val msgRcptStatus: MsgReceiptStatus, val sndProgress: SndCIStatusProgress): CIStatus() @Serializable @SerialName("sndRcvd") class SndRcvd(val msgRcptStatus: MsgReceiptStatus, val sndProgress: SndCIStatusProgress): CIStatus()
@Serializable @SerialName("sndErrorAuth") class SndErrorAuth: CIStatus() @Serializable @SerialName("sndErrorAuth") class SndErrorAuth: CIStatus()
@Serializable @SerialName("sndError") class SndError(val agentError: String): CIStatus() @Serializable @SerialName("sndError") class CISSndError(val agentError: SndError): CIStatus()
@Serializable @SerialName("sndWarning") class SndWarning(val agentError: SndError): CIStatus()
@Serializable @SerialName("rcvNew") class RcvNew: CIStatus() @Serializable @SerialName("rcvNew") class RcvNew: CIStatus()
@Serializable @SerialName("rcvRead") class RcvRead: CIStatus() @Serializable @SerialName("rcvRead") class RcvRead: CIStatus()
@Serializable @SerialName("invalid") class Invalid(val text: String): CIStatus() @Serializable @SerialName("invalid") class Invalid(val text: String): CIStatus()
@@ -2251,7 +2266,8 @@ sealed class CIStatus {
MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red
} }
is SndErrorAuth -> MR.images.ic_close to Color.Red is SndErrorAuth -> MR.images.ic_close to Color.Red
is SndError -> MR.images.ic_warning_filled to WarningYellow is CISSndError -> MR.images.ic_close to Color.Red
is SndWarning -> MR.images.ic_warning_filled to WarningOrange
is RcvNew -> MR.images.ic_circle_filled to primaryColor is RcvNew -> MR.images.ic_circle_filled to primaryColor
is RcvRead -> null is RcvRead -> null
is CIStatus.Invalid -> MR.images.ic_question_mark to metaColor is CIStatus.Invalid -> MR.images.ic_question_mark to metaColor
@@ -2262,13 +2278,48 @@ sealed class CIStatus {
is SndSent -> null is SndSent -> null
is SndRcvd -> null is SndRcvd -> null
is SndErrorAuth -> generalGetString(MR.strings.message_delivery_error_title) to generalGetString(MR.strings.message_delivery_error_desc) is SndErrorAuth -> generalGetString(MR.strings.message_delivery_error_title) to generalGetString(MR.strings.message_delivery_error_desc)
is SndError -> generalGetString(MR.strings.message_delivery_error_title) to (generalGetString(MR.strings.unknown_error) + ": $agentError") is CISSndError -> generalGetString(MR.strings.message_delivery_error_title) to agentError.errorInfo
is SndWarning -> generalGetString(MR.strings.message_delivery_warning_title) to agentError.errorInfo
is RcvNew -> null is RcvNew -> null
is RcvRead -> null is RcvRead -> null
is Invalid -> "Invalid status" to this.text is Invalid -> "Invalid status" to this.text
} }
} }
@Serializable
sealed class SndError {
@Serializable @SerialName("auth") class Auth: SndError()
@Serializable @SerialName("quota") class Quota: SndError()
@Serializable @SerialName("expired") class Expired: SndError()
@Serializable @SerialName("relay") class Relay(val srvError: SrvError): SndError()
@Serializable @SerialName("proxy") class Proxy(val proxyServer: String, val srvError: SrvError): SndError()
@Serializable @SerialName("proxyRelay") class ProxyRelay(val proxyServer: String, val srvError: SrvError): SndError()
@Serializable @SerialName("other") class Other(val sndError: String): SndError()
val errorInfo: String get() = when (this) {
is SndError.Auth -> generalGetString(MR.strings.snd_error_auth)
is SndError.Quota -> generalGetString(MR.strings.snd_error_quota)
is SndError.Expired -> generalGetString(MR.strings.snd_error_expired)
is SndError.Relay -> generalGetString(MR.strings.snd_error_relay).format(srvError.errorInfo)
is SndError.Proxy -> generalGetString(MR.strings.snd_error_proxy).format(proxyServer, srvError.errorInfo)
is SndError.ProxyRelay -> generalGetString(MR.strings.snd_error_proxy_relay).format(proxyServer, srvError.errorInfo)
is SndError.Other -> generalGetString(MR.strings.ci_status_other_error).format(sndError)
}
}
@Serializable
sealed class SrvError {
@Serializable @SerialName("host") class Host: SrvError()
@Serializable @SerialName("version") class Version: SrvError()
@Serializable @SerialName("other") class Other(val srvError: String): SrvError()
val errorInfo: String get() = when (this) {
is SrvError.Host -> generalGetString(MR.strings.srv_error_host)
is SrvError.Version -> generalGetString(MR.strings.srv_error_version)
is SrvError.Other -> srvError
}
}
@Serializable @Serializable
enum class MsgReceiptStatus { enum class MsgReceiptStatus {
@SerialName("ok") Ok, @SerialName("ok") Ok,
@@ -2593,6 +2644,7 @@ data class CIFile(
is CIFileStatus.RcvInvitation -> false is CIFileStatus.RcvInvitation -> false
is CIFileStatus.RcvAccepted -> false is CIFileStatus.RcvAccepted -> false
is CIFileStatus.RcvTransfer -> false is CIFileStatus.RcvTransfer -> false
is CIFileStatus.RcvAborted -> false
is CIFileStatus.RcvCancelled -> false is CIFileStatus.RcvCancelled -> false
is CIFileStatus.RcvComplete -> true is CIFileStatus.RcvComplete -> true
is CIFileStatus.RcvError -> false is CIFileStatus.RcvError -> false
@@ -2614,6 +2666,7 @@ data class CIFile(
is CIFileStatus.RcvInvitation -> null is CIFileStatus.RcvInvitation -> null
is CIFileStatus.RcvAccepted -> rcvCancelAction is CIFileStatus.RcvAccepted -> rcvCancelAction
is CIFileStatus.RcvTransfer -> rcvCancelAction is CIFileStatus.RcvTransfer -> rcvCancelAction
is CIFileStatus.RcvAborted -> null
is CIFileStatus.RcvCancelled -> null is CIFileStatus.RcvCancelled -> null
is CIFileStatus.RcvComplete -> null is CIFileStatus.RcvComplete -> null
is CIFileStatus.RcvError -> null is CIFileStatus.RcvError -> null
@@ -2653,6 +2706,12 @@ data class CIFile(
return res return res
} }
fun forwardingAllowed(): Boolean = when {
chatModel.connectedToRemote() && cachedRemoteFileRequests[fileSource] != false && loaded -> true
getLoadedFilePath(this) != null -> true
else -> false
}
companion object { companion object {
fun getSample( fun getSample(
fileId: Long = 1, fileId: Long = 1,
@@ -2788,6 +2847,7 @@ sealed class CIFileStatus {
@Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus() @Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus() @Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
@Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus() @Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
@Serializable @SerialName("rcvAborted") object RcvAborted: CIFileStatus()
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus() @Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus() @Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus() @Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
@@ -2802,6 +2862,7 @@ sealed class CIFileStatus {
is RcvInvitation -> false is RcvInvitation -> false
is RcvAccepted -> false is RcvAccepted -> false
is RcvTransfer -> false is RcvTransfer -> false
is RcvAborted -> false
is RcvComplete -> false is RcvComplete -> false
is RcvCancelled -> false is RcvCancelled -> false
is RcvError -> false is RcvError -> false
@@ -3348,7 +3409,8 @@ data class ChatItemVersion(
@Serializable @Serializable
data class MemberDeliveryStatus( data class MemberDeliveryStatus(
val groupMemberId: Long, val groupMemberId: Long,
val memberDeliveryStatus: CIStatus val memberDeliveryStatus: CIStatus,
val sentViaProxy: Boolean?
) )
enum class NotificationPreviewMode { enum class NotificationPreviewMode {
@@ -12,6 +12,7 @@ import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.group.toggleShowMemberMessages
import chat.simplex.common.views.migration.MigrationFileLinkData import chat.simplex.common.views.migration.MigrationFileLinkData
import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
@@ -106,6 +107,7 @@ class AppPreferences {
val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true) val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true)
val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false)
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true)
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
@@ -130,6 +132,8 @@ class AppPreferences {
}, },
set = fun(mode: TransportSessionMode) { _networkSessionMode.set(mode.name) } set = fun(mode: TransportSessionMode) { _networkSessionMode.set(mode.name) }
) )
val networkSMPProxyMode = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_MODE, SMPProxyMode.Never.name)
val networkSMPProxyFallback = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK, SMPProxyFallback.Allow.name)
val networkHostMode = mkStrPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.OnionViaSocks.name) val networkHostMode = mkStrPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.OnionViaSocks.name)
val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false) val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false)
val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout) val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout)
@@ -185,6 +189,8 @@ class AppPreferences {
val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null) val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null)
val showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false)
val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true) val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true)
val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false) val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false)
@@ -288,6 +294,7 @@ class AppPreferences {
private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft"
private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet" private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet"
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays"
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
@@ -306,6 +313,8 @@ class AppPreferences {
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort" private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort"
private const val SHARED_PREFS_NETWORK_SESSION_MODE = "NetworkSessionMode" private const val SHARED_PREFS_NETWORK_SESSION_MODE = "NetworkSessionMode"
private const val SHARED_PREFS_NETWORK_SMP_PROXY_MODE = "NetworkSMPProxyMode"
private const val SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK = "NetworkSMPProxyFallback"
private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode" private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode"
private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode" private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode"
private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout"
@@ -348,6 +357,7 @@ class AppPreferences {
private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto" private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto"
private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast" private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast"
private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState" private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState"
private const val SHARED_PREFS_SHOW_SENT_VIA_RPOXY = "showSentViaProxy"
private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled" private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled"
private const val SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS = "iOSCallKitCallsInRecents" private const val SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS = "iOSCallKitCallsInRecents"
@@ -775,8 +785,8 @@ object ChatController {
} }
} }
suspend fun apiForwardChatItem(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemId: Long): ChatItem? { suspend fun apiForwardChatItem(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemId: Long, ttl: Int?): ChatItem? {
val cmd = CC.ApiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId) val cmd = CC.ApiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId, ttl)
return processSendMessageCmd(rh, cmd)?.chatItem return processSendMessageCmd(rh, cmd)?.chatItem
} }
@@ -1330,9 +1340,9 @@ object ChatController {
} }
} }
suspend fun apiReceiveFile(rh: Long?, fileId: Long, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? { suspend fun apiReceiveFile(rh: Long?, fileId: Long, userApprovedRelays: Boolean, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
// -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected // -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected
val r = sendCmd(rh, CC.ReceiveFile(fileId, encrypted, inline)) val r = sendCmd(rh, CC.ReceiveFile(fileId, userApprovedRelays = userApprovedRelays, encrypt = encrypted, inline = inline))
return when (r) { return when (r) {
is CR.RcvFileAccepted -> r.chatItem is CR.RcvFileAccepted -> r.chatItem
is CR.RcvFileAcceptedSndCancelled -> { is CR.RcvFileAcceptedSndCancelled -> {
@@ -1351,7 +1361,23 @@ object ChatController {
val maybeChatError = chatError(r) val maybeChatError = chatError(r)
if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) { if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) {
Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error") Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error")
} else { } else if (maybeChatError is ChatErrorType.FileNotApproved) {
Log.d(TAG, "apiReceiveFile FileNotApproved error")
if (!auto) {
val srvs = maybeChatError.unknownServers.map{ serverHostname(it) }
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.file_not_approved_title),
text = generalGetString(MR.strings.file_not_approved_descr).format(srvs.sorted().joinToString(separator = ", ")),
confirmText = generalGetString(MR.strings.download_file),
onConfirm = {
val user = chatModel.currentUser.value
if (user != null) {
withBGApi { chatModel.controller.receiveFile(rh, user, fileId, userApprovedRelays = true) }
}
},
)
}
} else if (!auto) {
apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r) apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r)
} }
} }
@@ -2031,13 +2057,21 @@ object ChatController {
} }
} }
is CR.ContactSwitch -> is CR.ContactSwitch ->
chatModel.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats) if (active(r.user)) {
chatModel.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats)
}
is CR.GroupMemberSwitch -> is CR.GroupMemberSwitch ->
chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats) if (active(r.user)) {
chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats)
}
is CR.ContactRatchetSync -> is CR.ContactRatchetSync ->
chatModel.updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats) if (active(r.user)) {
chatModel.updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats)
}
is CR.GroupMemberRatchetSync -> is CR.GroupMemberRatchetSync ->
chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats) if (active(r.user)) {
chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats)
}
is CR.RemoteHostSessionCode -> { is CR.RemoteHostSessionCode -> {
chatModel.remoteHostPairing.value = r.remoteHost_ to RemoteHostSessionState.PendingConfirmation(r.sessionCode) chatModel.remoteHostPairing.value = r.remoteHost_ to RemoteHostSessionState.PendingConfirmation(r.sessionCode)
} }
@@ -2046,6 +2080,11 @@ object ChatController {
chatModel.currentRemoteHost.value = r.remoteHost chatModel.currentRemoteHost.value = r.remoteHost
switchUIRemoteHost(r.remoteHost.remoteHostId) switchUIRemoteHost(r.remoteHost.remoteHostId)
} }
is CR.ContactDisabled -> {
if (active(r.user)) {
chatModel.updateContact(rhId, r.contact)
}
}
is CR.RemoteHostStopped -> { is CR.RemoteHostStopped -> {
val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ } val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ }
chatModel.remoteHostPairing.value = null chatModel.remoteHostPairing.value = null
@@ -2196,9 +2235,14 @@ object ChatController {
} }
} }
suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, auto: Boolean = false) { suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) {
val encrypted = appPrefs.privacyEncryptLocalFiles.get() val chatItem = apiReceiveFile(
val chatItem = apiReceiveFile(rhId, fileId, encrypted = encrypted, auto = auto) rhId,
fileId,
userApprovedRelays = userApprovedRelays || !appPrefs.privacyAskToApproveRelays.get(),
encrypted = appPrefs.privacyEncryptLocalFiles.get(),
auto = auto
)
if (chatItem != null) { if (chatItem != null) {
chatItemSimpleUpdate(rhId, user, chatItem) chatItemSimpleUpdate(rhId, user, chatItem)
} }
@@ -2302,6 +2346,8 @@ object ChatController {
val hostMode = HostMode.valueOf(appPrefs.networkHostMode.get()!!) val hostMode = HostMode.valueOf(appPrefs.networkHostMode.get()!!)
val requiredHostMode = appPrefs.networkRequiredHostMode.get() val requiredHostMode = appPrefs.networkRequiredHostMode.get()
val sessionMode = appPrefs.networkSessionMode.get() val sessionMode = appPrefs.networkSessionMode.get()
val smpProxyMode = SMPProxyMode.valueOf(appPrefs.networkSMPProxyMode.get()!!)
val smpProxyFallback = SMPProxyFallback.valueOf(appPrefs.networkSMPProxyFallback.get()!!)
val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get() val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get()
val tcpTimeout = appPrefs.networkTCPTimeout.get() val tcpTimeout = appPrefs.networkTCPTimeout.get()
val tcpTimeoutPerKb = appPrefs.networkTCPTimeoutPerKb.get() val tcpTimeoutPerKb = appPrefs.networkTCPTimeoutPerKb.get()
@@ -2322,6 +2368,8 @@ object ChatController {
hostMode = hostMode, hostMode = hostMode,
requiredHostMode = requiredHostMode, requiredHostMode = requiredHostMode,
sessionMode = sessionMode, sessionMode = sessionMode,
smpProxyMode = smpProxyMode,
smpProxyFallback = smpProxyFallback,
tcpConnectTimeout = tcpConnectTimeout, tcpConnectTimeout = tcpConnectTimeout,
tcpTimeout = tcpTimeout, tcpTimeout = tcpTimeout,
tcpTimeoutPerKb = tcpTimeoutPerKb, tcpTimeoutPerKb = tcpTimeoutPerKb,
@@ -2340,6 +2388,8 @@ object ChatController {
appPrefs.networkHostMode.set(cfg.hostMode.name) appPrefs.networkHostMode.set(cfg.hostMode.name)
appPrefs.networkRequiredHostMode.set(cfg.requiredHostMode) appPrefs.networkRequiredHostMode.set(cfg.requiredHostMode)
appPrefs.networkSessionMode.set(cfg.sessionMode) appPrefs.networkSessionMode.set(cfg.sessionMode)
appPrefs.networkSMPProxyMode.set(cfg.smpProxyMode.name)
appPrefs.networkSMPProxyFallback.set(cfg.smpProxyFallback.name)
appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout) appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout)
appPrefs.networkTCPTimeout.set(cfg.tcpTimeout) appPrefs.networkTCPTimeout.set(cfg.tcpTimeout)
appPrefs.networkTCPTimeoutPerKb.set(cfg.tcpTimeoutPerKb) appPrefs.networkTCPTimeoutPerKb.set(cfg.tcpTimeoutPerKb)
@@ -2407,7 +2457,7 @@ sealed class CC {
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC() class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC() class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC()
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC() class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
class ApiForwardChatItem(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemId: Long): CC() class ApiForwardChatItem(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemId: Long, val ttl: Int?): CC()
class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC() class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC()
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC() class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
class ApiJoinGroup(val groupId: Long): CC() class ApiJoinGroup(val groupId: Long): CC()
@@ -2475,7 +2525,7 @@ sealed class CC {
class ApiRejectContact(val contactReqId: Long): CC() class ApiRejectContact(val contactReqId: Long): CC()
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC() class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
class ReceiveFile(val fileId: Long, val encrypt: Boolean, val inline: Boolean?): CC() class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC()
class CancelFile(val fileId: Long): CC() class CancelFile(val fileId: Long): CC()
// Remote control // Remote control
class SetLocalDeviceName(val displayName: String): CC() class SetLocalDeviceName(val displayName: String): CC()
@@ -2549,7 +2599,10 @@ sealed class CC {
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}" is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId" is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId"
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}" is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
is ApiForwardChatItem -> "/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} $itemId" is ApiForwardChatItem -> {
val ttlStr = if (ttl != null) "$ttl" else "default"
"/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} $itemId ttl=${ttlStr}"
}
is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}" is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}"
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}" is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
is ApiJoinGroup -> "/_join #$groupId" is ApiJoinGroup -> "/_join #$groupId"
@@ -2623,6 +2676,7 @@ sealed class CC {
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
is ReceiveFile -> is ReceiveFile ->
"/freceive $fileId" + "/freceive $fileId" +
(" approved_relays=${onOff(userApprovedRelays)}") +
(if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") + (if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") +
(if (inline == null) "" else " inline=${onOff(inline)}") (if (inline == null) "" else " inline=${onOff(inline)}")
is CancelFile -> "/fcancel $fileId" is CancelFile -> "/fcancel $fileId"
@@ -3025,9 +3079,12 @@ data class ParsedServerAddress (
@Serializable @Serializable
data class NetCfg( data class NetCfg(
val socksProxy: String?, val socksProxy: String?,
val socksMode: SocksMode = SocksMode.Always,
val hostMode: HostMode, val hostMode: HostMode,
val requiredHostMode: Boolean, val requiredHostMode: Boolean,
val sessionMode: TransportSessionMode, val sessionMode: TransportSessionMode,
val smpProxyMode: SMPProxyMode,
val smpProxyFallback: SMPProxyFallback,
val tcpConnectTimeout: Long, // microseconds val tcpConnectTimeout: Long, // microseconds
val tcpTimeout: Long, // microseconds val tcpTimeout: Long, // microseconds
val tcpTimeoutPerKb: Long, // microseconds val tcpTimeoutPerKb: Long, // microseconds
@@ -3056,6 +3113,8 @@ data class NetCfg(
hostMode = HostMode.OnionViaSocks, hostMode = HostMode.OnionViaSocks,
requiredHostMode = false, requiredHostMode = false,
sessionMode = TransportSessionMode.User, sessionMode = TransportSessionMode.User,
smpProxyMode = SMPProxyMode.Never,
smpProxyFallback = SMPProxyFallback.Allow,
tcpConnectTimeout = 25_000_000, tcpConnectTimeout = 25_000_000,
tcpTimeout = 15_000_000, tcpTimeout = 15_000_000,
tcpTimeoutPerKb = 10_000, tcpTimeoutPerKb = 10_000,
@@ -3071,6 +3130,8 @@ data class NetCfg(
hostMode = HostMode.OnionViaSocks, hostMode = HostMode.OnionViaSocks,
requiredHostMode = false, requiredHostMode = false,
sessionMode = TransportSessionMode.User, sessionMode = TransportSessionMode.User,
smpProxyMode = SMPProxyMode.Never,
smpProxyFallback = SMPProxyFallback.Allow,
tcpConnectTimeout = 35_000_000, tcpConnectTimeout = 35_000_000,
tcpTimeout = 20_000_000, tcpTimeout = 20_000_000,
tcpTimeoutPerKb = 15_000, tcpTimeoutPerKb = 15_000,
@@ -3109,6 +3170,35 @@ enum class HostMode {
@SerialName("public") Public; @SerialName("public") Public;
} }
@Serializable
enum class SocksMode {
@SerialName("always") Always,
@SerialName("onion") Onion;
}
@Serializable
enum class SMPProxyMode {
@SerialName("always") Always,
@SerialName("unknown") Unknown,
@SerialName("unprotected") Unprotected,
@SerialName("never") Never;
companion object {
val default = Never
}
}
@Serializable
enum class SMPProxyFallback {
@SerialName("allow") Allow,
@SerialName("allowProtected") AllowProtected,
@SerialName("prohibit") Prohibit;
companion object {
val default = Allow
}
}
@Serializable @Serializable
enum class TransportSessionMode { enum class TransportSessionMode {
@SerialName("user") User, @SerialName("user") User,
@@ -4197,6 +4287,7 @@ sealed class CR {
@Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR() @Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR()
@Serializable @SerialName("callEnded") class CallEnded(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("callEnded") class CallEnded(val user: UserRef, val contact: Contact): CR()
@Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: UserRef, val connection: PendingContactConnection): CR() @Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: UserRef, val connection: PendingContactConnection): CR()
@Serializable @SerialName("contactDisabled") class ContactDisabled(val user: UserRef, val contact: Contact): CR()
// remote events (desktop) // remote events (desktop)
@Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR() @Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List<RemoteHostInfo>): CR()
@Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): CR() @Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): CR()
@@ -4361,6 +4452,7 @@ sealed class CR {
is CallExtraInfo -> "callExtraInfo" is CallExtraInfo -> "callExtraInfo"
is CallEnded -> "callEnded" is CallEnded -> "callEnded"
is ContactConnectionDeleted -> "contactConnectionDeleted" is ContactConnectionDeleted -> "contactConnectionDeleted"
is ContactDisabled -> "contactDisabled"
is RemoteHostList -> "remoteHostList" is RemoteHostList -> "remoteHostList"
is CurrentRemoteHost -> "currentRemoteHost" is CurrentRemoteHost -> "currentRemoteHost"
is RemoteHostStarted -> "remoteHostStarted" is RemoteHostStarted -> "remoteHostStarted"
@@ -4521,6 +4613,7 @@ sealed class CR {
is CallExtraInfo -> withUser(user, "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}") is CallExtraInfo -> withUser(user, "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}")
is CallEnded -> withUser(user, "contact: ${contact.id}") is CallEnded -> withUser(user, "contact: ${contact.id}")
is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection)) is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection))
is ContactDisabled -> withUser(user, json.encodeToString(contact))
// remote events (mobile) // remote events (mobile)
is RemoteHostList -> json.encodeToString(remoteHosts) is RemoteHostList -> json.encodeToString(remoteHosts)
is CurrentRemoteHost -> if (remoteHost_ == null) "local" else json.encodeToString(remoteHost_) is CurrentRemoteHost -> if (remoteHost_ == null) "local" else json.encodeToString(remoteHost_)
@@ -4824,13 +4917,14 @@ sealed class ChatErrorType {
is FileCancel -> "fileCancel" is FileCancel -> "fileCancel"
is FileAlreadyExists -> "fileAlreadyExists" is FileAlreadyExists -> "fileAlreadyExists"
is FileRead -> "fileRead" is FileRead -> "fileRead"
is FileWrite -> "fileWrite" is FileWrite -> "fileWrite $message"
is FileSend -> "fileSend" is FileSend -> "fileSend"
is FileRcvChunk -> "fileRcvChunk" is FileRcvChunk -> "fileRcvChunk"
is FileInternal -> "fileInternal" is FileInternal -> "fileInternal"
is FileImageType -> "fileImageType" is FileImageType -> "fileImageType"
is FileImageSize -> "fileImageSize" is FileImageSize -> "fileImageSize"
is FileNotReceived -> "fileNotReceived" is FileNotReceived -> "fileNotReceived"
is FileNotApproved -> "fileNotApproved"
// is XFTPRcvFile -> "xftpRcvFile" // is XFTPRcvFile -> "xftpRcvFile"
// is XFTPSndFile -> "xftpSndFile" // is XFTPSndFile -> "xftpSndFile"
is FallbackToSMPProhibited -> "fallbackToSMPProhibited" is FallbackToSMPProhibited -> "fallbackToSMPProhibited"
@@ -4910,6 +5004,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("fileImageType") class FileImageType(val filePath: String): ChatErrorType() @Serializable @SerialName("fileImageType") class FileImageType(val filePath: String): ChatErrorType()
@Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType() @Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType()
@Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType() @Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType()
@Serializable @SerialName("fileNotApproved") class FileNotApproved(val fileId: Long, val unknownServers: List<String>): ChatErrorType()
// @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType() // @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType()
// @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType() // @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType()
@Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType() @Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType()
@@ -5408,6 +5503,7 @@ enum class NotificationsMode() {
data class AppSettings( data class AppSettings(
var networkConfig: NetCfg? = null, var networkConfig: NetCfg? = null,
var privacyEncryptLocalFiles: Boolean? = null, var privacyEncryptLocalFiles: Boolean? = null,
var privacyAskToApproveRelays: Boolean? = null,
var privacyAcceptImages: Boolean? = null, var privacyAcceptImages: Boolean? = null,
var privacyLinkPreviews: Boolean? = null, var privacyLinkPreviews: Boolean? = null,
var privacyShowChatPreviews: Boolean? = null, var privacyShowChatPreviews: Boolean? = null,
@@ -5431,6 +5527,7 @@ data class AppSettings(
val def = defaults val def = defaults
if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig } if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig }
if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages } if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages }
if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews } if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews }
if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews } if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews }
@@ -5462,6 +5559,7 @@ data class AppSettings(
setNetCfg(net) setNetCfg(net)
} }
privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) } privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) }
privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) }
privacyAcceptImages?.let { def.privacyAcceptImages.set(it) } privacyAcceptImages?.let { def.privacyAcceptImages.set(it) }
privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) } privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) }
privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) } privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) }
@@ -5486,6 +5584,7 @@ data class AppSettings(
get() = AppSettings( get() = AppSettings(
networkConfig = NetCfg.defaults, networkConfig = NetCfg.defaults,
privacyEncryptLocalFiles = true, privacyEncryptLocalFiles = true,
privacyAskToApproveRelays = true,
privacyAcceptImages = true, privacyAcceptImages = true,
privacyLinkPreviews = true, privacyLinkPreviews = true,
privacyShowChatPreviews = true, privacyShowChatPreviews = true,
@@ -5511,6 +5610,7 @@ data class AppSettings(
return defaults.copy( return defaults.copy(
networkConfig = getNetCfg(), networkConfig = getNetCfg(),
privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(), privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(),
privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(),
privacyAcceptImages = def.privacyAcceptImages.get(), privacyAcceptImages = def.privacyAcceptImages.get(),
privacyLinkPreviews = def.privacyLinkPreviews.get(), privacyLinkPreviews = def.privacyLinkPreviews.get(),
privacyShowChatPreviews = def.privacyShowChatPreviews.get(), privacyShowChatPreviews = def.privacyShowChatPreviews.get(),
@@ -304,7 +304,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
} }
@Composable @Composable
fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus) { fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus, sentViaProxy: Boolean?) {
SectionItemView( SectionItemView(
padding = PaddingValues(horizontal = 0.dp) padding = PaddingValues(horizontal = 0.dp)
) { ) {
@@ -317,6 +317,18 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Spacer(Modifier.fillMaxWidth().weight(1f)) Spacer(Modifier.fillMaxWidth().weight(1f))
if (sentViaProxy == true) {
Box(
Modifier.size(36.dp),
contentAlignment = Alignment.Center
) {
Icon(
painterResource(MR.images.ic_arrow_forward),
contentDescription = null,
tint = CurrentColors.value.colors.secondary
)
}
}
val statusIcon = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary) val statusIcon = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary)
var modifier = Modifier.size(36.dp).clip(RoundedCornerShape(20.dp)) var modifier = Modifier.size(36.dp).clip(RoundedCornerShape(20.dp))
val info = status.statusInto val info = status.statusInto
@@ -357,8 +369,8 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
if (mss.isNotEmpty()) { if (mss.isNotEmpty()) {
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
mss.forEach { (member, status) -> mss.forEach { (member, status, sentViaProxy) ->
MemberDeliveryStatusView(member, status) MemberDeliveryStatusView(member, status, sentViaProxy)
} }
} }
} else { } else {
@@ -482,10 +494,10 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
} }
} }
private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Pair<GroupMember, CIStatus>> { private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Triple<GroupMember, CIStatus, Boolean?>> {
return memberDeliveryStatuses.mapNotNull { mds -> return memberDeliveryStatuses.mapNotNull { mds ->
chatModel.getGroupMember(mds.groupMemberId)?.let { mem -> chatModel.getGroupMember(mds.groupMemberId)?.let { mem ->
mem to mds.memberDeliveryStatus Triple(mem, mds.memberDeliveryStatus, mds.sentViaProxy)
} }
} }
} }
@@ -479,6 +479,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}, },
onComposed, onComposed,
developerTools = chatModel.controller.appPrefs.developerTools.get(), developerTools = chatModel.controller.appPrefs.developerTools.get(),
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
) )
} }
is ChatInfo.ContactConnection -> { is ChatInfo.ContactConnection -> {
@@ -548,6 +549,7 @@ fun ChatLayout(
onSearchValueChanged: (String) -> Unit, onSearchValueChanged: (String) -> Unit,
onComposed: suspend (chatId: String) -> Unit, onComposed: suspend (chatId: String) -> Unit,
developerTools: Boolean, developerTools: Boolean,
showViaProxy: Boolean
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } } val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } }
@@ -606,7 +608,7 @@ fun ChatLayout(
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy,
) )
} }
} }
@@ -885,6 +887,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
setFloatingButton: (@Composable () -> Unit) -> Unit, setFloatingButton: (@Composable () -> Unit) -> Unit,
onComposed: suspend (chatId: String) -> Unit, onComposed: suspend (chatId: String) -> Unit,
developerTools: Boolean, developerTools: Boolean,
showViaProxy: Boolean
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -975,7 +978,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
tryOrShowError("${cItem.id}ChatItem", error = { tryOrShowError("${cItem.id}ChatItem", error = {
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
}) { }) {
ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools) ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
} }
} }
@@ -1543,6 +1546,7 @@ fun PreviewChatLayout() {
onSearchValueChanged = {}, onSearchValueChanged = {},
onComposed = {}, onComposed = {},
developerTools = false, developerTools = false,
showViaProxy = false,
) )
} }
} }
@@ -1615,6 +1619,7 @@ fun PreviewGroupChatLayout() {
onSearchValueChanged = {}, onSearchValueChanged = {},
onComposed = {}, onComposed = {},
developerTools = false, developerTools = false,
showViaProxy = false,
) )
} }
} }
@@ -408,14 +408,15 @@ fun ComposeView(
composeState.value = composeState.value.copy(inProgress = true) composeState.value = composeState.value.copy(inProgress = true)
} }
suspend fun forwardItem(rhId: Long?, forwardedItem: ChatItem, fromChatInfo: ChatInfo): ChatItem? { suspend fun forwardItem(rhId: Long?, forwardedItem: ChatItem, fromChatInfo: ChatInfo, ttl: Int?): ChatItem? {
val chatItem = controller.apiForwardChatItem( val chatItem = controller.apiForwardChatItem(
rh = rhId, rh = rhId,
toChatType = chat.chatInfo.chatType, toChatType = chat.chatInfo.chatType,
toChatId = chat.chatInfo.apiId, toChatId = chat.chatInfo.apiId,
fromChatType = fromChatInfo.chatType, fromChatType = fromChatInfo.chatType,
fromChatId = fromChatInfo.apiId, fromChatId = fromChatInfo.apiId,
itemId = forwardedItem.id itemId = forwardedItem.id,
ttl = ttl
) )
if (chatItem != null) { if (chatItem != null) {
chatModel.addChatItem(rhId, chat.chatInfo, chatItem) chatModel.addChatItem(rhId, chat.chatInfo, chatItem)
@@ -490,9 +491,9 @@ fun ComposeView(
sendMemberContactInvitation() sendMemberContactInvitation()
sent = null sent = null
} else if (cs.contextItem is ComposeContextItem.ForwardingItem) { } else if (cs.contextItem is ComposeContextItem.ForwardingItem) {
sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItem, cs.contextItem.fromChatInfo) sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItem, cs.contextItem.fromChatInfo, ttl = ttl)
if (cs.message.isNotEmpty()) { if (cs.message.isNotEmpty()) {
sent = send(chat, checkLinkPreview(), quoted = sent?.id, live = false, ttl = null) sent = send(chat, checkLinkPreview(), quoted = sent?.id, live = false, ttl = ttl)
} }
} else if (cs.contextItem is ComposeContextItem.EditingItem) { } else if (cs.contextItem is ComposeContextItem.EditingItem) {
val ei = cs.contextItem.chatItem val ei = cs.contextItem.chatItem
@@ -157,7 +157,7 @@ fun SendMsgView(
fun MenuItems(): List<@Composable () -> Unit> { fun MenuItems(): List<@Composable () -> Unit> {
val menuItems = mutableListOf<@Composable () -> Unit>() val menuItems = mutableListOf<@Composable () -> Unit>()
if (cs.liveMessage == null && !cs.editing && !cs.forwarding && !nextSendGrpInv || sendMsgEnabled) { if (cs.liveMessage == null && !cs.editing && !nextSendGrpInv || sendMsgEnabled) {
if ( if (
cs.preview !is ComposePreview.VoicePreview && cs.preview !is ComposePreview.VoicePreview &&
cs.contextItem is ComposeContextItem.NoContextItem && cs.contextItem is ComposeContextItem.NoContextItem &&
@@ -47,7 +47,7 @@ fun CICallItemView(
CICallStatus.Error -> {} CICallStatus.Error -> {}
} }
CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false) CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false)
} }
} }
@@ -1,6 +1,6 @@
package chat.simplex.common.views.chat.item package chat.simplex.common.views.chat.item
import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -28,6 +28,7 @@ import java.net.URI
fun CIFileView( fun CIFileView(
file: CIFile?, file: CIFile?,
edited: Boolean, edited: Boolean,
showMenu: MutableState<Boolean>,
receiveFile: (Long) -> Unit receiveFile: (Long) -> Unit
) { ) {
val saveFileLauncher = rememberSaveFileLauncher(ciFile = file) val saveFileLauncher = rememberSaveFileLauncher(ciFile = file)
@@ -62,7 +63,7 @@ fun CIFileView(
fun fileAction() { fun fileAction() {
if (file != null) { if (file != null) {
when { when {
file.fileStatus is CIFileStatus.RcvInvitation -> { file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> {
if (fileSizeValid(file)) { if (fileSizeValid(file)) {
receiveFile(file.fileId) receiveFile(file.fileId)
} else { } else {
@@ -86,7 +87,7 @@ fun CIFileView(
) )
FileProtocol.LOCAL -> {} FileProtocol.LOCAL -> {}
} }
file.fileStatus is CIFileStatus.RcvComplete || (file.fileStatus is CIFileStatus.SndStored && file.fileProtocol == FileProtocol.LOCAL) -> { file.forwardingAllowed() -> {
withLongRunningApi(slow = 600_000) { withLongRunningApi(slow = 600_000) {
var filePath = getLoadedFilePath(file) var filePath = getLoadedFilePath(file)
if (chatModel.connectedToRemote() && filePath == null) { if (chatModel.connectedToRemote() && filePath == null) {
@@ -136,8 +137,7 @@ fun CIFileView(
Box( Box(
Modifier Modifier
.size(42.dp) .size(42.dp)
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp)),
.clickable(onClick = { fileAction() }),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (file != null) { if (file != null) {
@@ -154,7 +154,13 @@ fun CIFileView(
FileProtocol.SMP -> progressIndicator() FileProtocol.SMP -> progressIndicator()
FileProtocol.LOCAL -> {} FileProtocol.LOCAL -> {}
} }
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled)) is CIFileStatus.SndComplete -> {
if ((file.forwardingAllowed() || (chatModel.connectedToRemote() && CIFile.cachedRemoteFileRequests[file.fileSource] == true))) {
fileIcon()
} else {
fileIcon(innerIcon = painterResource(MR.images.ic_check_filled))
}
}
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
is CIFileStatus.RcvInvitation -> is CIFileStatus.RcvInvitation ->
@@ -169,6 +175,8 @@ fun CIFileView(
} else { } else {
progressIndicator() progressIndicator()
} }
is CIFileStatus.RcvAborted ->
fileIcon(innerIcon = painterResource(MR.images.ic_sync_problem), color = MaterialTheme.colors.primary)
is CIFileStatus.RcvComplete -> fileIcon() is CIFileStatus.RcvComplete -> fileIcon()
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
@@ -181,7 +189,12 @@ fun CIFileView(
} }
Row( Row(
Modifier.clickable(onClick = { fileAction() }).padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp), Modifier
.combinedClickable(
onClick = { fileAction() },
onLongClick = { showMenu.value = true }
)
.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp),
//Modifier.clickable(enabled = file?.fileSource != null) { if (file?.fileSource != null && getLoadedFilePath(file) != null) openFile(file.fileSource) }.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp), //Modifier.clickable(enabled = file?.fileSource != null) { if (file?.fileSource != null && getLoadedFilePath(file) != null) openFile(file.fileSource) }.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp),
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(2.dp) horizontalArrangement = Arrangement.spacedBy(2.dp)
@@ -144,7 +144,7 @@ fun CIGroupInvitationView(
} }
} }
CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false) CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false)
} }
} }
} }
@@ -21,17 +21,12 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.File
import java.net.URI
@Composable @Composable
fun CIImageView( fun CIImageView(
image: String, image: String,
file: CIFile?, file: CIFile?,
metaColor: Color,
imageProvider: () -> ImageGalleryProvider, imageProvider: () -> ImageGalleryProvider,
showMenu: MutableState<Boolean>, showMenu: MutableState<Boolean>,
receiveFile: (Long) -> Unit receiveFile: (Long) -> Unit
@@ -51,7 +46,7 @@ fun CIImageView(
icon, icon,
stringResource(stringId), stringResource(stringId),
Modifier.fillMaxSize(), Modifier.fillMaxSize(),
tint = metaColor tint = Color.White
) )
} }
@@ -78,6 +73,7 @@ fun CIImageView(
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive) is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image) is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image)
is CIFileStatus.RcvTransfer -> progressIndicator() is CIFileStatus.RcvTransfer -> progressIndicator()
is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file)
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
@@ -206,7 +202,7 @@ fun CIImageView(
imageView(base64ToBitmap(image), onClick = { imageView(base64ToBitmap(image), onClick = {
if (file != null) { if (file != null) {
when (file.fileStatus) { when (file.fileStatus) {
CIFileStatus.RcvInvitation -> CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
if (fileSizeValid()) { if (fileSizeValid()) {
receiveFile(file.fileId) receiveFile(file.fileId)
} else { } else {
@@ -35,7 +35,8 @@ fun CIMetaView(
blue = minOf(metaColor.red * 1.33F, 1F)) blue = minOf(metaColor.red * 1.33F, 1F))
}, },
showStatus: Boolean = true, showStatus: Boolean = true,
showEdited: Boolean = true showEdited: Boolean = true,
showViaProxy: Boolean
) { ) {
Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) {
if (chatItem.isDeletedContent) { if (chatItem.isDeletedContent) {
@@ -53,7 +54,8 @@ fun CIMetaView(
metaColor, metaColor,
paleMetaColor, paleMetaColor,
showStatus = showStatus, showStatus = showStatus,
showEdited = showEdited showEdited = showEdited,
showViaProxy = showViaProxy
) )
} }
} }
@@ -68,7 +70,8 @@ private fun CIMetaText(
color: Color, color: Color,
paleColor: Color, paleColor: Color,
showStatus: Boolean = true, showStatus: Boolean = true,
showEdited: Boolean = true showEdited: Boolean = true,
showViaProxy: Boolean
) { ) {
if (showEdited && meta.itemEdited) { if (showEdited && meta.itemEdited) {
StatusIconText(painterResource(MR.images.ic_edit), color) StatusIconText(painterResource(MR.images.ic_edit), color)
@@ -82,6 +85,9 @@ private fun CIMetaText(
} }
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
} }
if (showViaProxy && meta.sentViaProxy == true) {
Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = CurrentColors.value.colors.secondary)
}
if (showStatus) { if (showStatus) {
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor) val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor)
if (statusIcon != null) { if (statusIcon != null) {
@@ -105,7 +111,14 @@ private fun CIMetaText(
} }
// the conditions in this function should match CIMetaText // the conditions in this function should match CIMetaText
fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, showStatus: Boolean = true, showEdited: Boolean = true): String { fun reserveSpaceForMeta(
meta: CIMeta,
chatTTL: Int?,
encrypted: Boolean?,
showStatus: Boolean = true,
showEdited: Boolean = true,
showViaProxy: Boolean = false
): String {
val iconSpace = " " val iconSpace = " "
var res = "" var res = ""
if (showEdited && meta.itemEdited) res += iconSpace if (showEdited && meta.itemEdited) res += iconSpace
@@ -116,6 +129,9 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, showSt
res += shortTimeText(ttl) res += shortTimeText(ttl)
} }
} }
if (showViaProxy && meta.sentViaProxy == true) {
res += iconSpace
}
if (showStatus && (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing)) { if (showStatus && (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing)) {
res += iconSpace res += iconSpace
} }
@@ -137,7 +153,8 @@ fun PreviewCIMetaView() {
chatItem = ChatItem.getSampleData( chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello" 1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
), ),
null null,
showViaProxy = false
) )
} }
@@ -149,7 +166,8 @@ fun PreviewCIMetaViewUnread() {
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", 1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
status = CIStatus.RcvNew() status = CIStatus.RcvNew()
), ),
null null,
showViaProxy = false
) )
} }
@@ -159,9 +177,10 @@ fun PreviewCIMetaViewSendFailed() {
CIMetaView( CIMetaView(
chatItem = ChatItem.getSampleData( chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", 1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
status = CIStatus.SndError("CMD SYNTAX") status = CIStatus.CISSndError(SndError.Other("CMD SYNTAX"))
), ),
null null,
showViaProxy = false
) )
} }
@@ -172,7 +191,8 @@ fun PreviewCIMetaViewSendNoAuth() {
chatItem = ChatItem.getSampleData( chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth() 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth()
), ),
null null,
showViaProxy = false
) )
} }
@@ -183,7 +203,8 @@ fun PreviewCIMetaViewSendSent() {
chatItem = ChatItem.getSampleData( chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent(SndCIStatusProgress.Complete) 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent(SndCIStatusProgress.Complete)
), ),
null null,
showViaProxy = false
) )
} }
@@ -195,7 +216,8 @@ fun PreviewCIMetaViewEdited() {
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", 1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
itemEdited = true itemEdited = true
), ),
null null,
showViaProxy = false
) )
} }
@@ -208,7 +230,8 @@ fun PreviewCIMetaViewEditedUnread() {
itemEdited = true, itemEdited = true,
status= CIStatus.RcvNew() status= CIStatus.RcvNew()
), ),
null null,
showViaProxy = false
) )
} }
@@ -221,7 +244,8 @@ fun PreviewCIMetaViewEditedSent() {
itemEdited = true, itemEdited = true,
status= CIStatus.SndSent(SndCIStatusProgress.Complete) status= CIStatus.SndSent(SndCIStatusProgress.Complete)
), ),
null null,
showViaProxy = false
) )
} }
@@ -230,6 +254,7 @@ fun PreviewCIMetaViewEditedSent() {
fun PreviewCIMetaViewDeletedContent() { fun PreviewCIMetaViewDeletedContent() {
CIMetaView( CIMetaView(
chatItem = ChatItem.getDeletedContentSampleData(), chatItem = ChatItem.getDeletedContentSampleData(),
null null,
showViaProxy = false
) )
} }
@@ -174,7 +174,7 @@ fun DecryptionErrorItemFixButton(
) )
} }
} }
CIMetaView(ci, timedMessagesTTL = null) CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false)
} }
} }
} }
@@ -202,7 +202,7 @@ fun DecryptionErrorItem(
}, },
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp) style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
) )
CIMetaView(ci, timedMessagesTTL = null) CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false)
} }
} }
} }
@@ -73,7 +73,7 @@ fun CIVideoView(
VideoPreviewImageView(preview, onClick = { VideoPreviewImageView(preview, onClick = {
if (file != null) { if (file != null) {
when (file.fileStatus) { when (file.fileStatus) {
CIFileStatus.RcvInvitation -> CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
receiveFileIfValidSize(file, receiveFile) receiveFileIfValidSize(file, receiveFile)
CIFileStatus.RcvAccepted -> CIFileStatus.RcvAccepted ->
when (file.fileProtocol) { when (file.fileProtocol) {
@@ -102,7 +102,7 @@ fun CIVideoView(
if (file != null) { if (file != null) {
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/) DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
} }
if (file?.fileStatus is CIFileStatus.RcvInvitation) { if (file?.fileStatus is CIFileStatus.RcvInvitation || file?.fileStatus is CIFileStatus.RcvAborted) {
PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) } PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
} }
} }
@@ -396,6 +396,7 @@ private fun loadingIndicator(file: CIFile?) {
} else { } else {
progressIndicator() progressIndicator()
} }
is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file)
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
@@ -22,6 +22,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
// TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901 // TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901
@@ -35,6 +36,7 @@ fun CIVoiceView(
hasText: Boolean, hasText: Boolean,
ci: ChatItem, ci: ChatItem,
timedMessagesTTL: Int?, timedMessagesTTL: Int?,
showViaProxy: Boolean,
longClick: () -> Unit, longClick: () -> Unit,
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
) { ) {
@@ -76,7 +78,7 @@ fun CIVoiceView(
durationText(time / 1000) durationText(time / 1000)
} }
} }
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick, receiveFile) { VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, play, pause, longClick, receiveFile) {
AudioPlayer.seekTo(it, progress, fileSource.value?.filePath) AudioPlayer.seekTo(it, progress, fileSource.value?.filePath)
} }
} else { } else {
@@ -102,6 +104,7 @@ private fun VoiceLayout(
sent: Boolean, sent: Boolean,
hasText: Boolean, hasText: Boolean,
timedMessagesTTL: Int?, timedMessagesTTL: Int?,
showViaProxy: Boolean,
play: () -> Unit, play: () -> Unit,
pause: () -> Unit, pause: () -> Unit,
longClick: () -> Unit, longClick: () -> Unit,
@@ -171,7 +174,7 @@ private fun VoiceLayout(
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
} }
Box(Modifier.padding(top = 6.dp, end = 6.dp)) { Box(Modifier.padding(top = 6.dp, end = 6.dp)) {
CIMetaView(ci, timedMessagesTTL) CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
} }
} }
} }
@@ -186,7 +189,7 @@ private fun VoiceLayout(
} }
} }
Box(Modifier.padding(top = 6.dp)) { Box(Modifier.padding(top = 6.dp)) {
CIMetaView(ci, timedMessagesTTL) CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
} }
} }
} }
@@ -218,7 +221,8 @@ private fun PlayPauseButton(
error: Boolean, error: Boolean,
play: () -> Unit, play: () -> Unit,
pause: () -> Unit, pause: () -> Unit,
longClick: () -> Unit longClick: () -> Unit,
icon: ImageResource = MR.images.ic_play_arrow_filled,
) { ) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
@@ -239,7 +243,7 @@ private fun PlayPauseButton(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(icon),
contentDescription = null, contentDescription = null,
Modifier.size(36.dp), Modifier.size(36.dp),
tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
@@ -292,6 +296,8 @@ private fun VoiceMsgIndicator(
) { ) {
ProgressIndicator() ProgressIndicator()
} }
} else if (file?.fileStatus is CIFileStatus.RcvAborted) {
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem)
} else { } else {
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick) PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick)
} }
@@ -68,6 +68,7 @@ fun ChatItemView(
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit,
developerTools: Boolean, developerTools: Boolean,
showViaProxy: Boolean
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val sent = cItem.chatDir.sent val sent = cItem.chatDir.sent
@@ -83,17 +84,15 @@ fun ChatItemView(
.fillMaxWidth(), .fillMaxWidth(),
contentAlignment = alignment, contentAlignment = alignment,
) { ) {
val onClick = { val info = cItem.meta.itemStatus.statusInto
when (cItem.meta.itemStatus) { val onClick = if (info != null) {
is CIStatus.SndErrorAuth -> { {
showMsgDeliveryErrorAlert(generalGetString(MR.strings.message_delivery_error_desc)) AlertManager.shared.showAlertMsg(
} title = info.first,
is CIStatus.SndError -> { text = info.second,
showMsgDeliveryErrorAlert(generalGetString(MR.strings.unknown_error) + ": ${cItem.meta.itemStatus.agentError}") )
}
else -> {}
} }
} } else { {} }
@Composable @Composable
fun ChatItemReactions() { fun ChatItemReactions() {
@@ -130,7 +129,7 @@ fun ChatItemView(
) { ) {
@Composable @Composable
fun framedItemView() { fun framedItemView() {
FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem) FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, receiveFile, onLinkLongClick, scrollToItem)
} }
fun deleteMessageQuestionText(): String { fun deleteMessageQuestionText(): String {
@@ -204,14 +203,10 @@ fun ChatItemView(
} }
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
val cachedRemoteReqs = remember { CIFile.cachedRemoteFileRequests } val cachedRemoteReqs = remember { CIFile.cachedRemoteFileRequests }
fun fileForwardingAllowed() = when {
cItem.file != null && chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file.fileSource] != false && cItem.file.loaded -> true
getLoadedFilePath(cItem.file) != null -> true
else -> false
}
val copyAndShareAllowed = when { val copyAndShareAllowed = when {
cItem.content.text.isNotEmpty() -> true cItem.content.text.isNotEmpty() -> true
fileForwardingAllowed() -> true cItem.file?.forwardingAllowed() == true -> true
else -> false else -> false
} }
@@ -261,7 +256,7 @@ fun ChatItemView(
}) })
} }
if (cItem.meta.itemDeleted == null && if (cItem.meta.itemDeleted == null &&
(cItem.file == null || fileForwardingAllowed()) && (cItem.file == null || cItem.file.forwardingAllowed()) &&
!cItem.isLiveDummy && !live !cItem.isLiveDummy && !live
) { ) {
ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = { ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = {
@@ -338,14 +333,14 @@ fun ChatItemView(
fun ContentItem() { fun ContentItem() {
val mc = cItem.content.msgContent val mc = cItem.content.msgContent
if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) { if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) {
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed) MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy)
MarkedDeletedItemDropdownMenu() MarkedDeletedItemDropdownMenu()
} else { } else {
if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) { if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) { if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem, cInfo.timedMessagesTTL) EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy)
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) { } else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") }, receiveFile) CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile)
} else { } else {
framedItemView() framedItemView()
} }
@@ -357,7 +352,7 @@ fun ChatItemView(
} }
@Composable fun LegacyDeletedItem() { @Composable fun LegacyDeletedItem() {
DeletedItemView(cItem, cInfo.timedMessagesTTL) DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy)
DefaultDropdownMenu(showMenu) { DefaultDropdownMenu(showMenu) {
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
@@ -410,7 +405,7 @@ fun ChatItemView(
@Composable @Composable
fun DeletedItem() { fun DeletedItem() {
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed) MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy)
DefaultDropdownMenu(showMenu) { DefaultDropdownMenu(showMenu) {
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages) DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages)
@@ -820,13 +815,6 @@ fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteM
) )
} }
private fun showMsgDeliveryErrorAlert(description: String) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.message_delivery_error_title),
text = description,
)
}
expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager)
@Preview @Preview
@@ -862,6 +850,7 @@ fun PreviewChatItemView() {
setReaction = { _, _, _, _ -> }, setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> }, showItemDetails = { _, _ -> },
developerTools = false, developerTools = false,
showViaProxy = false
) )
} }
} }
@@ -897,6 +886,7 @@ fun PreviewChatItemViewDeletedContent() {
setReaction = { _, _, _, _ -> }, setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> }, showItemDetails = { _, _ -> },
developerTools = false, developerTools = false,
showViaProxy = false
) )
} }
} }
@@ -16,7 +16,7 @@ import chat.simplex.common.model.ChatItem
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
@Composable @Composable
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) {
val sent = ci.chatDir.sent val sent = ci.chatDir.sent
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
@@ -36,7 +36,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) {
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp) modifier = Modifier.padding(end = 8.dp)
) )
CIMetaView(ci, timedMessagesTTL) CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
} }
} }
} }
@@ -50,7 +50,8 @@ fun PreviewDeletedItemView() {
SimpleXTheme { SimpleXTheme {
DeletedItemView( DeletedItemView(
ChatItem.getDeletedContentSampleData(), ChatItem.getDeletedContentSampleData(),
null null,
showViaProxy = false
) )
} }
} }
@@ -17,13 +17,13 @@ val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp, fontFamily = EmojiFo
val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp, fontFamily = EmojiFont) val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp, fontFamily = EmojiFont)
@Composable @Composable
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?) { fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) {
Column( Column(
Modifier.padding(vertical = 8.dp, horizontal = 12.dp), Modifier.padding(vertical = 8.dp, horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
EmojiText(chatItem.content.text) EmojiText(chatItem.content.text)
CIMetaView(chatItem, timedMessagesTTL) CIMetaView(chatItem, timedMessagesTTL, showViaProxy = showViaProxy)
} }
} }
@@ -23,7 +23,6 @@ import chat.simplex.common.model.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.chat.MEMBER_IMAGE_SIZE
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlin.math.min import kotlin.math.min
@@ -34,6 +33,7 @@ fun FramedItemView(
uriHandler: UriHandler? = null, uriHandler: UriHandler? = null,
imageProvider: (() -> ImageGalleryProvider)? = null, imageProvider: (() -> ImageGalleryProvider)? = null,
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
showViaProxy: Boolean,
showMenu: MutableState<Boolean>, showMenu: MutableState<Boolean>,
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
onLinkLongClick: (link: String) -> Unit = {}, onLinkLongClick: (link: String) -> Unit = {},
@@ -179,9 +179,9 @@ fun FramedItemView(
@Composable @Composable
fun ciFileView(ci: ChatItem, text: String) { fun ciFileView(ci: ChatItem, text: String) {
CIFileView(ci.file, ci.meta.itemEdited, receiveFile) CIFileView(ci.file, ci.meta.itemEdited, showMenu, receiveFile)
if (text != "" || ci.meta.isLive) { if (text != "" || ci.meta.isLive) {
CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler) CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy)
} }
} }
@@ -241,11 +241,11 @@ fun FramedItemView(
} else { } else {
when (val mc = ci.content.msgContent) { when (val mc = ci.content.msgContent) {
is MsgContent.MCImage -> { is MsgContent.MCImage -> {
CIImageView(image = mc.image, file = ci.file, metaColor = metaColor, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
if (mc.text == "" && !ci.meta.isLive) { if (mc.text == "" && !ci.meta.isLive) {
metaColor = Color.White metaColor = Color.White
} else { } else {
CIMarkdownText(ci, chatTTL, linkMode, uriHandler) CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
} }
} }
is MsgContent.MCVideo -> { is MsgContent.MCVideo -> {
@@ -253,35 +253,35 @@ fun FramedItemView(
if (mc.text == "" && !ci.meta.isLive) { if (mc.text == "" && !ci.meta.isLive) {
metaColor = Color.White metaColor = Color.White
} else { } else {
CIMarkdownText(ci, chatTTL, linkMode, uriHandler) CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
} }
} }
is MsgContent.MCVoice -> { is MsgContent.MCVoice -> {
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") }, receiveFile) CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile)
if (mc.text != "") { if (mc.text != "") {
CIMarkdownText(ci, chatTTL, linkMode, uriHandler) CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
} }
} }
is MsgContent.MCFile -> ciFileView(ci, mc.text) is MsgContent.MCFile -> ciFileView(ci, mc.text)
is MsgContent.MCUnknown -> is MsgContent.MCUnknown ->
if (ci.file == null) { if (ci.file == null) {
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
} else { } else {
ciFileView(ci, mc.text) ciFileView(ci, mc.text)
} }
is MsgContent.MCLink -> { is MsgContent.MCLink -> {
ChatItemLinkView(mc.preview) ChatItemLinkView(mc.preview)
Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) {
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
} }
} }
else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
} }
} }
} }
} }
Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) { Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) {
CIMetaView(ci, chatTTL, metaColor) CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy)
} }
} }
} }
@@ -293,14 +293,15 @@ fun CIMarkdownText(
chatTTL: Int?, chatTTL: Int?,
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
uriHandler: UriHandler?, uriHandler: UriHandler?,
onLinkLongClick: (link: String) -> Unit = {} onLinkLongClick: (link: String) -> Unit = {},
showViaProxy: Boolean
) { ) {
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text
MarkdownText( MarkdownText(
text, if (text.isEmpty()) emptyList() else ci.formattedText, toggleSecrets = true, text, if (text.isEmpty()) emptyList() else ci.formattedText, toggleSecrets = true,
meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode, meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode,
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy
) )
} }
} }
@@ -69,7 +69,7 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp) modifier = Modifier.padding(end = 8.dp)
) )
CIMetaView(ci, timedMessagesTTL) CIMetaView(ci, timedMessagesTTL, showViaProxy = false)
} }
} }
} }
@@ -20,7 +20,7 @@ import dev.icerock.moko.resources.compose.stringResource
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@Composable @Composable
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>) { fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>, showViaProxy: Boolean) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Surface( Surface(
@@ -35,7 +35,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: Mutabl
Box(Modifier.weight(1f, false)) { Box(Modifier.weight(1f, false)) {
MergedMarkedDeletedText(ci, revealed) MergedMarkedDeletedText(ci, revealed)
} }
CIMetaView(ci, timedMessagesTTL) CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
} }
} }
} }
@@ -112,7 +112,8 @@ fun PreviewMarkedDeletedItemView() {
SimpleXTheme { SimpleXTheme {
DeletedItemView( DeletedItemView(
ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())), ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())),
null null,
showViaProxy = false
) )
} }
} }
@@ -69,7 +69,8 @@ fun MarkdownText (
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = null, inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = null,
onLinkLongClick: (link: String) -> Unit = {} onLinkLongClick: (link: String) -> Unit = {},
showViaProxy: Boolean = false
) { ) {
val textLayoutDirection = remember (text) { val textLayoutDirection = remember (text) {
if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr
@@ -77,7 +78,7 @@ fun MarkdownText (
val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) { val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) {
"\n" "\n"
} else if (meta != null) { } else if (meta != null) {
reserveSpaceForMeta(meta, chatTTL, null) // LALAL reserveSpaceForMeta(meta, chatTTL, null, showViaProxy = showViaProxy)
} else { } else {
" " " "
} }
@@ -66,6 +66,8 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
hostMode = currentCfg.value.hostMode, hostMode = currentCfg.value.hostMode,
requiredHostMode = currentCfg.value.requiredHostMode, requiredHostMode = currentCfg.value.requiredHostMode,
sessionMode = currentCfg.value.sessionMode, sessionMode = currentCfg.value.sessionMode,
smpProxyMode = currentCfg.value.smpProxyMode,
smpProxyFallback = currentCfg.value.smpProxyFallback,
tcpConnectTimeout = networkTCPConnectTimeout.value, tcpConnectTimeout = networkTCPConnectTimeout.value,
tcpTimeout = networkTCPTimeout.value, tcpTimeout = networkTCPTimeout.value,
tcpTimeoutPerKb = networkTCPTimeoutPerKb.value, tcpTimeoutPerKb = networkTCPTimeoutPerKb.value,
@@ -159,7 +161,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
SectionItemView { SectionItemView {
TimeoutSettingRow( TimeoutSettingRow(
stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeout, stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeout,
listOf(10_000000, 15_000000, 20_000000, 25_000000, 35_000000, 50_000000), secondsLabel listOf(10_000000, 15_000000, 20_000000, 30_000000, 45_000000, 60_000000, 90_000000), secondsLabel
) )
} }
SectionItemView { SectionItemView {
@@ -175,12 +177,12 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
listOf(2_500, 5_000, 10_000, 15_000, 20_000, 30_000), secondsLabel listOf(2_500, 5_000, 10_000, 15_000, 20_000, 30_000), secondsLabel
) )
} }
SectionItemView { // SectionItemView {
IntSettingRow( // IntSettingRow(
stringResource(MR.strings.network_option_rcv_concurrency), networkRcvConcurrency, // stringResource(MR.strings.network_option_rcv_concurrency), networkRcvConcurrency,
listOf(1, 2, 4, 8, 12, 16, 24), "" // listOf(1, 2, 4, 8, 12, 16, 24), ""
) // )
} // }
SectionItemView { SectionItemView {
TimeoutSettingRow( TimeoutSettingRow(
stringResource(MR.strings.network_option_ping_interval), networkSMPPingInterval, stringResource(MR.strings.network_option_ping_interval), networkSMPPingInterval,
@@ -43,6 +43,8 @@ fun NetworkAndServersView() {
val developerTools = chatModel.controller.appPrefs.developerTools.get() val developerTools = chatModel.controller.appPrefs.developerTools.get()
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) } val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) } val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
val smpProxyMode = remember { mutableStateOf(netCfg.smpProxyMode) }
val smpProxyFallback = remember { mutableStateOf(netCfg.smpProxyFallback) }
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } } val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
NetworkAndServersLayout( NetworkAndServersLayout(
@@ -51,6 +53,8 @@ fun NetworkAndServersView() {
networkUseSocksProxy = networkUseSocksProxy, networkUseSocksProxy = networkUseSocksProxy,
onionHosts = onionHosts, onionHosts = onionHosts,
sessionMode = sessionMode, sessionMode = sessionMode,
smpProxyMode = smpProxyMode,
smpProxyFallback = smpProxyFallback,
proxyPort = proxyPort, proxyPort = proxyPort,
toggleSocksProxy = { enable -> toggleSocksProxy = { enable ->
if (enable) { if (enable) {
@@ -137,6 +141,59 @@ fun NetworkAndServersView() {
} }
} }
} }
},
updateSMPProxyMode = {
if (smpProxyMode.value == it) return@NetworkAndServersLayout
val prevValue = smpProxyMode.value
smpProxyMode.value = it
val startsWith = when (it) {
SMPProxyMode.Always -> generalGetString(MR.strings.network_smp_proxy_mode_always_description)
SMPProxyMode.Unknown -> generalGetString(MR.strings.network_smp_proxy_mode_unknown_description)
SMPProxyMode.Unprotected -> generalGetString(MR.strings.network_smp_proxy_mode_unprotected_description)
SMPProxyMode.Never -> generalGetString(MR.strings.network_smp_proxy_mode_never_description)
}
showUpdateNetworkSettingsDialog(
title = generalGetString(MR.strings.update_network_smp_proxy_mode_question),
startsWith,
onDismiss = { smpProxyMode.value = prevValue }
) {
withBGApi {
val newCfg = chatModel.controller.getNetCfg().copy(smpProxyMode = it)
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
if (res) {
chatModel.controller.setNetCfg(newCfg)
smpProxyMode.value = it
} else {
smpProxyMode.value = prevValue
}
}
}
},
updateSMPProxyFallback = {
if (smpProxyFallback.value == it) return@NetworkAndServersLayout
val prevValue = smpProxyFallback.value
smpProxyFallback.value = it
val startsWith = when (it) {
SMPProxyFallback.Allow -> generalGetString(MR.strings.network_smp_proxy_fallback_allow_description)
SMPProxyFallback.AllowProtected -> generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected_description)
SMPProxyFallback.Prohibit -> generalGetString(MR.strings.network_smp_proxy_fallback_prohibit_description)
}
showUpdateNetworkSettingsDialog(
title = generalGetString(MR.strings.update_network_smp_proxy_fallback_question),
startsWith,
onDismiss = { smpProxyFallback.value = prevValue }
) {
withBGApi {
val newCfg = chatModel.controller.getNetCfg().copy(smpProxyFallback = it)
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
if (res) {
chatModel.controller.setNetCfg(newCfg)
smpProxyFallback.value = it
} else {
smpProxyFallback.value = prevValue
}
}
}
} }
) )
} }
@@ -147,16 +204,22 @@ fun NetworkAndServersView() {
networkUseSocksProxy: MutableState<Boolean>, networkUseSocksProxy: MutableState<Boolean>,
onionHosts: MutableState<OnionHosts>, onionHosts: MutableState<OnionHosts>,
sessionMode: MutableState<TransportSessionMode>, sessionMode: MutableState<TransportSessionMode>,
smpProxyMode: MutableState<SMPProxyMode>,
smpProxyFallback: MutableState<SMPProxyFallback>,
proxyPort: State<Int>, proxyPort: State<Int>,
toggleSocksProxy: (Boolean) -> Unit, toggleSocksProxy: (Boolean) -> Unit,
useOnion: (OnionHosts) -> Unit, useOnion: (OnionHosts) -> Unit,
updateSessionMode: (TransportSessionMode) -> Unit, updateSessionMode: (TransportSessionMode) -> Unit,
updateSMPProxyMode: (SMPProxyMode) -> Unit,
updateSMPProxyFallback: (SMPProxyFallback) -> Unit,
) { ) {
val m = chatModel val m = chatModel
ColumnWithScrollBar( ColumnWithScrollBar(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) }
AppBarTitle(stringResource(MR.strings.network_and_servers)) AppBarTitle(stringResource(MR.strings.network_and_servers))
if (!chatModel.desktopNoUserNoRemote) { if (!chatModel.desktopNoUserNoRemote) {
SectionView(generalGetString(MR.strings.settings_section_title_messages)) { SectionView(generalGetString(MR.strings.settings_section_title_messages)) {
@@ -165,7 +228,6 @@ fun NetworkAndServersView() {
SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } }) SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } })
if (currentRemoteHost == null) { if (currentRemoteHost == null) {
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) }
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false) UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false)
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion) UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
if (developerTools) { if (developerTools) {
@@ -188,6 +250,18 @@ fun NetworkAndServersView() {
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
} }
if (currentRemoteHost == null) {
SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) {
SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode)
SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { mutableStateOf(smpProxyMode.value != SMPProxyMode.Never) })
SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
}
SectionCustomFooter {
Text(stringResource(MR.strings.private_routing_explanation))
}
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
}
SectionView(generalGetString(MR.strings.settings_section_title_calls)) { SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } }) SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } })
} }
@@ -452,6 +526,79 @@ private fun SessionModePicker(
) )
} }
@Composable
private fun SMPProxyModePicker(
smpProxyMode: MutableState<SMPProxyMode>,
showModal: (@Composable ModalData.() -> Unit) -> Unit,
updateSMPProxyMode: (SMPProxyMode) -> Unit,
) {
val density = LocalDensity.current
val values = remember {
SMPProxyMode.values().map {
when (it) {
SMPProxyMode.Always -> ValueTitleDesc(SMPProxyMode.Always, generalGetString(MR.strings.network_smp_proxy_mode_always), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_always_description), density))
SMPProxyMode.Unknown -> ValueTitleDesc(SMPProxyMode.Unknown, generalGetString(MR.strings.network_smp_proxy_mode_unknown), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unknown_description), density))
SMPProxyMode.Unprotected -> ValueTitleDesc(SMPProxyMode.Unprotected, generalGetString(MR.strings.network_smp_proxy_mode_unprotected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unprotected_description), density))
SMPProxyMode.Never -> ValueTitleDesc(SMPProxyMode.Never, generalGetString(MR.strings.network_smp_proxy_mode_never), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_never_description), density))
}
}
}
SectionItemWithValue(
generalGetString(MR.strings.network_smp_proxy_mode_private_routing),
smpProxyMode,
values,
icon = painterResource(MR.images.ic_settings_ethernet),
onSelected = {
showModal {
ColumnWithScrollBar(
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing))
SectionViewSelectable(null, smpProxyMode, values, updateSMPProxyMode)
}
}
}
)
}
@Composable
private fun SMPProxyFallbackPicker(
smpProxyFallback: MutableState<SMPProxyFallback>,
showModal: (@Composable ModalData.() -> Unit) -> Unit,
updateSMPProxyFallback: (SMPProxyFallback) -> Unit,
enabled: State<Boolean>,
) {
val density = LocalDensity.current
val values = remember {
SMPProxyFallback.values().map {
when (it) {
SMPProxyFallback.Allow -> ValueTitleDesc(SMPProxyFallback.Allow, generalGetString(MR.strings.network_smp_proxy_fallback_allow), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_description), density))
SMPProxyFallback.AllowProtected -> ValueTitleDesc(SMPProxyFallback.AllowProtected, generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected_description), density))
SMPProxyFallback.Prohibit -> ValueTitleDesc(SMPProxyFallback.Prohibit, generalGetString(MR.strings.network_smp_proxy_fallback_prohibit), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_prohibit_description), density))
}
}
}
SectionItemWithValue(
generalGetString(MR.strings.network_smp_proxy_fallback_allow_downgrade),
smpProxyFallback,
values,
icon = painterResource(MR.images.ic_arrows_left_right),
enabled = enabled,
onSelected = {
showModal {
ColumnWithScrollBar(
Modifier.fillMaxWidth(),
) {
AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade))
SectionViewSelectable(null, smpProxyFallback, values, updateSMPProxyFallback)
}
}
}
)
}
@Composable @Composable
private fun NetworkSectionFooter(revert: () -> Unit, save: () -> Unit, revertDisabled: Boolean, saveDisabled: Boolean) { private fun NetworkSectionFooter(revert: () -> Unit, save: () -> Unit, revertDisabled: Boolean, saveDisabled: Boolean) {
Row( Row(
@@ -506,8 +653,12 @@ fun PreviewNetworkAndServersLayout() {
toggleSocksProxy = {}, toggleSocksProxy = {},
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) }, onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
sessionMode = remember { mutableStateOf(TransportSessionMode.User) }, sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) },
smpProxyFallback = remember { mutableStateOf(SMPProxyFallback.Allow) },
useOnion = {}, useOnion = {},
updateSessionMode = {}, updateSessionMode = {},
updateSMPProxyMode = {},
updateSMPProxyFallback = {},
) )
} }
} }
@@ -1,6 +1,7 @@
package chat.simplex.common.views.usersettings package chat.simplex.common.views.usersettings
import SectionBottomSpacer import SectionBottomSpacer
import SectionCustomFooter
import SectionDividerSpaced import SectionDividerSpaced
import SectionItemView import SectionItemView
import SectionTextFooter import SectionTextFooter
@@ -63,10 +64,6 @@ fun PrivacySettingsView(
SectionDividerSpaced() SectionDividerSpaced()
SectionView(stringResource(MR.strings.settings_section_title_chats)) { SectionView(stringResource(MR.strings.settings_section_title_chats)) {
SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable ->
withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
})
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews) SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
SettingsPreferenceItem( SettingsPreferenceItem(
painterResource(MR.images.ic_chat_bubble), painterResource(MR.images.ic_chat_bubble),
@@ -91,6 +88,22 @@ fun PrivacySettingsView(
chatModel.simplexLinkMode.value = it chatModel.simplexLinkMode.value = it
}) })
} }
SectionDividerSpaced()
SectionView(stringResource(MR.strings.settings_section_title_files)) {
SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable ->
withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
})
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays)
}
SectionCustomFooter {
if (chatModel.controller.appPrefs.privacyAskToApproveRelays.state.value) {
Text(stringResource(MR.strings.app_will_ask_to_confirm_unknown_file_servers))
} else {
Text(stringResource(MR.strings.without_tor_or_vpn_ip_address_will_be_visible_to_file_servers))
}
}
val currentUser = chatModel.currentUser.value val currentUser = chatModel.currentUser.value
if (currentUser != null) { if (currentUser != null) {
@@ -141,7 +154,7 @@ fun PrivacySettingsView(
} }
if (!chatModel.desktopNoUserNoRemote) { if (!chatModel.desktopNoUserNoRemote) {
SectionDividerSpaced() SectionDividerSpaced(maxTopPadding = true)
DeliveryReceiptsSection( DeliveryReceiptsSection(
currentUser = currentUser, currentUser = currentUser,
setOrAskSendReceiptsContacts = { enable -> setOrAskSendReceiptsContacts = { enable ->
@@ -119,6 +119,8 @@
<string name="error_joining_group">Error joining group</string> <string name="error_joining_group">Error joining group</string>
<string name="cannot_receive_file">Cannot receive file</string> <string name="cannot_receive_file">Cannot receive file</string>
<string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string> <string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string>
<string name="file_not_approved_title">Unknown servers!</string>
<string name="file_not_approved_descr">Without Tor or VPN, your IP address will be visible to these XFTP relays:\n%1$s.</string>
<string name="error_receiving_file">Error receiving file</string> <string name="error_receiving_file">Error receiving file</string>
<string name="error_creating_address">Error creating address</string> <string name="error_creating_address">Error creating address</string>
<string name="contact_already_exists">Contact already exists</string> <string name="contact_already_exists">Contact already exists</string>
@@ -255,8 +257,20 @@
<!-- Chat Alerts - ChatItemView.kt --> <!-- Chat Alerts - ChatItemView.kt -->
<string name="message_delivery_error_title">Message delivery error</string> <string name="message_delivery_error_title">Message delivery error</string>
<string name="message_delivery_warning_title">Message delivery warning</string>
<string name="message_delivery_error_desc">Most likely this contact has deleted the connection with you.</string> <string name="message_delivery_error_desc">Most likely this contact has deleted the connection with you.</string>
<!-- CIStatus errors -->
<string name="ci_status_other_error">Error: %1$s</string>
<string name="snd_error_auth">Wrong key or unknown connection - most likely this connection is deleted.</string>
<string name="snd_error_quota">Capacity exceeded - recipient did not receive previously sent messages.</string>
<string name="snd_error_expired">Network issues - message expired after many attempts to send it.</string>
<string name="snd_error_relay">Destination server error: %1$s</string>
<string name="snd_error_proxy">Forwarding server: %1$s\nError: %2$s</string>
<string name="snd_error_proxy_relay">Forwarding server: %1$s\nDestination server error: %2$s</string>
<string name="srv_error_host">Server address is incompatible with network settings.</string>
<string name="srv_error_version">Server version is incompatible with network settings.</string>
<!-- Chat Actions - ChatItemView.kt (and general) --> <!-- Chat Actions - ChatItemView.kt (and general) -->
<string name="reply_verb">Reply</string> <string name="reply_verb">Reply</string>
<string name="share_verb">Share</string> <string name="share_verb">Share</string>
@@ -710,6 +724,26 @@
<string name="update_network_session_mode_question">Update transport isolation mode?</string> <string name="update_network_session_mode_question">Update transport isolation mode?</string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Set <i>Use .onion hosts</i> to No if SOCKS proxy does not support them.]]></string> <string name="disable_onion_hosts_when_not_supported"><![CDATA[Set <i>Use .onion hosts</i> to No if SOCKS proxy does not support them.]]></string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Please note</b>: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]></string> <string name="socks_proxy_setting_limitations"><![CDATA[<b>Please note</b>: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]></string>
<string name="network_smp_proxy_mode_private_routing">Private routing</string>
<string name="network_smp_proxy_mode_always">Always</string>
<string name="network_smp_proxy_mode_unknown">Unknown relays</string>
<string name="network_smp_proxy_mode_unprotected">Unprotected</string>
<string name="network_smp_proxy_mode_never">Never</string>
<string name="network_smp_proxy_mode_always_description">Always use private routing.</string>
<string name="network_smp_proxy_mode_unknown_description">Use private routing with unknown servers.</string>
<string name="network_smp_proxy_mode_unprotected_description">Use private routing with unknown servers when IP address is not protected.</string>
<string name="network_smp_proxy_mode_never_description">Do NOT use private routing.</string>
<string name="update_network_smp_proxy_mode_question">Message routing mode</string>
<string name="network_smp_proxy_fallback_allow_downgrade">Allow downgrade</string>
<string name="network_smp_proxy_fallback_allow">Yes</string>
<string name="network_smp_proxy_fallback_allow_protected">When IP hidden</string>
<string name="network_smp_proxy_fallback_prohibit">No</string>
<string name="network_smp_proxy_fallback_allow_description">Send messages directly when your or destination server does not support private routing.</string>
<string name="network_smp_proxy_fallback_allow_protected_description">Send messages directly when IP address is protected and your or destination server does not support private routing.</string>
<string name="network_smp_proxy_fallback_prohibit_description">Do NOT send messages directly, even if your or destination server does not support private routing.</string>
<string name="update_network_smp_proxy_fallback_question">Message routing fallback</string>
<string name="private_routing_show_message_status">Show message status</string>
<string name="private_routing_explanation">To protect your IP address, private routing uses your SMP servers to deliver messages.</string>
<string name="appearance_settings">Appearance</string> <string name="appearance_settings">Appearance</string>
<string name="customize_theme_title">Customize theme</string> <string name="customize_theme_title">Customize theme</string>
<string name="theme_colors_section_title">THEME COLORS</string> <string name="theme_colors_section_title">THEME COLORS</string>
@@ -960,6 +994,9 @@
<string name="protect_app_screen">Protect app screen</string> <string name="protect_app_screen">Protect app screen</string>
<string name="encrypt_local_files">Encrypt local files</string> <string name="encrypt_local_files">Encrypt local files</string>
<string name="auto_accept_images">Auto-accept images</string> <string name="auto_accept_images">Auto-accept images</string>
<string name="protect_ip_address">Protect IP address</string>
<string name="app_will_ask_to_confirm_unknown_file_servers">The app will ask to confirm downloads from unknown file servers (except .onion or when SOCKS proxy is enabled).</string>
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Without Tor or VPN, your IP address will be visible to file servers.</string>
<string name="send_link_previews">Send link previews</string> <string name="send_link_previews">Send link previews</string>
<string name="privacy_show_last_messages">Show last messages</string> <string name="privacy_show_last_messages">Show last messages</string>
<string name="privacy_message_draft">Message draft</string> <string name="privacy_message_draft">Message draft</string>
@@ -1024,6 +1061,7 @@
<string name="settings_section_title_app">APP</string> <string name="settings_section_title_app">APP</string>
<string name="settings_section_title_device">DEVICE</string> <string name="settings_section_title_device">DEVICE</string>
<string name="settings_section_title_chats">CHATS</string> <string name="settings_section_title_chats">CHATS</string>
<string name="settings_section_title_files">FILES</string>
<string name="settings_section_title_delivery_receipts">SEND DELIVERY RECEIPTS TO</string> <string name="settings_section_title_delivery_receipts">SEND DELIVERY RECEIPTS TO</string>
<string name="settings_restart_app">Restart</string> <string name="settings_restart_app">Restart</string>
<string name="settings_shutdown">Shutdown</string> <string name="settings_shutdown">Shutdown</string>
@@ -1035,6 +1073,7 @@
<string name="settings_section_title_themes">THEMES</string> <string name="settings_section_title_themes">THEMES</string>
<string name="settings_section_title_profile_images">Profile images</string> <string name="settings_section_title_profile_images">Profile images</string>
<string name="settings_section_title_messages">MESSAGES AND FILES</string> <string name="settings_section_title_messages">MESSAGES AND FILES</string>
<string name="settings_section_title_private_message_routing">PRIVATE MESSAGE ROUTING</string>
<string name="settings_section_title_calls">CALLS</string> <string name="settings_section_title_calls">CALLS</string>
<string name="settings_section_title_network_connection">Network connection</string> <string name="settings_section_title_network_connection">Network connection</string>
<string name="settings_section_title_incognito">Incognito mode</string> <string name="settings_section_title_incognito">Incognito mode</string>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="#5f6368"><path d="M629-446.5H235.48q-13.79 0-23.64-9.79-9.84-9.79-9.84-23.5t9.84-23.71q9.85-10 23.64-10H629L455.79-686.71Q445.5-697 445.25-710.5t10.25-24.48q10.5-10.52 24-10.27t23.81 10.57L734.1-503.59q4.9 4.91 7.65 10.97 2.75 6.06 2.75 12.78 0 6.71-2.75 12.78Q739-461 734.5-456.5l-231 231q-11 11-23.75 10.5t-23.25-11.02Q446-237 446-250.42q0-13.41 10.5-23.58L629-446.5Z"/></svg>

After

Width:  |  Height:  |  Size: 472 B

@@ -0,0 +1,4 @@
<svg height="24" viewBox="0 -960 960 960" width="24" fill="#000000" xmlns="http://www.w3.org/2000/svg">
<path d="M 831.5 -480 L 657 -656.5 C 651.667 -662.167 648.917 -668.833 648.75 -676.5 C 648.583 -684.167 651.333 -690.833 657 -696.5 C 662.667 -702.5 669.333 -705.5 677 -705.5 C 684.667 -705.5 691.5 -702.667 697.5 -697 L 894 -500.5 C 897 -497.167 899.25 -493.917 900.75 -490.75 C 902.25 -487.583 903 -484 903 -480 C 903 -476 902.25 -472.417 900.75 -469.25 C 899.25 -466.083 897 -463 894 -460 L 697.5 -263.5 C 691.5 -257.5 684.667 -254.583 677 -254.75 C 669.333 -254.917 662.667 -257.833 657 -263.5 C 651 -269.5 648.167 -276.25 648.5 -283.75 C 648.833 -291.25 651.667 -297.833 657 -303.5 L 831.5 -480 Z M 128.5 -480 L 303 -303.5 C 308.333 -297.833 311.083 -291.167 311.25 -283.5 C 311.417 -275.833 308.667 -269.167 303 -263.5 C 297.333 -257.5 290.667 -254.5 283 -254.5 C 275.333 -254.5 268.667 -257.5 263 -263.5 L 66.5 -460 C 63.167 -463 60.833 -466.083 59.5 -469.25 C 58.167 -472.417 57.5 -476 57.5 -480 C 57.5 -484 58.167 -487.583 59.5 -490.75 C 60.833 -493.917 63.167 -497.167 66.5 -500.5 L 263 -697 C 268.667 -702.667 275.333 -705.417 283 -705.25 C 290.667 -705.083 297.5 -702.167 303.5 -696.5 C 309.167 -690.5 311.833 -683.75 311.5 -676.25 C 311.167 -668.75 308.333 -662.167 303 -656.5 L 128.5 -480 Z" transform="matrix(0.9999999999999999, 0, 0, 0.9999999999999999, 0, 0)"/>
<rect x="123" y="-514" width="711.266" height="68" style="stroke: rgb(0, 0, 0);" transform="matrix(0.9999999999999999, 0, 0, 0.9999999999999999, 0, 0)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+4 -4
View File
@@ -26,11 +26,11 @@ android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11 kotlin.jvm.target=11
android.version_name=5.7.2 android.version_name=5.8-beta.2
android.version_code=204 android.version_code=210
desktop.version_name=5.7.2 desktop.version_name=5.8-beta.2
desktop.version_code=43 desktop.version_code=47
kotlin.version=1.9.23 kotlin.version=1.9.23
gradle.plugin.version=8.2.0 gradle.plugin.version=8.2.0
@@ -14,6 +14,7 @@ module Directory.Events
DirectoryRole (..), DirectoryRole (..),
SDirectoryRole (..), SDirectoryRole (..),
crDirectoryEvent, crDirectoryEvent,
directoryCmdTag,
viewName, viewName,
) )
where where
@@ -21,6 +22,8 @@ where
import Control.Applicative ((<|>)) import Control.Applicative ((<|>))
import Data.Attoparsec.Text (Parser) import Data.Attoparsec.Text (Parser)
import qualified Data.Attoparsec.Text as A import qualified Data.Attoparsec.Text as A
import Data.Char (isSpace)
import Data.Either (fromRight)
import Data.Functor (($>)) import Data.Functor (($>))
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
@@ -34,13 +37,11 @@ import Simplex.Chat.Types
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util ((<$?>)) import Simplex.Messaging.Util ((<$?>))
import Data.Char (isSpace)
import Data.Either (fromRight)
data DirectoryEvent data DirectoryEvent
= DEContactConnected Contact = DEContactConnected Contact
| DEGroupInvitation {contact :: Contact, groupInfo :: GroupInfo, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} | DEGroupInvitation {contact :: Contact, groupInfo :: GroupInfo, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole}
| DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember} | DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember}
| DEGroupUpdated {contactId :: ContactId, fromGroup :: GroupInfo, toGroup :: GroupInfo} | DEGroupUpdated {contactId :: ContactId, fromGroup :: GroupInfo, toGroup :: GroupInfo}
| DEContactRoleChanged GroupInfo ContactId GroupMemberRole -- contactId here is the contact whose role changed | DEContactRoleChanged GroupInfo ContactId GroupMemberRole -- contactId here is the contact whose role changed
| DEServiceRoleChanged GroupInfo GroupMemberRole | DEServiceRoleChanged GroupInfo GroupMemberRole
@@ -140,25 +141,26 @@ directoryCmdP =
cmdStrP = cmdStrP =
(tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t))) (tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t)))
<|> pure (ADC SDRUser DCUnknownCommand) <|> pure (ADC SDRUser DCUnknownCommand)
tagP = A.takeTill (== ' ') >>= \case tagP =
"help" -> u DCHelp_ A.takeTill (== ' ') >>= \case
"h" -> u DCHelp_ "help" -> u DCHelp_
"next" -> u DCSearchNext_ "h" -> u DCHelp_
"all" -> u DCAllGroups_ "next" -> u DCSearchNext_
"new" -> u DCRecentGroups_ "all" -> u DCAllGroups_
"submit" -> u DCSubmitGroup_ "new" -> u DCRecentGroups_
"confirm" -> u DCConfirmDuplicateGroup_ "submit" -> u DCSubmitGroup_
"list" -> u DCListUserGroups_ "confirm" -> u DCConfirmDuplicateGroup_
"ls" -> u DCListUserGroups_ "list" -> u DCListUserGroups_
"delete" -> u DCDeleteGroup_ "ls" -> u DCListUserGroups_
"approve" -> su DCApproveGroup_ "delete" -> u DCDeleteGroup_
"reject" -> su DCRejectGroup_ "approve" -> su DCApproveGroup_
"suspend" -> su DCSuspendGroup_ "reject" -> su DCRejectGroup_
"resume" -> su DCResumeGroup_ "suspend" -> su DCSuspendGroup_
"last" -> su DCListLastGroups_ "resume" -> su DCResumeGroup_
"exec" -> su DCExecuteCommand_ "last" -> su DCListLastGroups_
"x" -> su DCExecuteCommand_ "exec" -> su DCExecuteCommand_
_ -> fail "bad command tag" "x" -> su DCExecuteCommand_
_ -> fail "bad command tag"
where where
u = pure . ADCT SDRUser u = pure . ADCT SDRUser
su = pure . ADCT SDRSuperUser su = pure . ADCT SDRSuperUser
@@ -192,3 +194,23 @@ directoryCmdP =
viewName :: String -> String viewName :: String -> String
viewName n = if ' ' `elem` n then "'" <> n <> "'" else n viewName n = if ' ' `elem` n then "'" <> n <> "'" else n
directoryCmdTag :: DirectoryCmd r -> Text
directoryCmdTag = \case
DCHelp -> "help"
DCSearchGroup _ -> "search"
DCSearchNext -> "next"
DCAllGroups -> "all"
DCRecentGroups -> "new"
DCSubmitGroup _ -> "submit"
DCConfirmDuplicateGroup {} -> "confirm"
DCListUserGroups -> "list"
DCDeleteGroup {} -> "delete"
DCApproveGroup {} -> "approve"
DCRejectGroup {} -> "reject"
DCSuspendGroup {} -> "suspend"
DCResumeGroup {} -> "resume"
DCListLastGroups _ -> "last"
DCExecuteCommand _ -> "exec"
DCUnknownCommand -> "unknown"
DCCommandError _ -> "error"
@@ -2,9 +2,9 @@
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE MultiWayIf #-}
module Directory.Service module Directory.Service
( welcomeGetOpts, ( welcomeGetOpts,
@@ -15,6 +15,7 @@ where
import Control.Concurrent (forkIO) import Control.Concurrent (forkIO)
import Control.Concurrent.Async import Control.Concurrent.Async
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Logger.Simple
import Control.Monad import Control.Monad
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import Data.Maybe (fromMaybe, maybeToList) import Data.Maybe (fromMaybe, maybeToList)
@@ -37,7 +38,7 @@ import Simplex.Chat.Options
import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Protocol (MsgContent (..))
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Chat.View (serializeChatResponse, simplexChatContact) import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName)
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM import qualified Simplex.Messaging.TMap as TM
@@ -96,9 +97,11 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
DEUnsupportedMessage _ct _ciId -> pure () DEUnsupportedMessage _ct _ciId -> pure ()
DEItemEditIgnored _ct -> pure () DEItemEditIgnored _ct -> pure ()
DEItemDeleteIgnored _ct -> pure () DEItemDeleteIgnored _ct -> pure ()
DEContactCommand ct ciId aCmd -> case aCmd of DEContactCommand ct ciId (ADC sUser cmd) -> do
ADC SDRUser cmd -> deUserCommand env ct ciId cmd logInfo $ "command received " <> directoryCmdTag cmd
ADC SDRSuperUser cmd -> deSuperUserCommand ct ciId cmd case sUser of
SDRUser -> deUserCommand env ct ciId cmd
SDRSuperUser -> deSuperUserCommand ct ciId cmd
where where
withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId
notifySuperUsers s = withSuperUsers $ \contactId -> sendMessage' cc contactId s notifySuperUsers s = withSuperUsers $ \contactId -> sendMessage' cc contactId s
@@ -107,7 +110,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
withGroupReg GroupInfo {groupId, localDisplayName} err action = do withGroupReg GroupInfo {groupId, localDisplayName} err action = do
atomically (getGroupReg st groupId) >>= \case atomically (getGroupReg st groupId) >>= \case
Just gr -> action gr Just gr -> action gr
Nothing -> putStrLn $ T.unpack $ "Error: " <> err <> ", group: " <> localDisplayName <> ", can't find group registration ID " <> tshow groupId Nothing -> logError $ "Error: " <> err <> ", group: " <> localDisplayName <> ", can't find group registration ID " <> tshow groupId
groupInfoText GroupProfile {displayName = n, fullName = fn, description = d} = groupInfoText GroupProfile {displayName = n, fullName = fn, description = d} =
n <> (if n == fn || T.null fn then "" else " (" <> fn <> ")") <> maybe "" ("\nWelcome message:\n" <>) d n <> (if n == fn || T.null fn then "" else " (" <> fn <> ")") <> maybe "" ("\nWelcome message:\n" <>) d
userGroupReference gr GroupInfo {groupProfile = GroupProfile {displayName}} = userGroupReference' gr displayName userGroupReference gr GroupInfo {groupProfile = GroupProfile {displayName}} = userGroupReference' gr displayName
@@ -152,23 +155,25 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
deContactConnected :: Contact -> IO () deContactConnected :: Contact -> IO ()
deContactConnected ct = when (contactDirect ct) $ do deContactConnected ct = when (contactDirect ct) $ do
unless testing $ putStrLn $ T.unpack (localDisplayName' ct) <> " connected" logInfo $ (viewContactName ct) <> " connected"
sendMessage cc ct $ sendMessage cc ct $
"Welcome to " <> serviceName <> " service!\n\ ("Welcome to " <> serviceName <> " service!\n")
\Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\ <> "Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\
\For example, send _privacy_ to find groups about privacy.\n\ \For example, send _privacy_ to find groups about privacy.\n\
\Or send */all* or */new* to list groups.\n\n\ \Or send */all* or */new* to list groups.\n\n\
\Content and privacy policy: https://simplex.chat/docs/directory.html" \Content and privacy policy: https://simplex.chat/docs/directory.html"
deGroupInvitation :: Contact -> GroupInfo -> GroupMemberRole -> GroupMemberRole -> IO () deGroupInvitation :: Contact -> GroupInfo -> GroupMemberRole -> GroupMemberRole -> IO ()
deGroupInvitation ct g@GroupInfo {groupProfile = GroupProfile {displayName, fullName}} fromMemberRole memberRole = do deGroupInvitation ct g@GroupInfo {groupProfile = GroupProfile {displayName, fullName}} fromMemberRole memberRole = do
logInfo $ "invited to group " <> viewGroupName g <> " by " <> viewContactName ct
case badRolesMsg $ groupRolesStatus fromMemberRole memberRole of case badRolesMsg $ groupRolesStatus fromMemberRole memberRole of
Just msg -> sendMessage cc ct msg Just msg -> sendMessage cc ct msg
Nothing -> getDuplicateGroup g >>= \case Nothing ->
Just DGUnique -> processInvitation ct g getDuplicateGroup g >>= \case
Just DGRegistered -> askConfirmation Just DGUnique -> processInvitation ct g
Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g Just DGRegistered -> askConfirmation
Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g
Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers."
where where
askConfirmation = do askConfirmation = do
ugrId <- addGroupReg st ct g GRSPendingConfirmation ugrId <- addGroupReg st ct g GRSPendingConfirmation
@@ -205,7 +210,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
_ -> Nothing _ -> Nothing
deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO () deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO ()
deServiceJoinedGroup ctId g owner = deServiceJoinedGroup ctId g owner = do
logInfo $ "service joined group " <> viewGroupName g
withGroupReg g "joined group" $ \gr -> withGroupReg g "joined group" $ \gr ->
when (ctId `isOwner` gr) $ do when (ctId `isOwner` gr) $ do
setGroupRegOwner st gr owner setGroupRegOwner st gr owner
@@ -214,7 +220,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case
CRGroupLinkCreated {connReqContact} -> do CRGroupLinkCreated {connReqContact} -> do
setGroupStatus st gr GRSPendingUpdate setGroupStatus st gr GRSPendingUpdate
notifyOwner gr notifyOwner
gr
"Created the public link to join the group via this directory service that is always online.\n\n\ "Created the public link to join the group via this directory service that is always online.\n\n\
\Please add it to the group welcome message.\n\ \Please add it to the group welcome message.\n\
\For example, add:" \For example, add:"
@@ -228,24 +235,26 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
_ -> notifyOwner gr $ unexpectedError "can't create group link" _ -> notifyOwner gr $ unexpectedError "can't create group link"
deGroupUpdated :: ContactId -> GroupInfo -> GroupInfo -> IO () deGroupUpdated :: ContactId -> GroupInfo -> GroupInfo -> IO ()
deGroupUpdated ctId fromGroup toGroup = deGroupUpdated ctId fromGroup toGroup = do
logInfo $ "group updated " <> viewGroupName toGroup
unless (sameProfile p p') $ do unless (sameProfile p p') $ do
withGroupReg toGroup "group updated" $ \gr -> do withGroupReg toGroup "group updated" $ \gr -> do
let userGroupRef = userGroupReference gr toGroup let userGroupRef = userGroupReference gr toGroup
readTVarIO (groupRegStatus gr) >>= \case readTVarIO (groupRegStatus gr) >>= \case
GRSPendingConfirmation -> pure () GRSPendingConfirmation -> pure ()
GRSProposed -> pure () GRSProposed -> pure ()
GRSPendingUpdate -> groupProfileUpdate >>= \case GRSPendingUpdate ->
GPNoServiceLink -> groupProfileUpdate >>= \case
when (ctId `isOwner` gr) $ notifyOwner gr $ "The profile updated for " <> userGroupRef <> ", but the group link is not added to the welcome message." GPNoServiceLink ->
GPServiceLinkAdded when (ctId `isOwner` gr) $ notifyOwner gr $ "The profile updated for " <> userGroupRef <> ", but the group link is not added to the welcome message."
| ctId `isOwner` gr -> groupLinkAdded gr GPServiceLinkAdded
| otherwise -> notifyOwner gr "The group link is added by another group member, your registration will not be processed.\n\nPlease update the group profile yourself." | ctId `isOwner` gr -> groupLinkAdded gr
GPServiceLinkRemoved -> when (ctId `isOwner` gr) $ notifyOwner gr $ "The group link of " <> userGroupRef <> " is removed from the welcome message, please add it." | otherwise -> notifyOwner gr "The group link is added by another group member, your registration will not be processed.\n\nPlease update the group profile yourself."
GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr GPServiceLinkRemoved -> when (ctId `isOwner` gr) $ notifyOwner gr $ "The group link of " <> userGroupRef <> " is removed from the welcome message, please add it."
GPServiceLinkError -> do GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr
when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers." GPServiceLinkError -> do
putStrLn $ "Error: no group link for " <> userGroupRef when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers."
logError $ "Error: no group link for " <> T.pack userGroupRef
GRSPendingApproval n -> processProfileChange gr $ n + 1 GRSPendingApproval n -> processProfileChange gr $ n + 1
GRSActive -> processProfileChange gr 1 GRSActive -> processProfileChange gr 1
GRSSuspended -> processProfileChange gr 1 GRSSuspended -> processProfileChange gr 1
@@ -288,7 +297,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
notifyOwner gr $ "The group " <> userGroupRef <> " is updated!\nIt is hidden from the directory until approved." notifyOwner gr $ "The group " <> userGroupRef <> " is updated!\nIt is hidden from the directory until approved."
notifySuperUsers $ "The group " <> groupRef <> " is updated." notifySuperUsers $ "The group " <> groupRef <> " is updated."
checkRolesSendToApprove gr n' checkRolesSendToApprove gr n'
GPServiceLinkError -> putStrLn $ "Error: no group link for " <> groupRef <> " pending approval." GPServiceLinkError -> logError $ "Error: no group link for " <> T.pack groupRef <> " pending approval."
groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId) groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId)
where where
profileUpdate = \case profileUpdate = \case
@@ -297,7 +306,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
groupLink2 = safeDecodeUtf8 $ strEncode $ simplexChatContact connReqContact groupLink2 = safeDecodeUtf8 $ strEncode $ simplexChatContact connReqContact
hadLinkBefore = groupLink1 `isInfix` description p || groupLink2 `isInfix` description p hadLinkBefore = groupLink1 `isInfix` description p || groupLink2 `isInfix` description p
hasLinkNow = groupLink1 `isInfix` description p' || groupLink2 `isInfix` description p' hasLinkNow = groupLink1 `isInfix` description p' || groupLink2 `isInfix` description p'
in if in if
| hadLinkBefore && hasLinkNow -> GPHasServiceLink | hadLinkBefore && hasLinkNow -> GPHasServiceLink
| hadLinkBefore -> GPServiceLinkRemoved | hadLinkBefore -> GPServiceLinkRemoved
| hasLinkNow -> GPServiceLinkAdded | hasLinkNow -> GPServiceLinkAdded
@@ -311,18 +320,20 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO () sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO ()
sendToApprove GroupInfo {groupProfile = p@GroupProfile {displayName, image = image'}} GroupReg {dbGroupId, dbContactId} gaId = do sendToApprove GroupInfo {groupProfile = p@GroupProfile {displayName, image = image'}} GroupReg {dbGroupId, dbContactId} gaId = do
ct_ <- getContact cc dbContactId ct_ <- getContact cc dbContactId
gr_ <- getGroupAndSummary cc dbGroupId gr_ <- getGroupAndSummary cc dbGroupId
let membersStr = maybe "" (\(_, s) -> "_" <> tshow (currentMembers s) <> " members_\n") gr_ let membersStr = maybe "" (\(_, s) -> "_" <> tshow (currentMembers s) <> " members_\n") gr_
text = maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_ text =
<> "\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:" maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_
<> ("\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:")
msg = maybe (MCText text) (\image -> MCImage {text, image}) image' msg = maybe (MCText text) (\image -> MCImage {text, image}) image'
withSuperUsers $ \cId -> do withSuperUsers $ \cId -> do
sendComposedMessage' cc cId Nothing msg sendComposedMessage' cc cId Nothing msg
sendMessage' cc cId $ "/approve " <> show dbGroupId <> ":" <> viewName (T.unpack displayName) <> " " <> show gaId sendMessage' cc cId $ "/approve " <> show dbGroupId <> ":" <> viewName (T.unpack displayName) <> " " <> show gaId
deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO () deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO ()
deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = do
logInfo $ "contact ID " <> tshow ctId <> " role changed in group " <> viewGroupName g <> " to " <> tshow contactRole
withGroupReg g "contact role changed" $ \gr -> do withGroupReg g "contact role changed" $ \gr -> do
let userGroupRef = userGroupReference gr g let userGroupRef = userGroupReference gr g
uCtRole = "Your role in the group " <> userGroupRef <> " is changed to " <> ctRole uCtRole = "Your role in the group " <> userGroupRef <> " is changed to " <> ctRole
@@ -348,6 +359,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO () deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO ()
deServiceRoleChanged g serviceRole = do deServiceRoleChanged g serviceRole = do
logInfo $ "service role changed in group " <> viewGroupName g <> " to " <> tshow serviceRole
withGroupReg g "service role changed" $ \gr -> do withGroupReg g "service role changed" $ \gr -> do
let userGroupRef = userGroupReference gr g let userGroupRef = userGroupReference gr g
uSrvRole = serviceName <> " role in the group " <> userGroupRef <> " is changed to " <> srvRole uSrvRole = serviceName <> " role in the group " <> userGroupRef <> " is changed to " <> srvRole
@@ -371,11 +383,12 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
srvRole = "*" <> B.unpack (strEncode serviceRole) <> "*" srvRole = "*" <> B.unpack (strEncode serviceRole) <> "*"
suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")." suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")."
whenContactIsOwner gr action = whenContactIsOwner gr action =
getGroupMember gr >>= getGroupMember gr
mapM_ (\cm@GroupMember {memberRole} -> when (memberRole == GROwner && memberActive cm) action) >>= mapM_ (\cm@GroupMember {memberRole} -> when (memberRole == GROwner && memberActive cm) action)
deContactRemovedFromGroup :: ContactId -> GroupInfo -> IO () deContactRemovedFromGroup :: ContactId -> GroupInfo -> IO ()
deContactRemovedFromGroup ctId g = deContactRemovedFromGroup ctId g = do
logInfo $ "contact ID " <> tshow ctId <> " removed from group " <> viewGroupName g
withGroupReg g "contact removed" $ \gr -> do withGroupReg g "contact removed" $ \gr -> do
when (ctId `isOwner` gr) $ do when (ctId `isOwner` gr) $ do
setGroupStatus st gr GRSRemoved setGroupStatus st gr GRSRemoved
@@ -383,7 +396,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)." notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)."
deContactLeftGroup :: ContactId -> GroupInfo -> IO () deContactLeftGroup :: ContactId -> GroupInfo -> IO ()
deContactLeftGroup ctId g = deContactLeftGroup ctId g = do
logInfo $ "contact ID " <> tshow ctId <> " left group " <> viewGroupName g
withGroupReg g "contact left" $ \gr -> do withGroupReg g "contact left" $ \gr -> do
when (ctId `isOwner` gr) $ do when (ctId `isOwner` gr) $ do
setGroupStatus st gr GRSRemoved setGroupStatus st gr GRSRemoved
@@ -391,7 +405,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)." notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)."
deServiceRemovedFromGroup :: GroupInfo -> IO () deServiceRemovedFromGroup :: GroupInfo -> IO ()
deServiceRemovedFromGroup g = deServiceRemovedFromGroup g = do
logInfo $ "service removed from group " <> viewGroupName g
withGroupReg g "service removed" $ \gr -> do withGroupReg g "service removed" $ \gr -> do
setGroupStatus st gr GRSRemoved setGroupStatus st gr GRSRemoved
notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory."
@@ -402,11 +417,15 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
DCHelp -> DCHelp ->
sendMessage cc ct $ sendMessage cc ct $
"You must be the owner to add the group to the directory:\n\ "You must be the owner to add the group to the directory:\n\
\1. Invite " <> serviceName <> " bot to your group as *admin* (you can send `/list` to see all groups you submitted).\n\ \1. Invite "
\2. " <> serviceName <> " bot will create a public group link for the new members to join even when you are offline.\n\ <> serviceName
\3. You will then need to add this link to the group welcome message.\n\ <> " bot to your group as *admin* (you can send `/list` to see all groups you submitted).\n\
\4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\ \2. "
\Start from inviting the bot to your group as admin - it will guide you through the process" <> serviceName
<> " bot will create a public group link for the new members to join even when you are offline.\n\
\3. You will then need to add this link to the group welcome message.\n\
\4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\
\Start from inviting the bot to your group as admin - it will guide you through the process"
DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s
DCSearchNext -> DCSearchNext ->
atomically (TM.lookup (contactId' ct) searchRequests) >>= \case atomically (TM.lookup (contactId' ct) searchRequests) >>= \case
@@ -434,13 +453,13 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found" Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found"
Just g@GroupInfo {groupProfile = GroupProfile {displayName}} Just g@GroupInfo {groupProfile = GroupProfile {displayName}}
| displayName == gName -> | displayName == gName ->
readTVarIO groupRegStatus >>= \case readTVarIO groupRegStatus >>= \case
GRSPendingConfirmation -> do GRSPendingConfirmation -> do
getDuplicateGroup g >>= \case getDuplicateGroup g >>= \case
Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers."
Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g
_ -> processInvitation ct g _ -> processInvitation ct g
_ -> sendReply $ "Error: the group ID " <> show ugrId <> " (" <> T.unpack displayName <> ") is not pending confirmation." _ -> sendReply $ "Error: the group ID " <> show ugrId <> " (" <> T.unpack displayName <> ") is not pending confirmation."
| otherwise -> sendReply $ "Group ID " <> show ugrId <> " has the display name " <> T.unpack displayName | otherwise -> sendReply $ "Group ID " <> show ugrId <> " has the display name " <> T.unpack displayName
DCListUserGroups -> DCListUserGroups ->
atomically (getUserGroupRegs st $ contactId' ct) >>= \grs -> do atomically (getUserGroupRegs st $ contactId' ct) >>= \grs -> do
@@ -462,7 +481,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
let gs' = takeTop searchResults gs let gs' = takeTop searchResults gs
moreGroups = length gs - length gs' moreGroups = length gs - length gs'
more = if moreGroups > 0 then ", sending top " <> show (length gs') else "" more = if moreGroups > 0 then ", sending top " <> show (length gs') else ""
sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "." sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "."
updateSearchRequest (STSearch s) $ groupIds gs' updateSearchRequest (STSearch s) $ groupIds gs'
sendFoundGroups gs' moreGroups sendFoundGroups gs' moreGroups
sendAllGroups takeFirst sortName searchType = \case sendAllGroups takeFirst sortName searchType = \case
@@ -499,74 +518,76 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ msg = maybe (MCText text) (\image -> MCImage {text, image}) image_
sendComposedMessage cc ct Nothing msg sendComposedMessage cc ct Nothing msg
when (moreGroups > 0) $ when (moreGroups > 0) $
sendComposedMessage cc ct Nothing $ MCText $ "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)." sendComposedMessage cc ct Nothing $
MCText $
"Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)."
deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO () deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO ()
deSuperUserCommand ct ciId cmd deSuperUserCommand ct ciId cmd
| superUser `elem` superUsers = case cmd of | superUser `elem` superUsers = case cmd of
DCApproveGroup {groupId, displayName = n, groupApprovalId} -> do DCApproveGroup {groupId, displayName = n, groupApprovalId} ->
getGroupAndReg groupId n >>= \case getGroupAndReg groupId n >>= \case
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
Just (g, gr) -> Just (g, gr) ->
readTVarIO (groupRegStatus gr) >>= \case readTVarIO (groupRegStatus gr) >>= \case
GRSPendingApproval gaId GRSPendingApproval gaId
| gaId == groupApprovalId -> do | gaId == groupApprovalId -> do
getDuplicateGroup g >>= \case getDuplicateGroup g >>= \case
Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers." Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers."
Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory." Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory."
_ -> do _ -> do
getGroupRolesStatus g gr >>= \case getGroupRolesStatus g gr >>= \case
Just GRSOk -> do Just GRSOk -> do
setGroupStatus st gr GRSActive setGroupStatus st gr GRSActive
sendReply "Group approved!" sendReply "Group approved!"
notifyOwner gr $ "The group " <> userGroupReference' gr n <> " is approved and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved." notifyOwner gr $ "The group " <> userGroupReference' gr n <> " is approved and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved."
Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin
Just GRSContactNotOwner -> replyNotApproved "user is not an owner." Just GRSContactNotOwner -> replyNotApproved "user is not an owner."
Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin
Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers." Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers."
where where
replyNotApproved reason = sendReply $ "Group is not approved: " <> reason replyNotApproved reason = sendReply $ "Group is not approved: " <> reason
serviceNotAdmin = serviceName <> " is not an admin." serviceNotAdmin = serviceName <> " is not an admin."
| otherwise -> sendReply "Incorrect approval code" | otherwise -> sendReply "Incorrect approval code"
_ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval." _ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval."
where where
groupRef = groupReference' groupId n groupRef = groupReference' groupId n
DCRejectGroup _gaId _gName -> pure () DCRejectGroup _gaId _gName -> pure ()
DCSuspendGroup groupId gName -> do DCSuspendGroup groupId gName -> do
let groupRef = groupReference' groupId gName let groupRef = groupReference' groupId gName
getGroupAndReg groupId gName >>= \case getGroupAndReg groupId gName >>= \case
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
Just (_, gr) -> Just (_, gr) ->
readTVarIO (groupRegStatus gr) >>= \case readTVarIO (groupRegStatus gr) >>= \case
GRSActive -> do GRSActive -> do
setGroupStatus st gr GRSSuspended setGroupStatus st gr GRSSuspended
notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is suspended and hidden from directory. Please contact the administrators." notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is suspended and hidden from directory. Please contact the administrators."
sendReply "Group suspended!" sendReply "Group suspended!"
_ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended." _ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended."
DCResumeGroup groupId gName -> do DCResumeGroup groupId gName -> do
let groupRef = groupReference' groupId gName let groupRef = groupReference' groupId gName
getGroupAndReg groupId gName >>= \case getGroupAndReg groupId gName >>= \case
Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)."
Just (_, gr) -> Just (_, gr) ->
readTVarIO (groupRegStatus gr) >>= \case readTVarIO (groupRegStatus gr) >>= \case
GRSSuspended -> do GRSSuspended -> do
setGroupStatus st gr GRSActive setGroupStatus st gr GRSActive
notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is listed in the directory again!" notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is listed in the directory again!"
sendReply "Group listing resumed!" sendReply "Group listing resumed!"
_ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed." _ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed."
DCListLastGroups count -> DCListLastGroups count ->
readTVarIO (groupRegs st) >>= \grs -> do readTVarIO (groupRegs st) >>= \grs -> do
sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "") sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "")
void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do
ct_ <- getContact cc dbContactId ct_ <- getContact cc dbContactId
let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_ let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_
sendGroupInfo ct gr dbGroupId $ Just ownerStr sendGroupInfo ct gr dbGroupId $ Just ownerStr
DCExecuteCommand cmdStr -> DCExecuteCommand cmdStr ->
sendChatCmdStr cc cmdStr >>= \r -> do sendChatCmdStr cc cmdStr >>= \r -> do
ts <- getCurrentTime ts <- getCurrentTime
tz <- getCurrentTimeZone tz <- getCurrentTimeZone
sendReply $ serializeChatResponse (Nothing, Just user) ts tz Nothing r sendReply $ serializeChatResponse (Nothing, Just user) ts tz Nothing r
DCCommandError tag -> sendReply $ "Command error: " <> show tag DCCommandError tag -> sendReply $ "Command error: " <> show tag
| otherwise = sendReply "You are not allowed to use this command" | otherwise = sendReply "You are not allowed to use this command"
where where
superUser = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct} superUser = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct}
@@ -577,8 +598,9 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
getGroup cc gId getGroup cc gId
$>>= \g@GroupInfo {groupProfile = GroupProfile {displayName}} -> $>>= \g@GroupInfo {groupProfile = GroupProfile {displayName}} ->
if displayName == gName if displayName == gName
then atomically (getGroupReg st gId) then
$>>= \gr -> pure $ Just (g, gr) atomically (getGroupReg st gId)
$>>= \gr -> pure $ Just (g, gr)
else pure Nothing else pure Nothing
sendGroupInfo :: Contact -> GroupReg -> GroupId -> Maybe Text -> IO () sendGroupInfo :: Contact -> GroupReg -> GroupId -> Maybe Text -> IO ()
@@ -14,7 +14,7 @@ permalink: "/blog/20221206-simplex-chat-v4.3-voice-messages.html"
## SimpleX Chat reviews ## SimpleX Chat reviews
Since we published [the security assessment of SimpleX Chat](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) completed by Trail of Bits in November, several sites published the reviews and included it in their recommendations: Since we published [the security assessment of SimpleX Chat](./20221108-simplex-chat-v4.2-security-audit-new-website.md) completed by Trail of Bits in November, several sites published the reviews and included it in their recommendations:
- Privacy Guides added SimpleX Chat to [the recommended private and secure messengers](https://www.privacyguides.org/real-time-communication/#simplex-chat). - Privacy Guides added SimpleX Chat to [the recommended private and secure messengers](https://www.privacyguides.org/real-time-communication/#simplex-chat).
- Mike Kuketz a well-known security expert published [the review of SimpleX Chat](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) and added it to [the messenger matrix](https://www.messenger-matrix.de). - Mike Kuketz a well-known security expert published [the review of SimpleX Chat](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) and added it to [the messenger matrix](https://www.messenger-matrix.de).
@@ -42,7 +42,7 @@ Many large tech companies prioritizing value extraction over value creation earn
We started working full-time on the project in 2021 when [Portman Wills](https://www.linkedin.com/in/portmanwills/) and [Peter Briffett](https://www.linkedin.com/in/peterbriffett/) (the founders of [Wagestream](https://wagestream.com/en/) where I led the engineering team) supported the company very early on, and several other angel investors joined later. In July 2022 SimpleX Chat raised a pre-seed funding from the VC fund [Village Global](https://www.villageglobal.vc) - its co-founder [Ben Casnocha](https://casnocha.com) was very excited about our vision of privacy-first fully decentralized messaging and community platform, both for the individual users and for the companies, independent of any crypto-currencies, that might grow to replace large centralized platforms, such as WhatsApp, Telegram and Signal. We started working full-time on the project in 2021 when [Portman Wills](https://www.linkedin.com/in/portmanwills/) and [Peter Briffett](https://www.linkedin.com/in/peterbriffett/) (the founders of [Wagestream](https://wagestream.com/en/) where I led the engineering team) supported the company very early on, and several other angel investors joined later. In July 2022 SimpleX Chat raised a pre-seed funding from the VC fund [Village Global](https://www.villageglobal.vc) - its co-founder [Ben Casnocha](https://casnocha.com) was very excited about our vision of privacy-first fully decentralized messaging and community platform, both for the individual users and for the companies, independent of any crypto-currencies, that might grow to replace large centralized platforms, such as WhatsApp, Telegram and Signal.
Overall we raised from our investors approximately $370,000 for a small share of the company to allow the project team working full time for almost two years, funding product design and development, infrastructure, and also [the security assessment by Trail of Bits](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html). A large part of this money is not spent yet. Overall we raised from our investors approximately $370,000 for a small share of the company to allow the project team working full time for almost two years, funding product design and development, infrastructure, and also [the security assessment by Trail of Bits](./20221108-simplex-chat-v4.2-security-audit-new-website.md). A large part of this money is not spent yet.
The project was hugely supported by the users as well - collectively, [you donated](https://github.com/simplex-chat/simplex-chat#help-us-with-donations) over $25,000. Without these donations the investment we raised would not be possible, because we believe that voluntary user donations can sustain the project in the long term it already covers all infrastructure costs. There are only two ways an Internet service can exist - either users are paying for it, or the users data becomes the product for the real customers, as happened with many large Internet companies. In the latter case the users are losing much more money than they are saving by giving away their privacy and the rights to the content they create on the centralized platforms. The project was hugely supported by the users as well - collectively, [you donated](https://github.com/simplex-chat/simplex-chat#help-us-with-donations) over $25,000. Without these donations the investment we raised would not be possible, because we believe that voluntary user donations can sustain the project in the long term it already covers all infrastructure costs. There are only two ways an Internet service can exist - either users are paying for it, or the users data becomes the product for the real customers, as happened with many large Internet companies. In the latter case the users are losing much more money than they are saving by giving away their privacy and the rights to the content they create on the centralized platforms.
@@ -50,7 +50,7 @@ Other limitations of the desktop app:
- you cannot send voice messages. - you cannot send voice messages.
- there is no support for calls yet. - there is no support for calls yet.
You can download the desktop app for Linux and Mac via [downloads page](https://simplex.chat/downloads). Windows version will be available soon. You can download the desktop app for Linux and Mac via [downloads page](../docs/DOWNLOADS.md). Windows version will be available soon.
## Group directory service and other group improvements ## Group directory service and other group improvements
@@ -40,7 +40,7 @@ This is only possible when both devices are connected to the same local network.
**On desktop** **On desktop**
If you don't have desktop app installed yet, [download it](https://simplex.chat/downloads/) and create any chat profile - you don't need to use it, and when you create it there are no server requests sent and no accounts are created. Think about it as about user profile on your computer. If you don't have desktop app installed yet, [download it](../docs/DOWNLOADS.md) and create any chat profile - you don't need to use it, and when you create it there are no server requests sent and no accounts are created. Think about it as about user profile on your computer.
Then in desktop app settings choose *Link a mobile* - it will show a QR code. Then in desktop app settings choose *Link a mobile* - it will show a QR code.
@@ -10,6 +10,8 @@ permalink: "/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ra
# SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm # SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm
**Published:** Mar 14, 2024
This is a major upgrade for SimpleX messaging protocols, we are really proud to present the results of the hard work of our whole team on the [Pi day](https://en.wikipedia.org/wiki/Pi_Day). This is a major upgrade for SimpleX messaging protocols, we are really proud to present the results of the hard work of our whole team on the [Pi day](https://en.wikipedia.org/wiki/Pi_Day).
This post also covers various aspects of end-to-end encryption, compares different messengers, and explains why and how quantum-resistant encryption is added to SimpleX Chat: This post also covers various aspects of end-to-end encryption, compares different messengers, and explains why and how quantum-resistant encryption is added to SimpleX Chat:
@@ -101,7 +103,7 @@ This attack is much less understood by the users, and forward secrecy does not p
Out of all encryption algorithms known to us only _Signal double ratchet algorithm_ (also referred to as _Signal algorithm_ or _double ratchet algorithm_, which is not the same as Signal messaging platform and protocols) provides the ability for the encryption security to recover after break-ins attacks. This recovery happens automatically and transparently to the users, without them doing anything special or even knowing about break-in, by simply sending messages. Every time one of the communication parties replies to another party message, new random keys are generated and previously stolen keys become useless. Out of all encryption algorithms known to us only _Signal double ratchet algorithm_ (also referred to as _Signal algorithm_ or _double ratchet algorithm_, which is not the same as Signal messaging platform and protocols) provides the ability for the encryption security to recover after break-ins attacks. This recovery happens automatically and transparently to the users, without them doing anything special or even knowing about break-in, by simply sending messages. Every time one of the communication parties replies to another party message, new random keys are generated and previously stolen keys become useless.
Double ratchet algorithm is used in Signal, Cwtch and SimpleX Chat. This is why you cannot use SimpleX Chat profile on more than one device at the same time - the encryption scheme rotates the long term keys, randomly, and keys on another device become useless, as they would become useless for the attacker who stole them. Security always has some costs to the convenience. Double ratchet algorithm is used in Signal, Cwtch and SimpleX Chat. But Signal app by allowing to use the same profile on multiple devices compromises the break-in recovery function of Signal algorithm, as explained in [this paper](https://eprint.iacr.org/2021/626.pdf). Because of break-in recovery you cannot use SimpleX Chat profile on more than one device at the same time - the encryption scheme rotates the long term keys, randomly, and keys on another device become useless, as they would become useless for the attacker who stole them. Security always has some costs to the convenience.
### 5. Man-in-the-middle attack - mitigated by two-factor key exchange ### 5. Man-in-the-middle attack - mitigated by two-factor key exchange
@@ -33,7 +33,7 @@ For example, while WhatsApp messages are [end-to-end encrypted](https://faq.what
This is called [metadata](https://en.wikipedia.org/wiki/Metadata). It reveals a wealth of information about you and your connections, and in the hands of a centralized monopoly, this can and does get misused in incredibly dangerous ways. Once such metadata is logged, it can create very detailed profiles about who you are, everywhere youve been, and everyone youve ever spoken to. In settling for apps that normalize this while giving you the illusion of privacy in their marketing, we are doing ourselves a disservice by accepting this as the default. Collectively, we arent doing enough to protect ourselves and our social graph from this invasive overreach. This is called [metadata](https://en.wikipedia.org/wiki/Metadata). It reveals a wealth of information about you and your connections, and in the hands of a centralized monopoly, this can and does get misused in incredibly dangerous ways. Once such metadata is logged, it can create very detailed profiles about who you are, everywhere youve been, and everyone youve ever spoken to. In settling for apps that normalize this while giving you the illusion of privacy in their marketing, we are doing ourselves a disservice by accepting this as the default. Collectively, we arent doing enough to protect ourselves and our social graph from this invasive overreach.
When stored, aggregated and analyzed, this metadata provides ample information that could potentially incriminate someone or be submitted to authorities. When WhatsApp and Facebook Messenger enabled end-to-end encryption for messages, of course it was a welcome and widely celebrated change. But its important to remember that not all end-to-end encryption utilizes the same standards, [some implementations are more secure](https://simplex.chat/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.html#how-secure-is-end-to-end-encryption-in-different-messengers) than others, so its something that shouldnt necessarily be accepted at face value. More importantly: collecting and storing an obscene amount of metadata should invite global scrutiny, considering this data is often combined with whatever other information companies like Meta harvest about your identity (which is [a lot](https://www.vox.com/recode/23172691/meta-tracking-privacy-hospitals).) When stored, aggregated and analyzed, this metadata provides ample information that could potentially incriminate someone or be submitted to authorities. When WhatsApp and Facebook Messenger enabled end-to-end encryption for messages, of course it was a welcome and widely celebrated change. But its important to remember that not all end-to-end encryption utilizes the same standards, [some implementations are more secure](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md#how-secure-is-end-to-end-encryption-in-different-messengers) than others, so its something that shouldnt necessarily be accepted at face value. More importantly: collecting and storing an obscene amount of metadata should invite global scrutiny, considering this data is often combined with whatever other information companies like Meta harvest about your identity (which is [a lot](https://www.vox.com/recode/23172691/meta-tracking-privacy-hospitals).)
<img src="./images/20240416-metadata.png" class="float-to-right" width="38%"> <img src="./images/20240416-metadata.png" class="float-to-right" width="38%">
@@ -45,8 +45,8 @@ But we also need to acknowledge that the world is becoming increasingly dangerou
End-to-end encryption is a solid start, but it's just the beginning of our pursuit for true privacy and security. True privacy means that even when legal demands come knocking, there's no useful metadata to hand over. It's not enough to just protect the content of messages; we need consistent innovation in protecting metadata too. End-to-end encryption is a solid start, but it's just the beginning of our pursuit for true privacy and security. True privacy means that even when legal demands come knocking, there's no useful metadata to hand over. It's not enough to just protect the content of messages; we need consistent innovation in protecting metadata too.
Changing ingrained habits is tough, but your privacy is always worth the fight. Although giants like WhatsApp and Telegram may dominate global messaging for now, increasing concerns about data harvesting and AI-driven surveillance are fueling demand for alternatives. SimpleX Chat aims to be one of those strong alternatives, hence its radical focus on a decentralized framework with no user identifiers (in other words, nothing that uniquely identifies users on the protocol level to their contacts or to the relays) and extra optionality (self-hosting an [SMP server](https://simplex.chat/docs/server.html) or [XFTP server](https://simplex.chat/docs/xftp-server.html), access via Tor, [chat profiles](https://simplex.chat/docs/guide/chat-profiles.html) with incognito mode, etc.) Changing ingrained habits is tough, but your privacy is always worth the fight. Although giants like WhatsApp and Telegram may dominate global messaging for now, increasing concerns about data harvesting and AI-driven surveillance are fueling demand for alternatives. SimpleX Chat aims to be one of those strong alternatives, hence its radical focus on a decentralized framework with no user identifiers (in other words, nothing that uniquely identifies users on the protocol level to their contacts or to the relays) and extra optionality (self-hosting an [SMP server](../docs/SERVER.md) or [XFTP server](../docs/XFTP-SERVER.md), access via Tor, [chat profiles](../docs/guide/chat-profiles.md) with incognito mode, etc.)
As of today, most messaging alternatives, including SimpleX, will have some limitations. But with the limited resources we have, we are committed to daily progress towards creating a truly private messenger that anyone can use while maintaining the features that users have come to know and love in messaging interfaces. We want to be the prime example of a messenger that achieves genuine privacy without compromising it for convenience. We need to be able to reliably move away from small and niche use cases to endorsing and enforcing global standards for privacy and making it accessible for all users regardless of their technical expertise. As of today, most messaging alternatives, including SimpleX, will have some limitations. But with the limited resources we have, we are committed to daily progress towards creating a truly private messenger that anyone can use while maintaining the features that users have come to know and love in messaging interfaces. We want to be the prime example of a messenger that achieves genuine privacy without compromising it for convenience. We need to be able to reliably move away from small and niche use cases to endorsing and enforcing global standards for privacy and making it accessible for all users regardless of their technical expertise.
Were grateful for the users and [donors](https://github.com/simplex-chat/simplex-chat#help-us-with-donations) who have been following along on this journey thus far and helping with feedback, anything from bug reports to identifying potential risks. Building in the open has always been a necessity for transparency and ongoing [auditability](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html), because we dont want anyone to just take our word for it. [See for yourself](https://github.com/simplex-chat) and engage in the discussions. We fully expect you to hold us accountable to our word. Were grateful for the users and [donors](https://github.com/simplex-chat/simplex-chat#help-us-with-donations) who have been following along on this journey thus far and helping with feedback, anything from bug reports to identifying potential risks. Building in the open has always been a necessity for transparency and ongoing [auditability](./20221108-simplex-chat-v4.2-security-audit-new-website.md), because we dont want anyone to just take our word for it. [See for yourself](https://github.com/simplex-chat) and engage in the discussions. We fully expect you to hold us accountable to our word.
@@ -10,6 +10,8 @@ permalink: "/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user
# SimpleX network: legally binding transparency, v5.7 released with better calls and messages # SimpleX network: legally binding transparency, v5.7 released with better calls and messages
**Published:** Apr 26, 2024
What's new in v5.7: What's new in v5.7:
- [quantum resistant end-to-end encryption](#quantum-resistant-end-to-end-encryption) with all contacts. - [quantum resistant end-to-end encryption](#quantum-resistant-end-to-end-encryption) with all contacts.
- [forward and save messages](#forward-and-save-messages) without revealing the source. - [forward and save messages](#forward-and-save-messages) without revealing the source.
@@ -23,10 +25,10 @@ Also, we added Lithuanian interface language to the Android and desktop apps, th
We are committed to open-source, privacy and security. Here are the recent changes we made: We are committed to open-source, privacy and security. Here are the recent changes we made:
- We now have a [Transparency Reports](https://simplex.chat/transparency/) page. - We now have a [Transparency Reports](../docs/TRANSPARENCY.md) page.
- We updated our [Privacy Policy](https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md) to remove undefined terms "impermissible" and "acceptable", which would allow us to remove anything we don't like, without any clarity on what that is. You can see the edits [here](https://github.com/simplex-chat/simplex-chat/pull/4076/files). - We updated our [Privacy Policy](../PRIVACY.md) to remove undefined terms "impermissible" and "acceptable", which would allow us to remove anything we don't like, without any clarity on what that is. You can see the edits [here](https://github.com/simplex-chat/simplex-chat/pull/4076/files).
- We published a new page with [Frequently Asked Questions](https://simplex.chat/faq/), thanks to the guidance from users. - We published a new page with [Frequently Asked Questions](../docs/FAQ.md), thanks to the guidance from users.
- We also have a new [Security Policy](https://simplex.chat/security/) we welcome your feedback on it. - We also have a new [Security Policy](../docs/SECURITY.md) we welcome your feedback on it.
What do we mean by “legally binding transparency?”. It includes these principles: What do we mean by “legally binding transparency?”. It includes these principles:
- Accountability: an empty promise or commitment to transparency that is not legally binding is just marketing, and can provide opportunities for the organizations to be misleading or not disclose important information that can affect their users privacy and security. - Accountability: an empty promise or commitment to transparency that is not legally binding is just marketing, and can provide opportunities for the organizations to be misleading or not disclose important information that can affect their users privacy and security.
@@ -0,0 +1,79 @@
---
layout: layouts/article.html
title: "SimpleX: Redefining Privacy by Making Hard Choices"
date: 2024-05-16
previewBody: blog_previews/20240516.html
image: images/simplex-explained.png
imageWide: true
permalink: "/blog/20240516-simplex-redefining-privacy-hard-choices.html"
---
# SimpleX: Redefining Privacy by Making Hard Choices
**Published:** May 16, 2024
When it comes to open source privacy tools, the status quo often dictates the limitations of existing protocols and structures. However, these norms need to be challenged to radically shift how we approach genuinely private communication. This requires doing some uncomfortable things, like making hard choices as it relates to funding, alternative decentralization models, doubling down on privacy over convenience, and more.
There will always be questions on why the SimpleX Chat and network makes the choices it makes, and thats good! Its important to question us and to understand the reasoning behind each decision, whether its technical, structural, financial or any other.
In this post we explain a bit more about why SimpleX operates and makes decisions the way it does.
## No user accounts
Within SimpleX network there are no user accounts, and more importantly, no user profile identifiers whatsoever at the protocol level, not even random numbers or cryptographic keys used to identify the users. This means there is absolutely nothing that uniquely links users to their contacts or to the network relays. While it's accurate to say, "You need an address to send something," it's crucial to understand that this "address" serves merely as a transient delivery destination, and not as a user profile identifier in any sense.
You can read more about how SimpleX works [here](https://simplex.chat/#how-simplex-works).
## Privacy over convenience
One of the main considerations often ignored in security and privacy comparisons between messaging applications is multi-device access. For example, in Signals case, the Sesame protocol used to support multi-device access has the vulnerability that is [explained in detail here](https://eprint.iacr.org/2021/626.pdf):
_"We present an attack on the post-compromise security of the Signal messenger that allows to stealthily register a new device via the Sesame protocol. [...] This new device can send and receive messages without raising any Bad encrypted message errors. Our attack thus shows that the Signal messenger does not guarantee post-compromise security at all in the multi-device setting"_.
<img src="./images/20231125-mobile2.png" width="170" class="float-right">
Solutions are possible, and even the quoted paper proposes improvements, but they are not implemented in any existing communication solutions. Unfortunately this results in most communication systems, even those in the privacy space, having compromised security in multi-device settings due to these limitations. That's the reason we are not rushing a full multi-device support, and currently only provide [the ability to use mobile app profiles via the desktop app](./20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md#link-mobile-and-desktop-apps-via-secure-quantum-resistant-protocol), while they are on the same network.
Another choice that compromises privacy for convenience and usability is 3rd party push notifications. At SimpleX, we take a slow path of optimizing the network and battery consumption in the app, rather than simply hiding inefficiencies behind the quick fix solution of 3rd party push notifications that [increases vulnerability](https://www.wired.com/story/apple-google-push-notification-surveillance/), a path Signal and others chose. Like other choices, it has usability and optimization trade offs, but ultimately its the right thing to continue progressing towards a better solution as we explain [here](https://simplex.chat/blog/20220404-simplex-chat-instant-notifications.html).
Whenever possible, we strive to achieve significantly higher levels of privacy and security. For example, unlike most, if not all, applications (including Signal), [we encrypt application files with per-file unique key](https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html#encrypted-local-files-and-media-with-forward-secrecy). Consequently, once a message is deleted, there's no means to open a file that someone may have stolen in hopes of acquiring the key later. Similarly, apps like Session have done away with forward secrecy, a decision which caused them [not to be recommended](https://www.privacyguides.org/en/real-time-communication/#additional-options) for "long-term or sensitive communications". And [misinformation](https://simplifiedprivacy.com/spain-has-banned-telegram-defending-session/) around this makes it dangerous and irresponsible to recommend without such necessary disclosures for peoples awareness.
Sessions decision was based on [the incorrect statements](https://getsession.org/blog/session-protocol-explained) about double ratchet being impossible in decentralized networks, and underplayed importance of forward secrecy, break-in recovery and deniability - the absence of these crucial qualities makes Session a much weaker choice for private messaging. For transparency, this was something that was debated with their team [here](https://twitter.com/SimpleXChat/status/1755216356159414602). We also made [a separate post](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md#end-to-end-encryption-security-attacks-and-defense) about these qualities of end-to-end encryption and their presence in different messengers, to show that not all end-to-end encrypted apps offer the same level of protections.
## Network decentralization
<img src="./images/simplex-explained.svg" width="40%" class="float-right">
It's important to recognize that a model of decentralization where all servers are openly known and accessible to all clients, that some users ask for, actually results in a less decentralized network, and as the network grows it often requires an introduction of a central authority to protect from bad actors with malicious intent. Therefore, we've deliberately opted for a slower path towards achieving a higher degree of decentralization where there is no central server registry or network authority. For example, p2p designs may offer higher initial decentralization but often compromise on privacy and eventual decentralization. In essence, our approach prioritizes a balance between initial decentralization, privacy, and higher degree of decentralization down the line.
Additionally, while it's true that we haven't yet established a model to incentivize other network operators, it's certainly on the roadmap. We see the decentralization of network operators offered within the app as a top priority. 
Where it stands today, users have the freedom to select their preferred servers within the SimpleX network by configuring the app, with thousands of self-hosted servers in operation. Moreover, numerous third-party applications rely on our code for their in-app communications, operating independently of our servers, many of which we may not even be aware of.
Decentralization is an ongoing journey, and we strive to proceed at a measured pace to ensure its proper implementation. While the immediate results may not always appear ideal, prioritizing a careful approach ensures that in the long run, the decisions made in this area align with our ultimate objectives of a private, efficient, reliable and fully decentralized network.
## Funding and profitability
We explain our rationale for funding [here](../docs/FAQ.md#funding-and-business-model). Funding sources is always one of the most difficult choices to make, and its important to underline that VC models dont necessarily translate to a quest for control, interference of any kind, or overall influence on product roadmap and strategy. The vast majority of investors seek profitability. Irrespective of the organization type profitability is essential for a sustainable operation, and it can and should be done while adhering to the highest possible standards for privacy. For-profit vs. nonprofit is also not an accurate metric to measure a commitment towards privacy and open standards, which is further explained [here](./20240404-why-i-joined-simplex-chat-esraa-al-shafei.md).  
To make a profit, satisfying customers is the key. Unlike the many companies that profit from selling customer data, we put user privacy first. Doing this at scale requires investments. If the investors dont own or control a company, their participation becomes merely about profit for them, and not about how this profit is obtained. With the investors we have, we are completely aligned on this - they are betting on the future where privacy is the norm. They do not dictate on anything related to our model. We build SimpleX chat, protocols and network the way Internet should have been built if we as developers always put the privacy and empowerment of users first.
## Company jurisdiction
<img src="./images/20240516-parliament.jpg" width="40%" class="float-right">
With regards to jurisdictions, nowhere is perfect. For that reason we plan to establish the foundations for protocol governance in [various jurisdictions](https://simplex.chat/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html#the-journey-to-the-decentralized-non-profit-protocol-governance).
But wed like to clarify some misconceptions about the UK, where SimpleX Chat Ltd. is registered, and the UK legislation.
For example, the Online Safety Act (OSA). Some people believe that it applies only to UK companies. But the OSA applicability isnt determined by the companys jurisdiction - it applies based on the nature and characteristics of the business and its services, as well as the number of its users in the UK. In case of SimpleX network, the OSA doesnt apply for both of these reasons.
The UKs position on communication encryption, and more specifically, on end-to-end encrypted messaging, remains the subject of political debates. But with the OSA, the legislative intent was to propose technical measures to block CSAM, and it was trying to explore ways to do this via client-side scanning, which of course would undermine the encryption. However, and thanks to the hard work of privacy experts, researchers, academics and rights organizations throughout the UK and the rest of the world, the Online Safety Bill did not prohibit end-to-end encrypted apps without such scanners. It is an open question whether such technology will ever be possible, and the UK government made a public commitment that client-side scanning won't be required until it is.
For now, strong end-to-end encryption remains permissible and protected, and we hope to also add to the privacy advocacy and debates as a UK-based company to keep it legally protected.
Overall, we view the UK as being better jurisdiction for privacy than many alternatives - there are some trade-offs everywhere.
## Looking ahead 
The future of the Internet should be based on decentralized infrastructure operated by commercially viable organizations. These operators need to possess minimal user data, so that users have genuine control over their identities, and free from lock-in by the operators, to support fair competition. This requires a drastic re-imagining of the current norms and newer, more privacy-minded protocols. All in all, private messaging is surrounded by very difficult challenges but its worth it to keep pushing the industry forward and not settle for the status quo and current trade offs, protocol limitations and vulnerabilities. The Internet deserves better standards, and so do users.
+14
View File
@@ -1,5 +1,19 @@
# Blog # Blog
May 16, 2024 [SimpleX: Redefining Privacy by Making Hard Choices](./20240516-simplex-redefining-privacy-hard-choices.md)
When it comes to open source privacy tools, the status quo often dictates the limitations of existing protocols and structures. However, these norms need to be challenged to radically shift how we approach genuinely private communication. This requires doing some uncomfortable things, like making hard choices as it relates to funding, alternative decentralization models, doubling down on privacy over convenience, and more.
In this post we explain a bit more about why SimpleX operates and makes decisions the way it does:
- No user accounts.
- Privacy over convenience.
- Network decentralization.
- Funding and profitability.
- Company jurisdiction.
---
Apr 26, 2024 [SimpleX network: legally binding transparency, v5.7 released with better calls and messages](./20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md) Apr 26, 2024 [SimpleX network: legally binding transparency, v5.7 released with better calls and messages](./20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md)
We published Transparency Reports, Security Policy, and Frequently Asked Questions, and updated Privacy Policy. We published Transparency Reports, Security Policy, and Frequently Asked Questions, and updated Privacy Policy.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+82
View File
@@ -0,0 +1,82 @@
<svg width="539" height="399" viewBox="0 0 539 399" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="268.083" cy="80.9476" r="32.1985" fill="url(#paint0_linear_651_6627)"/>
<circle cx="268.082" cy="80.9513" r="39.3169" stroke="url(#paint1_linear_651_6627)" stroke-width="0.956242"/>
<circle cx="267.428" cy="73.3473" r="5.65297" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M256.025 90.0869H278.595C277.039 85.3429 272.575 81.917 267.31 81.917C262.046 81.917 257.582 85.3429 256.025 90.0869Z" fill="white"/>
<circle cx="40.5779" cy="218.884" r="32.1985" fill="url(#paint2_linear_651_6627)"/>
<circle cx="40.5777" cy="218.888" r="39.3169" stroke="url(#paint3_linear_651_6627)" stroke-width="0.956242"/>
<circle cx="39.9227" cy="211.284" r="5.65297" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.0904 228.023H28.5205C30.0767 223.279 34.541 219.854 39.8054 219.854C45.0699 219.854 49.5341 223.279 51.0904 228.023Z" fill="white"/>
<circle cx="268.083" cy="358.611" r="32.1985" fill="url(#paint4_linear_651_6627)"/>
<circle cx="268.082" cy="358.614" r="39.3169" stroke="url(#paint5_linear_651_6627)" stroke-width="0.956242"/>
<circle cx="267.428" cy="351.01" r="5.65297" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M256.025 367.75H278.595C277.039 363.006 272.575 359.58 267.31 359.58C262.046 359.58 257.582 363.006 256.025 367.75Z" fill="white"/>
<circle cx="499.17" cy="217.092" r="32.1985" fill="url(#paint6_linear_651_6627)"/>
<circle cx="499.17" cy="217.096" r="39.3169" stroke="url(#paint7_linear_651_6627)" stroke-width="0.956242"/>
<circle cx="498.515" cy="209.492" r="5.65297" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M509.683 226.231H487.113C488.669 221.487 493.133 218.062 498.398 218.062C503.662 218.062 508.126 221.487 509.683 226.231Z" fill="white"/>
<path d="M79.8735 192.248L224.526 103.265" stroke="#0197FF" stroke-width="2.99282" stroke-linecap="round" stroke-dasharray="5.99 8.98"/>
<path d="M455.16 192.248L310.507 103.265" stroke="#CD5CAE" stroke-width="2.99282" stroke-linecap="round" stroke-dasharray="5.99 8.98"/>
<path d="M222.747 338.695L78.0943 249.712" stroke="#9B90FB" stroke-width="2.99282" stroke-linecap="round" stroke-dasharray="5.99 8.98"/>
<path d="M268.016 301.529L268.016 137.652" stroke="#1BC3BF" stroke-width="2.99282" stroke-linecap="round" stroke-dasharray="5.99 8.98"/>
<rect x="87.3558" y="63.0347" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/>
<path d="M108.408 76.2031H111.281V92.9629H107.977V79.6987L104.817 80.5846L104.002 77.7594L108.408 76.2031Z" fill="#0197FF"/>
<rect x="167.228" y="198.131" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/>
<path d="M190.169 218.101C191.303 218.437 192.22 219.027 192.923 219.873C193.641 220.703 194 221.709 194 222.89C194 224.614 193.417 225.963 192.252 226.936C191.103 227.91 189.69 228.397 188.014 228.397C186.706 228.397 185.532 228.101 184.495 227.511C183.473 226.904 182.731 226.018 182.268 224.853L185.093 223.225C185.508 224.518 186.482 225.164 188.014 225.164C188.86 225.164 189.515 224.965 189.978 224.566C190.457 224.151 190.696 223.592 190.696 222.89C190.696 222.204 190.457 221.653 189.978 221.238C189.515 220.823 188.86 220.615 188.014 220.615H187.296L186.027 218.7L189.331 214.39H182.771V211.302H193.306V214.031L190.169 218.101Z" fill="#0197FF"/>
<rect x="86.9073" y="326.685" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/>
<path d="M108.065 345.96C109.677 345.96 111.034 346.447 112.136 347.42C113.237 348.378 113.788 349.719 113.788 351.443C113.788 353.167 113.205 354.515 112.04 355.489C110.891 356.463 109.478 356.949 107.802 356.949C106.445 356.949 105.248 356.646 104.211 356.04C103.173 355.433 102.423 354.539 101.96 353.358L104.785 351.706C105.2 353.047 106.206 353.717 107.802 353.717C108.648 353.717 109.302 353.518 109.765 353.119C110.244 352.704 110.484 352.145 110.484 351.443C110.484 350.756 110.252 350.206 109.789 349.791C109.326 349.376 108.696 349.168 107.898 349.168H102.822L103.492 339.854H112.997V342.943H106.557L106.341 345.96H108.065Z" fill="#0197FF"/>
<path d="M72.9777 331.097C72.8728 331.917 72.1232 332.497 71.3035 332.392L57.9447 330.682C57.125 330.577 56.5455 329.828 56.6504 329.008C56.7553 328.188 57.5049 327.609 58.3246 327.714L70.199 329.233L71.7186 317.359C71.8235 316.539 72.5731 315.959 73.3928 316.064C74.2126 316.169 74.7921 316.919 74.6872 317.739L72.9777 331.097ZM38.6697 271.837C44.3979 288.6 58.4045 311.53 72.6773 329.992L70.3095 331.823C55.9494 313.248 41.7191 290.016 35.8377 272.805L38.6697 271.837Z" fill="#9B90FB"/>
<path d="M215.639 371.664C216.25 372.22 216.295 373.167 215.738 373.778L206.672 383.737C206.115 384.348 205.169 384.392 204.558 383.836C203.947 383.279 203.902 382.333 204.459 381.722L212.518 372.87L203.666 364.811C203.054 364.254 203.01 363.308 203.566 362.697C204.123 362.085 205.069 362.041 205.68 362.597L215.639 371.664ZM147.42 365.75C164.435 370.679 191.252 372.369 214.562 371.276L214.702 374.265C191.249 375.365 164.057 373.686 146.587 368.625L147.42 365.75Z" fill="#9B90FB"/>
<path d="M186.13 264.557C186.388 263.772 187.233 263.344 188.018 263.601L200.815 267.8C201.6 268.057 202.028 268.903 201.77 269.688C201.513 270.473 200.667 270.901 199.882 270.644L188.507 266.912L184.775 278.286C184.518 279.072 183.672 279.499 182.887 279.242C182.102 278.984 181.674 278.139 181.932 277.353L186.13 264.557ZM228.079 319.816C211.627 305.232 196.047 285.13 186.217 265.699L188.887 264.348C198.544 283.436 213.89 303.238 230.065 317.576L228.079 319.816Z" fill="#9B90FB"/>
<path d="M101.379 249.32C100.828 248.704 100.881 247.758 101.497 247.208L111.539 238.233C112.155 237.682 113.101 237.736 113.652 238.352C114.203 238.968 114.149 239.914 113.533 240.465L104.607 248.442L112.584 257.368C113.135 257.984 113.082 258.93 112.466 259.481C111.85 260.032 110.904 259.978 110.353 259.362L101.379 249.32ZM162.973 242.498C143.252 249.825 124.069 251.033 102.41 249.817L102.578 246.829C124.02 248.033 142.745 246.821 161.931 239.692L162.973 242.498Z" fill="#9B90FB"/>
<rect x="325.884" y="198.131" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/>
<path d="M353.504 222.148V225.212H351.708V228.062H348.38V225.212H340.455V222.148L346.034 211.302H349.577L344.022 222.148H348.38V218.173H351.708V222.148H353.504Z" fill="#0197FF"/>
<path d="M317.746 191.65C318.511 191.964 319.385 191.599 319.699 190.835L324.815 178.376C325.129 177.612 324.764 176.738 323.999 176.424C323.235 176.11 322.36 176.475 322.046 177.24L317.499 188.313L306.425 183.766C305.661 183.452 304.786 183.817 304.472 184.582C304.158 185.346 304.524 186.221 305.288 186.535L317.746 191.65ZM283.185 132.538C294.926 145.802 307.938 169.311 316.934 190.843L319.696 189.689C310.645 168.026 297.481 144.173 285.425 130.554L283.185 132.538Z" fill="#1BC3BF"/>
<path d="M302.485 317.64C302.635 318.453 303.416 318.99 304.228 318.839L317.472 316.393C318.284 316.243 318.822 315.462 318.671 314.649C318.521 313.837 317.741 313.3 316.928 313.45L305.156 315.625L302.981 303.853C302.831 303.04 302.05 302.503 301.238 302.653C300.425 302.803 299.888 303.584 300.038 304.396L302.485 317.64ZM333.458 256.571C328.667 273.625 315.952 297.296 302.724 316.52L305.189 318.216C318.498 298.875 331.42 274.89 336.339 257.38L333.458 256.571Z" fill="#1BC3BF"/>
<path d="M214.41 250.543C213.646 250.228 212.771 250.592 212.456 251.356L207.324 263.807C207.009 264.571 207.373 265.446 208.137 265.761C208.902 266.076 209.776 265.712 210.091 264.948L214.653 253.88L225.721 258.442C226.485 258.757 227.36 258.393 227.675 257.629C227.99 256.864 227.626 255.99 226.862 255.675L214.41 250.543ZM248.894 309.7C237.17 296.421 224.189 272.895 215.221 251.351L212.458 252.501C221.481 274.177 234.613 298.046 246.651 311.681L248.894 309.7Z" fill="#1BC3BF"/>
<path d="M252.992 138.384C252.581 137.667 251.666 137.42 250.949 137.832L239.272 144.542C238.556 144.954 238.309 145.868 238.72 146.585C239.132 147.302 240.047 147.549 240.763 147.137L251.143 141.172L257.107 151.552C257.519 152.269 258.434 152.516 259.15 152.104C259.867 151.692 260.114 150.778 259.702 150.061L252.992 138.384ZM223.657 201.324C235.535 186.647 247.194 161.53 253.14 139.52L250.25 138.739C244.379 160.474 232.874 185.176 221.331 199.442L223.657 201.324Z" fill="#1BC3BF"/>
<rect x="398.022" y="63.0347" width="42.9204" height="42.9204" rx="2.99282" stroke="#0197FF" stroke-width="1.99521"/>
<path d="M413.321 92.9629V90.4968L419.044 84.607C420.321 83.2662 420.959 82.1489 420.959 81.255C420.959 80.6006 420.752 80.0738 420.337 79.6748C419.938 79.2757 419.419 79.0762 418.78 79.0762C417.519 79.0762 416.586 79.7307 415.979 81.0395L413.202 79.4114C413.728 78.2622 414.487 77.3843 415.476 76.7777C416.466 76.1712 417.551 75.8679 418.732 75.8679C420.249 75.8679 421.55 76.3468 422.635 77.3045C423.721 78.2462 424.263 79.5232 424.263 81.1353C424.263 82.8751 423.345 84.6628 421.51 86.4984L418.23 89.7785H424.527V92.9629H413.321Z" fill="#0197FF"/>
<path d="M77.1479 111.221C77.253 110.401 76.6737 109.651 75.854 109.546L62.4958 107.833C61.676 107.727 60.9263 108.307 60.8211 109.126C60.716 109.946 61.2953 110.696 62.115 110.801L73.989 112.324L72.4659 124.198C72.3607 125.018 72.94 125.768 73.7598 125.873C74.5795 125.978 75.3292 125.399 75.4344 124.579L77.1479 111.221ZM42.3631 168.218C46.7014 146.722 60.4086 124.708 76.5785 112.214L74.7487 109.846C57.9918 122.793 43.9057 145.446 39.4294 167.626L42.3631 168.218Z" fill="#0197FF"/>
<path d="M215.45 70.1934C216.205 69.8575 216.545 68.973 216.209 68.2179L210.734 55.9134C210.398 55.1584 209.513 54.8186 208.758 55.1546C208.003 55.4906 207.663 56.3751 207.999 57.1301L212.866 68.0675L201.929 72.9343C201.174 73.2703 200.834 74.1548 201.17 74.9098C201.506 75.6649 202.391 76.0046 203.146 75.6686L215.45 70.1934ZM148.677 70.0794C169.298 62.6197 195.23 62.8963 214.305 70.2232L215.378 67.4294C195.611 59.8365 168.936 59.5682 147.659 67.2651L148.677 70.0794Z" fill="#0197FF"/>
<path d="M203.562 181.745C203.705 182.559 204.482 183.102 205.295 182.959L218.558 180.62C219.372 180.476 219.916 179.7 219.772 178.886C219.629 178.072 218.852 177.529 218.039 177.672L206.249 179.752L204.17 167.962C204.027 167.148 203.25 166.605 202.436 166.749C201.623 166.892 201.079 167.668 201.223 168.482L203.562 181.745ZM235.03 120.93C230.101 137.944 217.194 161.511 203.81 180.627L206.261 182.343C219.727 163.111 232.843 139.232 237.905 121.762L235.03 120.93Z" fill="#0197FF"/>
<path d="M86.7735 223.138C86.1511 223.682 86.0872 224.627 86.6309 225.249L95.4903 235.393C96.034 236.015 96.9793 236.079 97.6018 235.535C98.2242 234.992 98.2881 234.046 97.7444 233.424L89.8693 224.408L98.8857 216.533C99.5081 215.989 99.572 215.044 99.0283 214.421C98.4846 213.799 97.5393 213.735 96.9169 214.279L86.7735 223.138ZM155.1 218.631C137.987 223.208 111.141 224.345 87.8588 222.772L87.657 225.758C111.082 227.341 138.303 226.222 155.873 221.522L155.1 218.631Z" fill="#0197FF"/>
<path d="M383.345 66.4144C384.119 66.1236 384.51 65.2608 384.219 64.4872L379.481 51.8806C379.19 51.107 378.327 50.7156 377.554 51.0064C376.78 51.2971 376.389 52.16 376.679 52.9336L380.891 64.1394L369.686 68.3513C368.912 68.6421 368.521 69.505 368.811 70.2786C369.102 71.0522 369.965 71.4436 370.739 71.1528L383.345 66.4144ZM316.695 62.3541C337.722 56.1262 363.592 57.9349 382.2 66.3764L383.437 63.6509C364.152 54.903 337.54 53.0586 315.845 59.4845L316.695 62.3541Z" fill="#CD5CAE"/>
<path d="M484.334 169.427C484.984 169.937 485.925 169.823 486.435 169.173L494.746 158.575C495.256 157.925 495.142 156.984 494.491 156.474C493.841 155.964 492.901 156.078 492.391 156.728L485.003 166.149L475.583 158.761C474.933 158.251 473.992 158.365 473.482 159.016C472.972 159.666 473.086 160.607 473.736 161.117L484.334 169.427ZM453.18 110.367C469.425 125.098 481.318 148.143 483.772 168.429L486.743 168.07C484.2 147.047 471.952 123.35 455.191 108.15L453.18 110.367Z" fill="#CD5CAE"/>
<path d="M381.01 219.006C380.358 219.514 380.241 220.455 380.749 221.107L389.024 231.732C389.532 232.384 390.472 232.501 391.124 231.993C391.776 231.485 391.893 230.545 391.386 229.893L384.03 220.448L393.475 213.093C394.127 212.585 394.244 211.645 393.736 210.993C393.228 210.341 392.288 210.224 391.636 210.731L381.01 219.006ZM449.482 218.349C432.139 221.957 405.272 221.582 382.115 218.702L381.745 221.672C405.044 224.57 432.285 224.983 450.091 221.279L449.482 218.349Z" fill="#CD5CAE"/>
<path d="M305.2 125.099C304.379 125.194 303.791 125.937 303.886 126.758L305.439 140.136C305.534 140.957 306.277 141.545 307.098 141.45C307.919 141.355 308.507 140.612 308.412 139.791L307.032 127.9L318.923 126.52C319.744 126.424 320.332 125.682 320.237 124.861C320.142 124.04 319.399 123.451 318.578 123.547L305.2 125.099ZM342.253 184.234C340.61 174.231 335.593 162.688 329.05 152.113C322.503 141.531 314.345 131.783 306.302 125.413L304.444 127.759C312.143 133.857 320.086 143.312 326.505 153.687C332.928 164.069 337.742 175.237 339.299 184.719L342.253 184.234Z" fill="#CD5CAE"/>
<defs>
<linearGradient id="paint0_linear_651_6627" x1="299.334" y1="48.749" x2="222.184" y2="75.0796" gradientUnits="userSpaceOnUse">
<stop stop-color="#53C1FF"/>
<stop offset="1" stop-color="#0053D0"/>
</linearGradient>
<linearGradient id="paint1_linear_651_6627" x1="306.707" y1="41.1562" x2="211.356" y2="73.6989" gradientUnits="userSpaceOnUse">
<stop stop-color="#53C1FF"/>
<stop offset="1" stop-color="#0053D0"/>
</linearGradient>
<linearGradient id="paint2_linear_651_6627" x1="71.8294" y1="186.686" x2="-5.32023" y2="213.016" gradientUnits="userSpaceOnUse">
<stop stop-color="#53C1FF"/>
<stop offset="1" stop-color="#0053D0"/>
</linearGradient>
<linearGradient id="paint3_linear_651_6627" x1="79.2023" y1="179.093" x2="-16.149" y2="211.635" gradientUnits="userSpaceOnUse">
<stop stop-color="#53C1FF"/>
<stop offset="1" stop-color="#0053D0"/>
</linearGradient>
<linearGradient id="paint4_linear_651_6627" x1="299.334" y1="326.412" x2="222.184" y2="352.743" gradientUnits="userSpaceOnUse">
<stop stop-color="#53C1FF"/>
<stop offset="1" stop-color="#0053D0"/>
</linearGradient>
<linearGradient id="paint5_linear_651_6627" x1="306.707" y1="318.819" x2="211.356" y2="351.362" gradientUnits="userSpaceOnUse">
<stop stop-color="#53C1FF"/>
<stop offset="1" stop-color="#0053D0"/>
</linearGradient>
<linearGradient id="paint6_linear_651_6627" x1="530.421" y1="184.894" x2="453.272" y2="211.224" gradientUnits="userSpaceOnUse">
<stop stop-color="#53C1FF"/>
<stop offset="1" stop-color="#0053D0"/>
</linearGradient>
<linearGradient id="paint7_linear_651_6627" x1="537.794" y1="177.301" x2="442.443" y2="209.843" gradientUnits="userSpaceOnUse">
<stop stop-color="#53C1FF"/>
<stop offset="1" stop-color="#0053D0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

+20 -20
View File
@@ -1,6 +1,6 @@
# Blog # Blog
4 févr. 2023 [SimpleX Chat v4.5 publié](./20230103-simplex-chat-v4.4-disappearing-messages.md) 4 févr. 2023 [SimpleX Chat v4.5 publié](../../20230103-simplex-chat-v4.4-disappearing-messages.md)
- profils de chat multiples. - profils de chat multiples.
- brouillon de message. - brouillon de message.
@@ -10,7 +10,7 @@
Nous avons également ajouté [l'interface en italien](#french-language-interface), grâce à nos utilisateurs et à Weblate ! Nous avons également ajouté [l'interface en italien](#french-language-interface), grâce à nos utilisateurs et à Weblate !
3 janv. 2023 [SimpleX Chat v4.4 publié](./20230103-simplex-chat-v4.4-disappearing-messages.md) 3 janv. 2023 [SimpleX Chat v4.4 publié](../../20230103-simplex-chat-v4.4-disappearing-messages.md)
- messages éphèméres. - messages éphèméres.
- messages "en direct" (dynamique). - messages "en direct" (dynamique).
@@ -19,7 +19,7 @@ Nous avons également ajouté [l'interface en italien](#french-language-interfac
Nous avons également ajouté [l'interface en français](#french-language-interface), grâce à nos utilisateurs et à Weblate ! Nous avons également ajouté [l'interface en français](#french-language-interface), grâce à nos utilisateurs et à Weblate !
6 déc. 2022 [SimpleX Chat : révision et sortie de la v4.3](./20221206-simplex-chat-v4.3-voice-messages.md) 6 déc. 2022 [SimpleX Chat : révision et sortie de la v4.3](../../20221206-simplex-chat-v4.3-voice-messages.md)
Critiques de novembre : Critiques de novembre :
@@ -35,7 +35,7 @@ Sortie de la v4.3 :
- amélioration de la configuration du serveur SMP et du support des mots de passe du serveur - amélioration de la configuration du serveur SMP et du support des mots de passe du serveur
- améliorations de la confidentialité et de la sécurité : protection de l'écran de l'application, sécurité des liens SimpleX, etc. - améliorations de la confidentialité et de la sécurité : protection de l'écran de l'application, sécurité des liens SimpleX, etc.
8 nov. 2022 [Audit de sécurité par Trail of Bits, nouveau site web et sortie de la v4.2](./20221108-simplex-chat-v4.2-security-audit-new-website.md) 8 nov. 2022 [Audit de sécurité par Trail of Bits, nouveau site web et sortie de la v4.2](../../20221108-simplex-chat-v4.2-security-audit-new-website.md)
_"Avez-vous été audité ou devons-nous simplement vous ignorer ?"_ _"Avez-vous été audité ou devons-nous simplement vous ignorer ?"_
@@ -51,7 +51,7 @@ Sortie de la v4.2 :
- changer manuellement de contact ou de membre vers une autre adresse / serveur (BETA) - changer manuellement de contact ou de membre vers une autre adresse / serveur (BETA)
- recevoir des fichiers plus rapidement (BETA) - recevoir des fichiers plus rapidement (BETA)
28 sept. 2022 [v4 : chiffrement de la base de données locale](./20220928-simplex-chat-v4-encrypted-database.md) 28 sept. 2022 [v4 : chiffrement de la base de données locale](../../20220928-simplex-chat-v4-encrypted-database.md)
- base de données locale de chat chiffrée - si vous utilisez déjà l'application, vous pouvez chiffrer la base de données dans les paramètres de l'application. - base de données locale de chat chiffrée - si vous utilisez déjà l'application, vous pouvez chiffrer la base de données dans les paramètres de l'application.
- support pour les serveurs WebRTC ICE auto-hébergés - support pour les serveurs WebRTC ICE auto-hébergés
@@ -61,7 +61,7 @@ Sortie de la v4.2 :
- support des images animées dans l'application Android - support des images animées dans l'application Android
- Interface utilisateur en allemand pour les applications mobiles - Interface utilisateur en allemand pour les applications mobiles
1 sept. 2022 [v3.2 : Mode Incognito](./20220901-simplex-chat-v3.2-incognito-mode.md) 1 sept. 2022 [v3.2 : Mode Incognito](../../20220901-simplex-chat-v3.2-incognito-mode.md)
- Mode Incognito - utiliser un nouveau nom de profil aléatoire pour chaque contact - Mode Incognito - utiliser un nouveau nom de profil aléatoire pour chaque contact
- utiliser des adresses de serveur .onion avec Tor - utiliser des adresses de serveur .onion avec Tor
@@ -71,7 +71,7 @@ Sortie de la v4.2 :
L'audit d'implémentation est prévu pour Octobre ! L'audit d'implémentation est prévu pour Octobre !
8 août 2022 [v3.1 : groupes de discussion](./20220808-simplex-chat-v3.1-chat-groups.md) 8 août 2022 [v3.1 : groupes de discussion](../../20220808-simplex-chat-v3.1-chat-groups.md)
- enfin, des groupes de chat secrets - personne d'autre que les membres ne sait qu'ils existent ! - enfin, des groupes de chat secrets - personne d'autre que les membres ne sait qu'ils existent !
- accès aux serveurs de messagerie via Tor sur toutes les plateformes - accès aux serveurs de messagerie via Tor sur toutes les plateformes
@@ -79,37 +79,37 @@ L'audit d'implémentation est prévu pour Octobre !
- protocole de chat publié - protocole de chat publié
- nouvelles icônes d'application - nouvelles icônes d'application
23 juil. 2022 [v3.1-beta : accès aux serveurs via Tor](./20220723-simplex-chat-v3.1-tor-groups-efficiency.md) 23 juil. 2022 [v3.1-beta : accès aux serveurs via Tor](../../20220723-simplex-chat-v3.1-tor-groups-efficiency.md)
- application terminale : accès aux serveurs de messagerie via un proxy SOCKS5 (par exemple, Tor). - application terminale : accès aux serveurs de messagerie via un proxy SOCKS5 (par exemple, Tor).
- applications mobiles : rejoindre et quitter des groupes de discussion. - applications mobiles : rejoindre et quitter des groupes de discussion.
- utilisation optimisée de la batterie et du trafic - réduction jusqu'à 90x ! - utilisation optimisée de la batterie et du trafic - réduction jusqu'à 90x !
- deux configurations docker pour les serveurs SMP auto-hébergés. - deux configurations docker pour les serveurs SMP auto-hébergés.
11 juil. 2022 [v3 : notifications push instantanées pour iOS et appels audio/vidéo](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md) : 11 juil. 2022 [v3 : notifications push instantanées pour iOS et appels audio/vidéo](../../20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md) :
- exportation et importation de la base de données de chat - exportation et importation de la base de données de chat
- appels audio/vidéo chiffrés de bout en bout - appels audio/vidéo chiffrés de bout en bout
- amélioration de la confidentialité du protocole et des performances - amélioration de la confidentialité du protocole et des performances
4 juin 2022 [v2.2 : nouveaux paramètres de confidentialité et de sécurité](./20220604-simplex-chat-new-privacy-security-settings.md) 4 juin 2022 [v2.2 : nouveaux paramètres de confidentialité et de sécurité](../../20220604-simplex-chat-new-privacy-security-settings.md)
24 mai 2022 [v2.1 : effacement des messages pour une meilleure confidentialité des conversations](./20220524-simplex-chat-better-privacy.md) 24 mai 2022 [v2.1 : effacement des messages pour une meilleure confidentialité des conversations](../../20220524-simplex-chat-better-privacy.md)
11 mai 2022 [Publication de la v2.0 - envoi d'images et de fichiers dans les applications mobiles](./20220511-simplex-chat-v2-images-files.md) 11 mai 2022 [Publication de la v2.0 - envoi d'images et de fichiers dans les applications mobiles](../../20220511-simplex-chat-v2-images-files.md)
04 avr. 2022 [Notifications instantanées pour les applications mobiles SimpleX Chat](./20220404-simplex-chat-instant-notifications.md) 04 avr. 2022 [Notifications instantanées pour les applications mobiles SimpleX Chat](../../20220404-simplex-chat-instant-notifications.md)
08 mars 2022 [Applications mobiles pour iOS et Android](./20220308-simplex-chat-mobile-apps.md) 08 mars 2022 [Applications mobiles pour iOS et Android](../../20220308-simplex-chat-mobile-apps.md)
14 févr. 2022. [SimpleX Chat : rejoignez notre version bêta publique pour iOS](./20220214-simplex-chat-ios-public-beta.md) 14 févr. 2022. [SimpleX Chat : rejoignez notre version bêta publique pour iOS](../../20220214-simplex-chat-ios-public-beta.md)
12 janv. 2022. [SimpleX Chat v1 : la plateforme de chat et d'application la plus privée et la plus sécurisée](./20220112-simplex-chat-v1-released.md) 12 janv. 2022. [SimpleX Chat v1 : la plateforme de chat et d'application la plus privée et la plus sécurisée](../../20220112-simplex-chat-v1-released.md)
08 déc. 2021. [Sortie de SimpleX Chat v0.5 : la première plateforme de chat 100% privée par définition - aucun accès à votre graphe de connexions](./20211208-simplex-chat-v0.5-released.md) 08 déc. 2021. [Sortie de SimpleX Chat v0.5 : la première plateforme de chat 100% privée par définition - aucun accès à votre graphe de connexions](../../20211208-simplex-chat-v0.5-released.md)
14 septembre 2021. [SimpleX Chat v0.4 publié : chat open-source qui utilise un protocole de routage de messages préservant la confidentialité](./20210914-simplex-chat-v0.4-released.md) 14 septembre 2021. [SimpleX Chat v0.4 publié : chat open-source qui utilise un protocole de routage de messages préservant la confidentialité](../../20210914-simplex-chat-v0.4-released.md)
12 mai 2021. [Prototype de chat SimpleX](./20210512-simplex-chat-terminal-ui.md) 12 mai 2021. [Prototype de chat SimpleX](../../20210512-simplex-chat-terminal-ui.md)
22 oct. 2020. [SimpleX Chat](./20201022-simplex-chat.md) 22 oct. 2020. [SimpleX Chat](../../20201022-simplex-chat.md)
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: dc111437fda043b4921cf31f6e402a9644ad3324 tag: e3f5d244c1a435593e33adc023bf1f920f379f8d
source-repository-package source-repository-package
type: git type: git
+1 -1
View File
@@ -98,7 +98,7 @@ git checkout stable
DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
``` ```
> **Please note:** If you encounter `` version `GLIBC_2.28' not found `` error, rebuild it with `haskell:8.10.7-stretch` base image (change it in your local [Dockerfile](Dockerfile)). > **Please note:** If you encounter `` version `GLIBC_2.28' not found `` error, rebuild it with `haskell:8.10.7-stretch` base image (change it in your local [Dockerfile](/Dockerfile)).
#### In any OS #### In any OS
+1 -1
View File
@@ -8,7 +8,7 @@ revision: 23.04.2024
While great care is taken to ensure the highest level of security and privacy in SimpleX network servers and clients, all software can have flaws, and we believe it is a critical part of an organization's social responsibility to minimize the impact of these flaws through continual vulnerability discovery efforts, defense in depth design, and prompt remediation and notification. While great care is taken to ensure the highest level of security and privacy in SimpleX network servers and clients, all software can have flaws, and we believe it is a critical part of an organization's social responsibility to minimize the impact of these flaws through continual vulnerability discovery efforts, defense in depth design, and prompt remediation and notification.
The security assessment of SimpleX cryptography and networking was done by Trail of Bits in [November 2022](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html). The security assessment of SimpleX cryptography and networking was done by Trail of Bits in [November 2022](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
We are planning design review of SimpleX protocols in July 2024 and implementation review in December 2024/January 2025. We are planning design review of SimpleX protocols in July 2024 and implementation review in December 2024/January 2025.
+1 -1
View File
@@ -97,7 +97,7 @@ git checkout stable
DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
``` ```
> **Upozornění:** Pokud narazíte na chybu `` verze `GLIBC_2.28' nenalezena ``, obnovte jej pomocí základního obrazu `haskell:8.10.7-stretch` (změňte jej ve svém lokálním [Dockerfile](Dockerfile)). > **Upozornění:** Pokud narazíte na chybu `` verze `GLIBC_2.28' nenalezena ``, obnovte jej pomocí základního obrazu `haskell:8.10.7-stretch` (změňte jej ve svém lokálním [Dockerfile](/Dockerfile)).
#### V libovolném operačním systému #### V libovolném operačním systému
+20 -20
View File
@@ -26,7 +26,7 @@
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) s novými funkcemi o 1-2 týdny dříve - **omezeno na 10 000 uživatelů**! - 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) s novými funkcemi o 1-2 týdny dříve - **omezeno na 10 000 uživatelů**!
- 🖥 K dispozici jako terminálová (konzolová) [aplikace / CLI](#zap-quick-installation-of-a-terminal-app) v systémech Linux, MacOS, Windows. - 🖥 K dispozici jako terminálová (konzolová) [aplikace / CLI](#zap-quick-installation-of-a-terminal-app) v systémech Linux, MacOS, Windows.
**NOVINKA**: Bezpečnostní audit od [Trail of Bits](https://www.trailofbits.com/about), [nové webové stránky](https://simplex.chat) a vydána verze 4.2! [Viz oznámení](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). **NOVINKA**: Bezpečnostní audit od [Trail of Bits](https://www.trailofbits.com/about), [nové webové stránky](https://simplex.chat) a vydána verze 4.2! [Viz oznámení](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
## Obsah ## Obsah
@@ -62,23 +62,23 @@ Nestačí používat end-to-end šifrovaný messenger, všichni bychom měli pou
### Úplné soukromí vaší identity, profilu, kontaktů a metadat. ### Úplné soukromí vaší identity, profilu, kontaktů a metadat.
**Na rozdíl od všech ostatních existujících platforem pro zasílání zpráv nemá SimpleX přiřazeny žádné identifikátory uživatelů** - dokonce ani náhodná čísla. To chrání soukromí toho, s kým komunikujete, a skrývá to před servery platformy SimpleX i před jakýmikoli pozorovateli. [Více informací](./docs/SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata). **Na rozdíl od všech ostatních existujících platforem pro zasílání zpráv nemá SimpleX přiřazeny žádné identifikátory uživatelů** - dokonce ani náhodná čísla. To chrání soukromí toho, s kým komunikujete, a skrývá to před servery platformy SimpleX i před jakýmikoli pozorovateli. [Více informací](./SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata).
### Nejlepší ochrana proti spamu a zneužití ### Nejlepší ochrana proti spamu a zneužití
Protože na platformě SimpleX nemáte žádný identifikátor, nelze vás kontaktovat, pokud nesdílíte odkaz na jednorázovou pozvánku nebo volitelnou dočasnou uživatelskou adresu. [Více informací](./docs/SIMPLEX.md#nejlepší-ochrana-před-spamem-a-zneužitím). Protože na platformě SimpleX nemáte žádný identifikátor, nelze vás kontaktovat, pokud nesdílíte odkaz na jednorázovou pozvánku nebo volitelnou dočasnou uživatelskou adresu. [Více informací](./SIMPLEX.md#nejlepší-ochrana-proti-spamu-a-zneužití).
### Úplné vlastnictví, kontrola a zabezpečení vašich dat ### Úplné vlastnictví, kontrola a zabezpečení vašich dat
SimpleX ukládá všechna uživatelská data na klientských zařízeních, zprávy jsou pouze dočasně uchovávány na relay serverech SimpleX, dokud nejsou přijaty. [Více informací](./docs/SIMPLEX.md#complete-ownership-control-and-security-of-your-data). SimpleX ukládá všechna uživatelská data na klientských zařízeních, zprávy jsou pouze dočasně uchovávány na relay serverech SimpleX, dokud nejsou přijaty. [Více informací](./SIMPLEX.md#complete-ownership-control-and-security-of-your-data).
### Uživatelé vlastní síť SimpleX ### Uživatelé vlastní síť SimpleX
Můžete používat SimpleX s vlastními servery a přitom komunikovat s lidmi, kteří používají servery předkonfigurované v aplikacích nebo jakékoli jiné servery SimpleX. [Více informací](./docs/SIMPLEX.md#users-own-simplex-network). Můžete používat SimpleX s vlastními servery a přitom komunikovat s lidmi, kteří používají servery předkonfigurované v aplikacích nebo jakékoli jiné servery SimpleX. [Více informací](./SIMPLEX.md#users-own-simplex-network).
## Často kladené otázky ## Často kladené otázky
1. _Jak může SimpleX doručovat zprávy bez identifikátorů uživatelů?_ Viz [oznámení o vydání v2](./blog/20220511-simplex-chat-v2-images-files.md#prvni-platforma-zasilani-zpráv-bez-identifikátoru-uživatele), kde je vysvětleno, jak SimpleX funguje. 1. _Jak může SimpleX doručovat zprávy bez identifikátorů uživatelů?_ Viz [oznámení o vydání v2](../../../blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers), kde je vysvětleno, jak SimpleX funguje.
2. _Proč bych neměl používat jen Signal?_ Signal je centralizovaná platforma, která k identifikaci svých uživatelů a jejich kontaktů používá telefonní čísla. To znamená, že zatímco obsah vašich zpráv na službě Signal je chráněn robustním šifrováním end-to-end, pro službu Signal je viditelné velké množství metadat - s kým a kdy hovoříte. 2. _Proč bych neměl používat jen Signal?_ Signal je centralizovaná platforma, která k identifikaci svých uživatelů a jejich kontaktů používá telefonní čísla. To znamená, že zatímco obsah vašich zpráv na službě Signal je chráněn robustním šifrováním end-to-end, pro službu Signal je viditelné velké množství metadat - s kým a kdy hovoříte.
@@ -88,17 +88,17 @@ Můžete používat SimpleX s vlastními servery a přitom komunikovat s lidmi,
Poslední aktualizace: V současné době je k dispozici několik nových aplikací, např: Poslední aktualizace: V současné době je k dispozici několik nových aplikací, např:
[Vydání verze 4.5 - s více uživatelskými profily, návrhem zpráv, izolací transportu a italským rozhraním](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md). [Vydání verze 4.5 - s více uživatelskými profily, návrhem zpráv, izolací transportu a italským rozhraním](../../../blog/20230204-simplex-chat-v4-5-user-chat-profiles.md).
[03. 01. 2023. v4.4 vydána - s mizejícími zprávami, "živými" zprávami, bezpečnostním ověřováním spojení, GIFy a nálepkami a s francouzským jazykem rozhraní](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md). [03. 01. 2023. v4.4 vydána - s mizejícími zprávami, "živými" zprávami, bezpečnostním ověřováním spojení, GIFy a nálepkami a s francouzským jazykem rozhraní](../../../blog/20230103-simplex-chat-v4.4-disappearing-messages.md).
[prosinec 06, 2022. Listopadové recenze a vydána verze 4.3 - s okamžitými hlasovými zprávami, nevratným mazáním odeslaných zpráv a vylepšenou konfigurací serveru](./blog/20221206-simplex-chat-v4.3-hlasove-zpravy.md). [prosinec 06, 2022. Listopadové recenze a vydána verze 4.3 - s okamžitými hlasovými zprávami, nevratným mazáním odeslaných zpráv a vylepšenou konfigurací serveru](../../../blog/20221206-simplex-chat-v4.3-voice-messages.md).
[Nov 08, 2022. Bezpečnostní audit Trail of Bits, vydány nové webové stránky a verze 4.2](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). [Nov 08, 2022. Bezpečnostní audit Trail of Bits, vydány nové webové stránky a verze 4.2](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
[28. 9. 2022. v4.0: šifrovaná lokální databáze chatu a mnoho dalších změn](./blog/20220928-simplex-chat-v4-encrypted-database.md). [28. 9. 2022. v4.0: šifrovaná lokální databáze chatu a mnoho dalších změn](../../../blog/20220928-simplex-chat-v4-encrypted-database.md).
[Všechny aktualizace](./blog) [Všechny aktualizace](../../../blog)
## Vytvoření soukromého připojení ## Vytvoření soukromého připojení
@@ -118,13 +118,13 @@ Po instalaci chatovacího klienta jednoduše spusťte `simplex-chat` z terminál
![simplex-chat](./images/connection.gif) ![simplex-chat](./images/connection.gif)
Více informací o [instalaci a používání terminálové aplikace](./docs/CLI.md). Více informací o [instalaci a používání terminálové aplikace](./CLI.md).
## Návrh platformy SimpleX ## Návrh platformy SimpleX
SimpleX je síť klient-server s unikátní topologií sítě, která využívá redundantní, jednorázové uzly pro předávání zpráv (relay nodes) k asynchronnímu předávání zpráv prostřednictvím jednosměrných (simplexních) front zpráv, což zajišťuje anonymitu příjemce i odesílatele. SimpleX je síť klient-server s unikátní topologií sítě, která využívá redundantní, jednorázové uzly pro předávání zpráv (relay nodes) k asynchronnímu předávání zpráv prostřednictvím jednosměrných (simplexních) front zpráv, což zajišťuje anonymitu příjemce i odesílatele.
Na rozdíl od sítí P2P jsou všechny zprávy předávány přes jeden nebo několik serverových uzlů, které ani nemusí mít perzistenci. Současná implementace [SMP serveru](https://github.com/simplex-chat/simplexmq#smp-server) ve skutečnosti používá ukládání zpráv v paměti a uchovává pouze záznamy o frontách. SimpleX poskytuje lepší ochranu metadat než návrhy P2P, protože k doručování zpráv se nepoužívají globální identifikátory účastníků, a vyhýbá se [problémům sítí P2P](./docs/SIMPLEX.md#comparison-with-p2p-messaging-protocols). Na rozdíl od sítí P2P jsou všechny zprávy předávány přes jeden nebo několik serverových uzlů, které ani nemusí mít perzistenci. Současná implementace [SMP serveru](https://github.com/simplex-chat/simplexmq#smp-server) ve skutečnosti používá ukládání zpráv v paměti a uchovává pouze záznamy o frontách. SimpleX poskytuje lepší ochranu metadat než návrhy P2P, protože k doručování zpráv se nepoužívají globální identifikátory účastníků, a vyhýbá se [problémům sítí P2P](./SIMPLEX.md#comparison-with-p2p-messaging-protocols).
Na rozdíl od federativních sítí nemají uzly serveru **záznamy o uživatelích**, **nekomunikují mezi sebou** a **neukládají zprávy** po jejich doručení příjemcům. Neexistuje způsob, jak zjistit úplný seznam serverů účastnících se sítě SimpleX. Tato konstrukce se vyhýbá problému viditelnosti metadat, který mají všechny federované sítě, a lépe chrání před útoky na celou síť. Na rozdíl od federativních sítí nemají uzly serveru **záznamy o uživatelích**, **nekomunikují mezi sebou** a **neukládají zprávy** po jejich doručení příjemcům. Neexistuje způsob, jak zjistit úplný seznam serverů účastnících se sítě SimpleX. Tato konstrukce se vyhýbá problému viditelnosti metadat, který mají všechny federované sítě, a lépe chrání před útoky na celou síť.
@@ -132,7 +132,7 @@ Informace o uživatelích, jejich kontaktech a skupinách mají pouze klientská
Další informace o cílech a technickém návrhu platformy naleznete v dokumentu [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md). Další informace o cílech a technickém návrhu platformy naleznete v dokumentu [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md).
Formát zpráv zasílaných mezi klienty chatu prostřednictvím [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md) viz [SimpleX Chat Protocol](./docs/protocol/simplex-chat.md). Formát zpráv zasílaných mezi klienty chatu prostřednictvím [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md) viz [SimpleX Chat Protocol](../../protocol/simplex-chat.md).
## Soukromí: technické podrobnosti a omezení ## Soukromí: technické podrobnosti a omezení
@@ -149,7 +149,7 @@ Co je již implementováno:
6. Počínaje verzí v2 protokolu SMP (současná verze je v4) jsou všechna metadata zprávy včetně času, kdy byla zpráva přijata serverem (zaokrouhleno na sekundy), odesílána příjemcům uvnitř šifrované obálky, takže ani v případě kompromitace TLS je nelze pozorovat. 6. Počínaje verzí v2 protokolu SMP (současná verze je v4) jsou všechna metadata zprávy včetně času, kdy byla zpráva přijata serverem (zaokrouhleno na sekundy), odesílána příjemcům uvnitř šifrované obálky, takže ani v případě kompromitace TLS je nelze pozorovat.
7. Pro spojení klient-server je povoleno pouze TLS 1.2/1.3, omezené na kryptografické algoritmy: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448. 7. Pro spojení klient-server je povoleno pouze TLS 1.2/1.3, omezené na kryptografické algoritmy: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
8. Na ochranu proti útokům typu replay vyžadují servery SimpleX [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) jako ID relace v každém klientském příkazu podepsaném efemérním klíčem per-queue. 8. Na ochranu proti útokům typu replay vyžadují servery SimpleX [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) jako ID relace v každém klientském příkazu podepsaném efemérním klíčem per-queue.
9. Pro ochranu vaší IP adresy podporují všichni klienti SimpleX Chat přístup k serverům pro zasílání zpráv přes Tor - více informací najdete v [oznámení o vydání v3.1](./blog/20220808-simplex-chat-v3.1-chat-groups.md). 9. Pro ochranu vaší IP adresy podporují všichni klienti SimpleX Chat přístup k serverům pro zasílání zpráv přes Tor - více informací najdete v [oznámení o vydání v3.1](../../../blog/20220808-simplex-chat-v3.1-chat-groups.md).
10. Šifrování místní databáze s přístupovou frází - kontakty, skupiny a všechny odeslané a přijaté zprávy jsou uloženy šifrovaně. Pokud jste používali SimpleX Chat před verzí 4.0, musíte šifrování povolit prostřednictvím nastavení aplikace. 10. Šifrování místní databáze s přístupovou frází - kontakty, skupiny a všechny odeslané a přijaté zprávy jsou uloženy šifrovaně. Pokud jste používali SimpleX Chat před verzí 4.0, musíte šifrování povolit prostřednictvím nastavení aplikace.
11. Izolace transportu - pro provoz různých uživatelských profilů se používají různá spojení TCP a okruhy Tor, volitelně - pro různá spojení kontaktů a členů skupin. 11. Izolace transportu - pro provoz různých uživatelských profilů se používají různá spojení TCP a okruhy Tor, volitelně - pro různá spojení kontaktů a členů skupin.
@@ -166,7 +166,7 @@ Můžete:
- použít knihovnu SimpleX Chat k integraci funkcí chatu do svých mobilních aplikací. - použít knihovnu SimpleX Chat k integraci funkcí chatu do svých mobilních aplikací.
- vytvářet chatovací boty a služby v jazyce Haskell - viz [simple](./apps/simplex-bot/) a více [advanced chat bot example](./apps/simplex-bot-advanced/). - vytvářet chatovací boty a služby v jazyce Haskell - viz [simple](./apps/simplex-bot/) a více [advanced chat bot example](./apps/simplex-bot-advanced/).
- vytvářet chatovací boty a služby v libovolném jazyce se spuštěným terminálem SimpleX Chat CLI jako lokálním serverem WebSocket. Viz [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) a [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js). - vytvářet chatovací boty a služby v libovolném jazyce se spuštěným terminálem SimpleX Chat CLI jako lokálním serverem WebSocket. Viz [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) a [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js).
- spustit [simplex-chat terminal CLI](./docs/CLI.md) pro provádění jednotlivých příkazů chatu, např. pro odesílání zpráv v rámci provádění shellových skriptů. - spustit [simplex-chat terminal CLI](./CLI.md) pro provádění jednotlivých příkazů chatu, např. pro odesílání zpráv v rámci provádění shellových skriptů.
Pokud uvažujete o vývoji s platformou SimpleX, obraťte se na nás pro případné rady a podporu. Pokud uvažujete o vývoji s platformou SimpleX, obraťte se na nás pro případné rady a podporu.
@@ -228,7 +228,7 @@ Pokud uvažujete o vývoji s platformou SimpleX, obraťte se na nás pro přípa
## Připojte se ke skupině uživatelů ## Připojte se ke skupině uživatelů
Pokud se chcete na něco zeptat, můžete se připojit k anglicky mluvící skupině: [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D) Pokud se chcete na něco zeptat, můžete se připojit k anglicky mluvící skupině: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
Existuje také několik skupin v jiných jazycích než v angličtině, do kterých jsme nechali přeložit rozhraní aplikací. Tyto skupiny slouží k testování a pokládání otázek ostatním uživatelům SimpleX Chat. Ne vždy v nich odpovídáme na dotazy, proto je prosím pokládejte v některé z anglicky mluvících skupin. Existuje také několik skupin v jiných jazycích než v angličtině, do kterých jsme nechali přeložit rozhraní aplikací. Tyto skupiny slouží k testování a pokládání otázek ostatním uživatelům SimpleX Chat. Ne vždy v nich odpovídáme na dotazy, proto je prosím pokládejte v některé z anglicky mluvících skupin.
@@ -253,7 +253,7 @@ Aktuální jazyky rozhraní:
- Italština: [@unbranched](https://github.com/unbranched) - Italština: [@unbranched](https://github.com/unbranched)
- Ruština: projektový tým - Ruština: projektový tým
Jazyky ve vývoji: Čínština, hindština, čeština, japonština, holandština a [mnoho dalších](https://hosted.weblate.org/projects/simplex-chat/#languages). Další jazyky budeme přidávat, jakmile budou některé z již přidaných jazyků dokončeny - navrhněte prosím nové jazyky, projděte si [průvodce překladem](./docs/TRANSLATIONS.md) a kontaktujte nás! Jazyky ve vývoji: Čínština, hindština, čeština, japonština, holandština a [mnoho dalších](https://hosted.weblate.org/projects/simplex-chat/#languages). Další jazyky budeme přidávat, jakmile budou některé z již přidaných jazyků dokončeny - navrhněte prosím nové jazyky, projděte si [průvodce překladem](./TRANSLATIONS.md) a kontaktujte nás!
## Přispívejte ## Přispívejte
@@ -294,7 +294,7 @@ Zakladatel SimpleX Chat
Protokoly a bezpečnostní model [SimpleX](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) byly revidovány a ve verzi 1.0.0 došlo k mnoha zlomovým změnám a vylepšením. Protokoly a bezpečnostní model [SimpleX](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) byly revidovány a ve verzi 1.0.0 došlo k mnoha zlomovým změnám a vylepšením.
Bezpečnostní audit provedla v říjnu 2022 společnost [Trail of Bits](https://www.trailofbits.com/about) a většina oprav byla vydána ve verzi 4.2.0 - viz [oznámení](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). Bezpečnostní audit provedla v říjnu 2022 společnost [Trail of Bits](https://www.trailofbits.com/about) a většina oprav byla vydána ve verzi 4.2.0 - viz [oznámení](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
SimpleX Chat je stále relativně ranou fází platformy (mobilní aplikace byly vydány v březnu 2022), takže můžete objevit některé chyby a chybějící funkce. Velmi oceníme, pokud nám dáte vědět o všem, co je třeba opravit nebo vylepšit. SimpleX Chat je stále relativně ranou fází platformy (mobilní aplikace byly vydány v březnu 2022), takže můžete objevit některé chyby a chybějící funkce. Velmi oceníme, pokud nám dáte vědět o všem, co je třeba opravit nebo vylepšit.
+1 -1
View File
@@ -12,7 +12,7 @@ SMP server je relay server používaný k předávání zpráv v síti SimpleX.
Klienti SimpleX pouze určují, který server bude použit pro příjem zpráv, a to pro každý kontakt (nebo spojení skupiny s členem skupiny) zvlášť, přičemž tyto servery jsou pouze dočasné, protože adresa pro doručování se může změnit. Klienti SimpleX pouze určují, který server bude použit pro příjem zpráv, a to pro každý kontakt (nebo spojení skupiny s členem skupiny) zvlášť, přičemž tyto servery jsou pouze dočasné, protože adresa pro doručování se může změnit.
_Upozornění_: když změníte servery v konfiguraci aplikace, ovlivní to pouze to, který server bude použit pro nové kontakty, stávající kontakty se na nové servery automaticky nepřesunou, ale můžete je přesunout ručně pomocí tlačítka ["Změnit adresu příjmu"](../blog/20221108-simplex-chat-v4.2-bezpecnostni-audit-novy-website.md#zmeny-dorucovani-adresy-beta) na stránkách s informacemi o kontaktech/členech - brzy bude automatizováno. _Upozornění_: když změníte servery v konfiguraci aplikace, ovlivní to pouze to, který server bude použit pro nové kontakty, stávající kontakty se na nové servery automaticky nepřesunou, ale můžete je přesunout ručně pomocí tlačítka ["Změnit adresu příjmu"](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) na stránkách s informacemi o kontaktech/členech - brzy bude automatizováno.
## Instalace ## Instalace
+1 -1
View File
@@ -97,7 +97,7 @@ git checkout stable
DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
``` ```
> **Veuillez noter** : Si vous rencontrez l'erreur ``version `GLIBC_2.28' non trouvée``, reconstruisez-le avec l'image de base `haskell:8.10.7-stretch`(changez-la dans votre [Dockerfile](Dockerfile) local). > **Veuillez noter** : Si vous rencontrez l'erreur ``version `GLIBC_2.28' non trouvée``, reconstruisez-le avec l'image de base `haskell:8.10.7-stretch`(changez-la dans votre [Dockerfile](/Dockerfile) local).
#### Utiliser Haskell stack #### Utiliser Haskell stack
+1 -1
View File
@@ -48,7 +48,7 @@
## Rejoindre des groupes d'utilisateurs ## Rejoindre des groupes d'utilisateurs
Vous pouvez rejoindre un groupe anglophone pour poser des questions : [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D) Vous pouvez rejoindre un groupe anglophone pour poser des questions : [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
Il existe des groupes dans d'autres langues, dans lesquelles l'interface de l'application a été traduite. Ces groupes servent à tester et à poser des questions aux autres utilisateurs de SimpleX Chat : Il existe des groupes dans d'autres langues, dans lesquelles l'interface de l'application a été traduite. Ces groupes servent à tester et à poser des questions aux autres utilisateurs de SimpleX Chat :
+5 -5
View File
@@ -97,7 +97,7 @@ git checkout stable
DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
``` ```
> **Uwaga:** Jeśli napotkasz błąd `` version `GLIBC_2.28' not found ``, przebuduj go z obrazem bazowym `haskell:8.10.7-stretch` (zmień go w Twoim lokalnym pliku [Dockerfile](Dockerfile)). > **Uwaga:** Jeśli napotkasz błąd `` version `GLIBC_2.28' not found ``, przebuduj go z obrazem bazowym `haskell:8.10.7-stretch` (zmień go w Twoim lokalnym pliku [Dockerfile](/Dockerfile)).
#### Używając Haskella na dowolnym systemie operacyjnym #### Używając Haskella na dowolnym systemie operacyjnym
@@ -200,7 +200,7 @@ Po uruchomieniu czatu zostaniesz poproszony o podanie swojej "nazwy wyświetlane
Poniższy schemat przedstawia sposób łączenia się z kontaktem i wysyłania do niego wiadomości: Poniższy schemat przedstawia sposób łączenia się z kontaktem i wysyłania do niego wiadomości:
<div align="center"> <div align="center">
<img align="center" src="../images/how-to-use-simplex.svg"> <img align="center" src="/images/how-to-use-simplex.svg">
</div> </div>
Gdy już skonfigurujesz swój profil lokalny, wpisz `/c` (oznaczające `/connect`), aby utworzyć nowe połączenie i wygenerować zaproszenie. Wyślij to zaproszenie do swojego kontaktu za pośrednictwem dowolnego innego kanału komunikacji. Gdy już skonfigurujesz swój profil lokalny, wpisz `/c` (oznaczające `/connect`), aby utworzyć nowe połączenie i wygenerować zaproszenie. Wyślij to zaproszenie do swojego kontaktu za pośrednictwem dowolnego innego kanału komunikacji.
@@ -219,7 +219,7 @@ Użyj `/help` na czacie, by uzyskać listę pozostałych dostępnych komend.
Aby utworzyć grupę, użyj `/g <nazwa_grupy>`, a następnie dodaj do niej kontakty za pomocą `/a <nazwa_grupy> <nazwa_kontaktu>`. Możesz wysyłać wiadomości do grupy wpisując `#<nazwa_grupy> <wiadomość>`. Użyj `/help groups`, by uzyskać listę pozostałych dostępnych komend. Aby utworzyć grupę, użyj `/g <nazwa_grupy>`, a następnie dodaj do niej kontakty za pomocą `/a <nazwa_grupy> <nazwa_kontaktu>`. Możesz wysyłać wiadomości do grupy wpisując `#<nazwa_grupy> <wiadomość>`. Użyj `/help groups`, by uzyskać listę pozostałych dostępnych komend.
![simplex-chat](../images/groups.gif) ![simplex-chat](/images/groups.gif)
> **Uwaga**: informacje o grupach nie są przechowywane na żadnym serwerze, są one zapisywane jako lista członków w bazie danych aplikacji klientów, do których będą wysyłane wiadomości. > **Uwaga**: informacje o grupach nie są przechowywane na żadnym serwerze, są one zapisywane jako lista członków w bazie danych aplikacji klientów, do których będą wysyłane wiadomości.
@@ -227,7 +227,7 @@ Aby utworzyć grupę, użyj `/g <nazwa_grupy>`, a następnie dodaj do niej konta
Możesz wysłać plik do kontaktu za pomocą `/f @<nazwa_kontaktu> <ścieżka_do_pliku>` - odbiorca będzie musiał go zaakceptować przed rozpoczęciem wysyłania. Użyj `/help files`, by uzyskać listę pozostałych dostępnych komend. Możesz wysłać plik do kontaktu za pomocą `/f @<nazwa_kontaktu> <ścieżka_do_pliku>` - odbiorca będzie musiał go zaakceptować przed rozpoczęciem wysyłania. Użyj `/help files`, by uzyskać listę pozostałych dostępnych komend.
![simplex-chat](../images/files.gif) ![simplex-chat](/images/files.gif)
Możesz wysyłać pliki do grupy za pomocą `/f #<nazwa_grupy> <ścieżka_do_pliku>`. Możesz wysyłać pliki do grupy za pomocą `/f #<nazwa_grupy> <ścieżka_do_pliku>`.
@@ -241,4 +241,4 @@ Prośby o kontakt możesz przyjąć za pomocą komendy `/ac <nazwa>` oraz odrzuc
Użyj `/help address`, by uzyskać listę pozostałych dostępnych komend. Użyj `/help address`, by uzyskać listę pozostałych dostępnych komend.
![simplex-chat](../images/user-addresses.gif) ![simplex-chat](/images/user-addresses.gif)
+37 -37
View File
@@ -50,7 +50,7 @@ Możesz połączyć się z naszym zespołem za pośrednictwem aplikacji, korzyst
Odpowiadamy na pytania manualnie, więc nie jest to natychmiastowe - może to potrwać do 24 godzin. Odpowiadamy na pytania manualnie, więc nie jest to natychmiastowe - może to potrwać do 24 godzin.
Jeśli jesteś zainteresowany pomocą w integracji otwartoźródłowych modeli językowych i [dołączeniem do naszego zespołu](./docs/lang/pl/JOIN_TEAM.md), skontaktuj się z nami. Jeśli jesteś zainteresowany pomocą w integracji otwartoźródłowych modeli językowych i [dołączeniem do naszego zespołu](../../JOIN_TEAM.md), skontaktuj się z nami.
## Dołącz do grup użytkowników ## Dołącz do grup użytkowników
@@ -62,7 +62,7 @@ Możesz również:
- krytykować aplikację i dokonywać porównań z innymi komunikatorami. - krytykować aplikację i dokonywać porównań z innymi komunikatorami.
- udostępniać nowe komunikatory, które Twoim zdaniem mogą być interesujące z punktu widzenia prywatności, o ile nie spamujesz. - udostępniać nowe komunikatory, które Twoim zdaniem mogą być interesujące z punktu widzenia prywatności, o ile nie spamujesz.
- udostępniać niektóre publikacje związane z prywatnością, raczej dość rzadko. - udostępniać niektóre publikacje związane z prywatnością, raczej dość rzadko.
- po wstępnym zatwierdzeniu przez administratora w prywatnej wiadomości, udostępnić link do utworzonej grupy, ale tylko raz. Gdy grupa ma więcej niż 10 członków, może zostać przesłana do [SimpleX Directory Service](./docs/DIRECTORY.md), gdzie nowi użytkownicy będą mogli ją odkryć. - po wstępnym zatwierdzeniu przez administratora w prywatnej wiadomości, udostępnić link do utworzonej grupy, ale tylko raz. Gdy grupa ma więcej niż 10 członków, może zostać przesłana do [SimpleX Directory Service](../../DIRECTORY.md), gdzie nowi użytkownicy będą mogli ją odkryć.
Musisz: Musisz:
- być uprzejmym wobec innych użytkowników. - być uprzejmym wobec innych użytkowników.
@@ -74,7 +74,7 @@ Wiadomości nieprzestrzegające tych zasad będą usuwane, prawo do wysyłania w
Jeśli chcesz zadać jakieś pytania, możesz dołączyć do anglojęzycznej grupy użytkowników: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D) Jeśli chcesz zadać jakieś pytania, możesz dołączyć do anglojęzycznej grupy użytkowników: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
Istnieje również [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) dla deweloperów, którzy na bazie platformy SimpleX tworzą: Istnieje również [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) dla deweloperów, którzy na bazie platformy SimpleX tworzą:
- czatboty i automatyzacje - czatboty i automatyzacje
- integracje z innymi aplikacjami - integracje z innymi aplikacjami
@@ -104,11 +104,11 @@ Kanał, za pośrednictwem którego udostępniasz link, nie musi być bezpieczny
<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app1.png" alt="Wykonaj prywatne połączenie" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app2.png" alt="Conversation" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app3.png" alt="Połączenie wideo" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app1.png" alt="Wykonaj prywatne połączenie" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app2.png" alt="Conversation" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app3.png" alt="Połączenie wideo" height="360">
Po wykonaniu połączenia możesz [zweryfikować kod bezpieczeństwa połączenia](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md#connection-security-verification). Po wykonaniu połączenia możesz [zweryfikować kod bezpieczeństwa połączenia](../../../blog/20230103-simplex-chat-v4.4-disappearing-messages.md#connection-security-verification).
## Poradnik dla użytkownika (NOWE) ## Poradnik dla użytkownika (NOWE)
Przeczytaj o funkcjach i ustawieniach aplikacji w nowym [Przewodniku użytkownika](./docs/guide/README.md). Przeczytaj o funkcjach i ustawieniach aplikacji w nowym [Przewodniku użytkownika](../../guide/README.md).
## Pomóż nam przetłumaczyć SimpleX Chat ## Pomóż nam przetłumaczyć SimpleX Chat
@@ -139,13 +139,13 @@ Dołącz do naszych tłumaczy, aby pomóc SimpleX w rozwoju!
|🇺🇦 uk|Українська| |[![android app](https://hosted.weblate.org/widgets/simplex-chat/uk/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/uk/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/uk/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/uk/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/uk/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/uk/)|| |🇺🇦 uk|Українська| |[![android app](https://hosted.weblate.org/widgets/simplex-chat/uk/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/uk/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/uk/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/uk/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/uk/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/uk/)||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br>&nbsp;|<br><br>[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)|| |🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br>&nbsp;|<br><br>[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)||
Trwają prace nad wersjami językowymi: Arabski, japoński, koreański, portugalski i [inne](https://hosted.weblate.org/projects/simplex-chat/#languages). Będziemy dodawać kolejne języki, gdy niektóre z już dodanych zostaną ukończone - zasugeruj nowe języki, przejrzyj [przewodnik po tłumaczeniach](./docs/lang/pl/TRANSLATIONS.md) i skontaktuj się z nami! Trwają prace nad wersjami językowymi: Arabski, japoński, koreański, portugalski i [inne](https://hosted.weblate.org/projects/simplex-chat/#languages). Będziemy dodawać kolejne języki, gdy niektóre z już dodanych zostaną ukończone - zasugeruj nowe języki, przejrzyj [przewodnik po tłumaczeniach](./TRANSLATIONS.md) i skontaktuj się z nami!
## Kontrybuuj ## Kontrybuuj
Chcielibyśmy, abyś przyczynił się do naszego rozwoju! Możesz nam pomóc: Chcielibyśmy, abyś przyczynił się do naszego rozwoju! Możesz nam pomóc:
- [dzieląc się motywem kolorystycznym](./docs/THEMES.md), którego używasz w aplikacji na Androida! - [dzieląc się motywem kolorystycznym](../../THEMES.md), którego używasz w aplikacji na Androida!
- pisząc samouczki lub poradniki, które dotyczą hostowania serwerów, automatyzacji czatbotów itp. - pisząc samouczki lub poradniki, które dotyczą hostowania serwerów, automatyzacji czatbotów itp.
- współtworząc bazy wiedzy SimpleX Chat. - współtworząc bazy wiedzy SimpleX Chat.
- rozwijając funkcje - skontaktuj się z nami za pośrednictwem czatu, abyśmy mogli pomóc Ci zacząć. - rozwijając funkcje - skontaktuj się z nami za pośrednictwem czatu, abyśmy mogli pomóc Ci zacząć.
@@ -208,23 +208,23 @@ Używanie szyfrowanego komunikatora end-to-end nie jest wystarczające. Powinni
### Kompletna prywatność Twojej tożsamości, profilu, kontaktów i metadanych. ### Kompletna prywatność Twojej tożsamości, profilu, kontaktów i metadanych.
**W przeciwieństwie do innych komunikatorów, SimpleX nie posiada żadnych identyfikatorów przypisanych do użytkowników**. Nie posiada nawet numerów generowanych losowo. Zapewnia to prywatność tego, z kim się komunikujesz, ukrywając jego tożsamość oraz fakt komunikacji przed serwerami platformy SimpleX i wszelkimi obserwatorami [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata). **W przeciwieństwie do innych komunikatorów, SimpleX nie posiada żadnych identyfikatorów przypisanych do użytkowników**. Nie posiada nawet numerów generowanych losowo. Zapewnia to prywatność tego, z kim się komunikujesz, ukrywając jego tożsamość oraz fakt komunikacji przed serwerami platformy SimpleX i wszelkimi obserwatorami [Czytaj więcej](./SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata).
### Najlepsza ochrona przed spamem i nadużyciami ### Najlepsza ochrona przed spamem i nadużyciami
Ponieważ na platformie SimpleX nie masz identyfikatora ani stałego adresu, nikt nie może się z Tobą skontaktować, chyba że udostępnisz jednorazowy lub tymczasowy adres użytkownika, w postaci kodu QR lub linku. [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#the-best-protection-against-spam-and-abuse). Ponieważ na platformie SimpleX nie masz identyfikatora ani stałego adresu, nikt nie może się z Tobą skontaktować, chyba że udostępnisz jednorazowy lub tymczasowy adres użytkownika, w postaci kodu QR lub linku. [Czytaj więcej](./SIMPLEX.md#the-best-protection-against-spam-and-abuse).
### Pełna kontrola i bezpieczeństwo Twoich danych ### Pełna kontrola i bezpieczeństwo Twoich danych
SimpleX przechowuje wszystkie dane użytkownika na urządzeniach klienckich, wiadomości są przechowywane tymczasowo na serwerach przekaźnikowych SimpleX do momentu ich odebrania, po czym są trwale usuwane. [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#complete-ownership-control-and-security-of-your-data). SimpleX przechowuje wszystkie dane użytkownika na urządzeniach klienckich, wiadomości są przechowywane tymczasowo na serwerach przekaźnikowych SimpleX do momentu ich odebrania, po czym są trwale usuwane. [Czytaj więcej](./SIMPLEX.md#complete-ownership-control-and-security-of-your-data).
### Użytkownicy są właścicielami sieci SimpleX ### Użytkownicy są właścicielami sieci SimpleX
Możesz używać SimpleX na własnych serwerach i nadal komunikować się z ludźmi za pomocą serwerów, które są wstępnie skonfigurowane w aplikacjach lub z dowolnymi innymi serwerami SimpleX. [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#users-own-simplex-network). Możesz używać SimpleX na własnych serwerach i nadal komunikować się z ludźmi za pomocą serwerów, które są wstępnie skonfigurowane w aplikacjach lub z dowolnymi innymi serwerami SimpleX. [Czytaj więcej](./SIMPLEX.md#users-own-simplex-network).
## Często zadawane pytania ## Często zadawane pytania
1. _W jaki sposób SimpleX może dostarczać wiadomości bez jakichkolwiek identyfikatorów użytkownika?_ Zobacz [ogłoszenie wydania v2](./blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) wyjaśniające jak SimpleX działa. 1. _W jaki sposób SimpleX może dostarczać wiadomości bez jakichkolwiek identyfikatorów użytkownika?_ Zobacz [ogłoszenie wydania v2](../../../blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) wyjaśniające jak SimpleX działa.
2. _Dlaczego po prostu nie mogę używać Signal?_ Signal to scentralizowana platforma, która wykorzystuje numery telefonów do identyfikacji użytkowników i ich kontaktów. Oznacza to, że podczas gdy treść wiadomości w Signal jest chroniona solidnym szyfrowaniem end-to-end, istnieje duża ilość metadanych widocznych dla Signal - to, z kim rozmawiasz i kiedy. 2. _Dlaczego po prostu nie mogę używać Signal?_ Signal to scentralizowana platforma, która wykorzystuje numery telefonów do identyfikacji użytkowników i ich kontaktów. Oznacza to, że podczas gdy treść wiadomości w Signal jest chroniona solidnym szyfrowaniem end-to-end, istnieje duża ilość metadanych widocznych dla Signal - to, z kim rozmawiasz i kiedy.
@@ -234,29 +234,29 @@ Możesz używać SimpleX na własnych serwerach i nadal komunikować się z lud
Najnowsze i ważne wiadomości: Najnowsze i ważne wiadomości:
[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md) [Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](../../../blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md)
[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) [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) [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). [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). [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).
[Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md). [Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](../../../blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md).
[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md). [May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](../../../blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.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). [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). [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). [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). [Sep 28, 2022. v4.0: encrypted local chat database and many other changes](../../../blog/20220928-simplex-chat-v4-encrypted-database.md).
[All updates](./blog) [All updates](../../../blog)
## :zap: Szybka instalacja terminalowej wersji aplikacji ## :zap: Szybka instalacja terminalowej wersji aplikacji
@@ -268,13 +268,13 @@ Po pobraniu klienta czatu można go uruchomić za pomocą polecenia `simplex-cha
![simplex-chat](./images/connection.gif) ![simplex-chat](./images/connection.gif)
Przeczytaj więcej o [instalowaniu i używaniu terminalowej wersji czatu](./docs/lang/pl/CLI.md). Przeczytaj więcej o [instalowaniu i używaniu terminalowej wersji czatu](./CLI.md).
## Budowa Platformy SimpleX ## Budowa Platformy SimpleX
SimpleX to sieć typu klient-serwer z unikatową topologią sieciową, która wykorzystuje redundantne, jednorazowe węzły przekazywania wiadomości do asynchronicznego przekazywania wiadomości za pośrednictwem jednokierunkowych (simpleksowych) kolejek wiadomości, zapewniając anonimowość odbiorcy i nadawcy. SimpleX to sieć typu klient-serwer z unikatową topologią sieciową, która wykorzystuje redundantne, jednorazowe węzły przekazywania wiadomości do asynchronicznego przekazywania wiadomości za pośrednictwem jednokierunkowych (simpleksowych) kolejek wiadomości, zapewniając anonimowość odbiorcy i nadawcy.
W przeciwieństwie do sieci P2P, wszystkie wiadomości są przekazywane przez jeden lub kilka węzłów serwera, które nawet nie muszą być trwałe. Obecna implementacja [serwera SMP](https://github.com/simplex-chat/simplexmq#smp-server) wykorzystuje przechowywanie wiadomości w pamięci, utrzymując jedynie rejestr kolejki. SimpleX zapewnia lepszą ochronę metadanych niż projekty P2P, ponieważ żadne globalne identyfikatory uczestników nie są używane do dostarczania wiadomości i pozwala to uniknąć [różnych problemów związanych z sieciami P2P](./docs/lang/pl/SIMPLEX.md#comparison-with-p2p-messaging-protocols). W przeciwieństwie do sieci P2P, wszystkie wiadomości są przekazywane przez jeden lub kilka węzłów serwera, które nawet nie muszą być trwałe. Obecna implementacja [serwera SMP](https://github.com/simplex-chat/simplexmq#smp-server) wykorzystuje przechowywanie wiadomości w pamięci, utrzymując jedynie rejestr kolejki. SimpleX zapewnia lepszą ochronę metadanych niż projekty P2P, ponieważ żadne globalne identyfikatory uczestników nie są używane do dostarczania wiadomości i pozwala to uniknąć [różnych problemów związanych z sieciami P2P](./SIMPLEX.md#comparison-with-p2p-messaging-protocols).
W przeciwieństwie do sieci sfederowanych, węzły serwera **nie posiadają danych użytkowników**, **nie komunikują się ze sobą** i **nie przechowują wiadomości** po ich dostarczeniu do odbiorców. Nie ma możliwości na odkrycie pełnej listy serwerów działających w sieci SimpleX. Taka konstrukcja pozwala uniknąć problemu związanego z widocznością metadanych, z którym borykają się wszystkie sieci sfederowane i pozwala ona na lepszą ochronę przed atakami obejmującymi całą sieć. W przeciwieństwie do sieci sfederowanych, węzły serwera **nie posiadają danych użytkowników**, **nie komunikują się ze sobą** i **nie przechowują wiadomości** po ich dostarczeniu do odbiorców. Nie ma możliwości na odkrycie pełnej listy serwerów działających w sieci SimpleX. Taka konstrukcja pozwala uniknąć problemu związanego z widocznością metadanych, z którym borykają się wszystkie sieci sfederowane i pozwala ona na lepszą ochronę przed atakami obejmującymi całą sieć.
@@ -282,29 +282,29 @@ Informacje o użytkownikach, ich kontaktach i grupach znajdują się wyłącznie
Przeczytaj [whitepaper SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md) po więcej informacji o zadaniach platformy oraz by dowiedzieć się jak wygląda koncepcja techniczna modelu. Przeczytaj [whitepaper SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md) po więcej informacji o zadaniach platformy oraz by dowiedzieć się jak wygląda koncepcja techniczna modelu.
Zobacz [Protokół Czatu SimpleX](./docs/protocol/simplex-chat.md) by dowiedzieć się o formacie wiadomości wysyłanych między klientem czatu za pośrednictwem [Protokołu Wiadomości SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md). Zobacz [Protokół Czatu SimpleX](../../protocol/simplex-chat.md) by dowiedzieć się o formacie wiadomości wysyłanych między klientem czatu za pośrednictwem [Protokołu Wiadomości SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md).
## Prywatność i bezpieczeństwo: szczegóły techniczne i ograniczenia ## Prywatność i bezpieczeństwo: szczegóły techniczne i ograniczenia
Prace nad SimpleX Chat wciąż trwają - udostępniamy nowe ulepszenia, gdy tylko będą gotowe. To Ty musisz zdecydować, czy obecny stan jest wystarczająco dobry dla Twojego przypadku zastosowania. Prace nad SimpleX Chat wciąż trwają - udostępniamy nowe ulepszenia, gdy tylko będą gotowe. To Ty musisz zdecydować, czy obecny stan jest wystarczająco dobry dla Twojego przypadku zastosowania.
Stworzyliśmy [słownik pojęć](./docs/GLOSSARY.md) używany do opisu systemów komunikacyjnych, aby pomóc zrozumieć niektóre z poniższych pojęć oraz aby pomóc Ci w porównaniu zalet i wad różnych systemów komunikacyjnych. Stworzyliśmy [słownik pojęć](../../GLOSSARY.md) używany do opisu systemów komunikacyjnych, aby pomóc zrozumieć niektóre z poniższych pojęć oraz aby pomóc Ci w porównaniu zalet i wad różnych systemów komunikacyjnych.
Co zostało już wprowadzone: Co zostało już wprowadzone:
1. Zamiast identyfikatorów użytkownika używanych przez wszystkie inne platformy, nawet te najbardziej prywatne, SimpleX używa [pairwise per-queue identifiers](./docs/GLOSSARY.md#pairwise-pseudonymous-identifier) (2 adresy dla każdej jednokierunkowej kolejki wiadomości, z opcjonalnym trzecim adresem dla powiadomień push na iOS, 2 kolejki w każdym połączeniu między użytkownikami). Sprawia to, że trudniej jest w ten sposób obserwować przebieg połączeń sieciowych na poziomie aplikacji, ponieważ dla `n` użytkowników może istnieć do `n * (n-1)` kolejek wiadomości. 1. Zamiast identyfikatorów użytkownika używanych przez wszystkie inne platformy, nawet te najbardziej prywatne, SimpleX używa [pairwise per-queue identifiers](../../GLOSSARY.md#pairwise-pseudonymous-identifier) (2 adresy dla każdej jednokierunkowej kolejki wiadomości, z opcjonalnym trzecim adresem dla powiadomień push na iOS, 2 kolejki w każdym połączeniu między użytkownikami). Sprawia to, że trudniej jest w ten sposób obserwować przebieg połączeń sieciowych na poziomie aplikacji, ponieważ dla `n` użytkowników może istnieć do `n * (n-1)` kolejek wiadomości.
2. [Szyfrowanie end-to-end](./docs/GLOSSARY.md#end-to-end-encryption) w każdej kolejce wiadomości używając [cryptoboxa NaCl](https://nacl.cr.yp.to/box.html). Zostało to dodane, aby umożliwić redundancję w przyszłości (przekazywanie każdej wiadomości przez kilka serwerów), aby uniknąć posiadania tego samego ciphertext w różnych kolejkach (które byłyby widoczne tylko dla atakującego, w przypadku przejęcia TLS). Klucze szyfrujące używane do tego szyfrowania nie są rotowane, zamiast tego planujemy rotować kolejki. Do negocjacji kluczy używane są klucze Curve25519. 2. [Szyfrowanie end-to-end](../../GLOSSARY.md#end-to-end-encryption) w każdej kolejce wiadomości używając [cryptoboxa NaCl](https://nacl.cr.yp.to/box.html). Zostało to dodane, aby umożliwić redundancję w przyszłości (przekazywanie każdej wiadomości przez kilka serwerów), aby uniknąć posiadania tego samego ciphertext w różnych kolejkach (które byłyby widoczne tylko dla atakującego, w przypadku przejęcia TLS). Klucze szyfrujące używane do tego szyfrowania nie są rotowane, zamiast tego planujemy rotować kolejki. Do negocjacji kluczy używane są klucze Curve25519.
3. Szyfrowanie end-to-end [double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) w każdej rozmowie między dwoma użytkownikami (lub członkami grupy). Jest to ten sam algorytm, który jest używany w Signal i wielu innych komunikatorach; zapewnia on komunikację OTR z [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (każda wiadomość jest szyfrowana własnym kluczem efemerycznym) i [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (klucze są często renegocjowane w ramach wymiany wiadomości). Dwie pary kluczy Curve448 są używane do początkowego [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), strona inicjująca przekazuje te klucze przez link połączenia, a strona akceptująca - w nagłówku wiadomości potwierdzającej. 3. Szyfrowanie end-to-end [double ratchet](../../GLOSSARY.md#double-ratchet-algorithm) w każdej rozmowie między dwoma użytkownikami (lub członkami grupy). Jest to ten sam algorytm, który jest używany w Signal i wielu innych komunikatorach; zapewnia on komunikację OTR z [forward secrecy](../../GLOSSARY.md#forward-secrecy) (każda wiadomość jest szyfrowana własnym kluczem efemerycznym) i [break-in recovery](../../GLOSSARY.md#post-compromise-security) (klucze są często renegocjowane w ramach wymiany wiadomości). Dwie pary kluczy Curve448 są używane do początkowego [key agreement](../../GLOSSARY.md#key-agreement-protocol), strona inicjująca przekazuje te klucze przez link połączenia, a strona akceptująca - w nagłówku wiadomości potwierdzającej.
4. Dodatkowa warstwa szyfrowania przy użyciu NaCL cryptobox dla wiadomości dostarczanych z serwera do odbiorcy. Warstwa ta pozwala uniknąć wspólnego szyfrogramu między wysyłanym i odbieranym ruchem serwera wewnątrz TLS (i nie ma też wspólnych identyfikatorów). 4. Dodatkowa warstwa szyfrowania przy użyciu NaCL cryptobox dla wiadomości dostarczanych z serwera do odbiorcy. Warstwa ta pozwala uniknąć wspólnego szyfrogramu między wysyłanym i odbieranym ruchem serwera wewnątrz TLS (i nie ma też wspólnych identyfikatorów).
5. Kilka poziomów [content padding](./docs/GLOSSARY.md#message-padding) w celu utrudnienia ataków na rozmiar wiadomości. 5. Kilka poziomów [content padding](../../GLOSSARY.md#message-padding) w celu utrudnienia ataków na rozmiar wiadomości.
6. Wszystkie metadane wiadomości, w tym czas odebrania wiadomości przez serwer (zaokrąglony do sekundy), są wysyłane do odbiorców w zaszyfrowanej postaci, więc nawet jeśli TLS zostanie przejęty, nie można ich zobaczyć. 6. Wszystkie metadane wiadomości, w tym czas odebrania wiadomości przez serwer (zaokrąglony do sekundy), są wysyłane do odbiorców w zaszyfrowanej postaci, więc nawet jeśli TLS zostanie przejęty, nie można ich zobaczyć.
7. Dozwolone są tylko TLS 1.2/1.3 dla połączeń klient-serwer, z ograniczeniem do algorytmów kryptograficznych: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448. 7. Dozwolone są tylko TLS 1.2/1.3 dla połączeń klient-serwer, z ograniczeniem do algorytmów kryptograficznych: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
8. Aby zapobiec atakom typu replay, serwery SimpleX wymagają [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) jako identyfikatora sesji w każdym poleceniu klienta podpisanym kluczem efemerycznym dla każdej kolejki. 8. Aby zapobiec atakom typu replay, serwery SimpleX wymagają [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) jako identyfikatora sesji w każdym poleceniu klienta podpisanym kluczem efemerycznym dla każdej kolejki.
9. Aby ochronić swój adres IP, wszystkie klienty SimpleX Chat obsługują dostęp do serwerów komunikacyjnych za pośrednictwem Tora - zobacz [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) po więcej szczegółów. 9. Aby ochronić swój adres IP, wszystkie klienty SimpleX Chat obsługują dostęp do serwerów komunikacyjnych za pośrednictwem Tora - zobacz [v3.1 release announcement](../../../blog/20220808-simplex-chat-v3.1-chat-groups.md) po więcej szczegółów.
10. Lokalne szyfrowanie bazy danych z hasłem - kontakty, grupy oraz wszystkie wysłane i odebrane wiadomości są przechowywane w postaci zaszyfrowanej. Jeśli korzystałeś z SimpleX Chat przed wersją v4.0, musisz włączyć szyfrowanie w ustawieniach aplikacji. 10. Lokalne szyfrowanie bazy danych z hasłem - kontakty, grupy oraz wszystkie wysłane i odebrane wiadomości są przechowywane w postaci zaszyfrowanej. Jeśli korzystałeś z SimpleX Chat przed wersją v4.0, musisz włączyć szyfrowanie w ustawieniach aplikacji.
11. Izolacja transportu - różne połączenia TCP i obwody Tor używane są dla ruchu różnych profili użytkowników, opcjonalnie - dla różnych kontaktów i połączeń członków grupy. 11. Izolacja transportu - różne połączenia TCP i obwody Tor używane są dla ruchu różnych profili użytkowników, opcjonalnie - dla różnych kontaktów i połączeń członków grupy.
12. Ręczne obracanie kolejki wiadomości w celu przeniesienia konwersacji do innego przekaźnika SMP. 12. Ręczne obracanie kolejki wiadomości w celu przeniesienia konwersacji do innego przekaźnika SMP.
13. Wysyłanie zaszyfrowanych plików end-to-end przy użyciu [protokołu XFTP](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html). 13. Wysyłanie zaszyfrowanych plików end-to-end przy użyciu [protokołu XFTP](../../../blog/20230301-simplex-file-transfer-protocol.md).
14. Szyfrowanie plików lokalnych. 14. Szyfrowanie plików lokalnych.
Planujemy dodać: Planujemy dodać:
@@ -322,7 +322,7 @@ Możesz:
- korzystać z biblioteki SimpleX Chat w celu zintegrowania funkcji czatu z aplikacjami mobilnymi. - korzystać z biblioteki SimpleX Chat w celu zintegrowania funkcji czatu z aplikacjami mobilnymi.
- tworzyć boty i usługi czatu w języku Haskell - zobacz [prosty](./apps/simplex-bot/) i bardziej [zaawansowany przykład bota czatu](./apps/simplex-bot-advanced/). - tworzyć boty i usługi czatu w języku Haskell - zobacz [prosty](./apps/simplex-bot/) i bardziej [zaawansowany przykład bota czatu](./apps/simplex-bot-advanced/).
- tworzenie chat botów i usług w dowolnym języku z wykorzystaniem terminala CLI SimpleX Chat jako lokalnego serwera WebSocket. Zobacz [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) i [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js). - tworzenie chat botów i usług w dowolnym języku z wykorzystaniem terminala CLI SimpleX Chat jako lokalnego serwera WebSocket. Zobacz [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) i [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js).
- uruchomić [simplex-chat w terminal ](./docs/lang/pl/CLI.md), aby wykonywać poszczególne polecenia czatu, np. wysyłać wiadomości w ramach wykonywania skryptu powłoki. - uruchomić [simplex-chat w terminal ](./CLI.md), aby wykonywać poszczególne polecenia czatu, np. wysyłać wiadomości w ramach wykonywania skryptu powłoki.
Jeśli chcesz rozwijać platformę SimpleX, skontaktuj się z nami, aby uzyskać porady i wsparcie. Jeśli chcesz rozwijać platformę SimpleX, skontaktuj się z nami, aby uzyskać porady i wsparcie.
@@ -365,7 +365,7 @@ Dołącz również do grupy [#simplex-devs](https://simplex.chat/contact#/?v=1-2
- ✅ Ulepszone połączenia audio i wideo. - ✅ Ulepszone połączenia audio i wideo.
- ✅ Obsługa starszego systemu operacyjnego Android i 32-bitowych procesorów. - ✅ Obsługa starszego systemu operacyjnego Android i 32-bitowych procesorów.
- ✅ Ukryte profile czatu. - ✅ Ukryte profile czatu.
- ✅ Wysyłanie i odbieranie dużych plików przez [protokół XFTP](./blog/20230301-simplex-file-transfer-protocol.md). - ✅ Wysyłanie i odbieranie dużych plików przez [protokół XFTP](../../../blog/20230301-simplex-file-transfer-protocol.md).
- ✅ Wiadomości wideo. - ✅ Wiadomości wideo.
- ✅ Kod dostępu do aplikacji. - ✅ Kod dostępu do aplikacji.
- ✅ Ulepszenie interfejsu Androidowej aplikacji. - ✅ Ulepszenie interfejsu Androidowej aplikacji.
@@ -402,7 +402,7 @@ Dołącz również do grupy [#simplex-devs](https://simplex.chat/contact#/?v=1-2
[Protokoły i model bezpieczeństwa SimpleX](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) zostały poddane przeglądowi i zawierały wiele istotnych zmian i ulepszeń w wersji v1.0.0. [Protokoły i model bezpieczeństwa SimpleX](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) zostały poddane przeglądowi i zawierały wiele istotnych zmian i ulepszeń w wersji v1.0.0.
Audyt bezpieczeństwa został przeprowadzony w październiku 2022 r. przez [Trail of Bits](https://www.trailofbits.com/about), a większość poprawek została wydana w wersji 4.2.0 - zobacz [ogłoszenie](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). Audyt bezpieczeństwa został przeprowadzony w październiku 2022 r. przez [Trail of Bits](https://www.trailofbits.com/about), a większość poprawek została wydana w wersji 4.2.0 - zobacz [ogłoszenie](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
SimpleX Chat jest nadal na stosunkowo wczesnym etapie rozwoju (aplikacje mobilne zostały wydane w marcu 2022 r.), więc możesz odkryć pewne błędy i brakujące funkcje. Będziemy bardzo wdzięczni za poinformowanie nas o wszystkim, co wymaga naprawy lub ulepszenia. SimpleX Chat jest nadal na stosunkowo wczesnym etapie rozwoju (aplikacje mobilne zostały wydane w marcu 2022 r.), więc możesz odkryć pewne błędy i brakujące funkcje. Będziemy bardzo wdzięczni za poinformowanie nas o wszystkim, co wymaga naprawy lub ulepszenia.
+1 -1
View File
@@ -13,7 +13,7 @@ Serwer SMP to serwer przekaźnikowy używany do przekazywania wiadomości w siec
Klienty SimpleX określają tylko, który serwer jest używany do odbierania wiadomości, oddzielnie dla każdego kontaktu (lub połączenia grupowego z członkiem grupy), a serwery te są tylko tymczasowe, ponieważ adres dostawy może ulec zmianie. Klienty SimpleX określają tylko, który serwer jest używany do odbierania wiadomości, oddzielnie dla każdego kontaktu (lub połączenia grupowego z członkiem grupy), a serwery te są tylko tymczasowe, ponieważ adres dostawy może ulec zmianie.
_Uwaga_: gdy zmienisz serwery w ustawieniach aplikacji, wpłynie to tylko na to, który serwer będzie używany dla nowych kontaktów, istniejące kontakty nie zostaną automatycznie przeniesione na nowe serwery, ale możesz przenieść je ręcznie za pomocą przycisku ["Zmień adres odbiorczy"](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) na stronie z informacjami kontaktu/członka - wkrótce zostanie to zautomatyzowane. _Uwaga_: gdy zmienisz serwery w ustawieniach aplikacji, wpłynie to tylko na to, który serwer będzie używany dla nowych kontaktów, istniejące kontakty nie zostaną automatycznie przeniesione na nowe serwery, ale możesz przenieść je ręcznie za pomocą przycisku ["Zmień adres odbiorczy"](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) na stronie z informacjami kontaktu/członka - wkrótce zostanie to zautomatyzowane.
## Instalacja ## Instalacja
+1 -1
View File
@@ -11,7 +11,7 @@ revision: 07.02.2023
Istniejące komunikatory oraz protokoły borykają się ze wszystkimi lub kilkoma podanymi problemami: Istniejące komunikatory oraz protokoły borykają się ze wszystkimi lub kilkoma podanymi problemami:
- Brak zachowania prywatności profilu i kontaktów użytkownika (zachowanie poufności metadanych). - Brak zachowania prywatności profilu i kontaktów użytkownika (zachowanie poufności metadanych).
- Brak ochrony (lub jedynie opcjonalna ochrona) przed atakami MITM przez dostawcę usług przy użyciu szyfrowania [end to end](1) - Brak ochrony (lub jedynie opcjonalna ochrona) przed atakami MITM przez dostawcę usług przy użyciu szyfrowania [end to end][1]
- Niechciane wiadomości (spam i nadużycia). - Niechciane wiadomości (spam i nadużycia).
- Brak własności danych i ich ochrony. - Brak własności danych i ich ochrony.
- Dla nietechnicznych użytkowników używanie niescentralizowanych protokołów jest skomplikowane. - Dla nietechnicznych użytkowników używanie niescentralizowanych protokołów jest skomplikowane.
+1 -1
View File
@@ -1,5 +1,5 @@
name: simplex-chat name: simplex-chat
version: 5.7.2.0 version: 5.8.0.2
#synopsis: #synopsis:
#description: #description:
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."dc111437fda043b4921cf31f6e402a9644ad3324" = "12v35447fm1nfnywymysl81938wz7ns7g17v2vdcq0aawzzgqqnq"; "https://github.com/simplex-chat/simplexmq.git"."e3f5d244c1a435593e33adc023bf1f920f379f8d" = "1klin78kgvgzdvf64nahn3280m7hw5f8wzrca43cmyajm2qp3wfs";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+4 -1
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
name: simplex-chat name: simplex-chat
version: 5.7.2.0 version: 5.8.0.2
category: Web, System, Services, Cryptography category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat author: simplex.chat
@@ -142,6 +142,9 @@ library
Simplex.Chat.Migrations.M20240324_custom_data Simplex.Chat.Migrations.M20240324_custom_data
Simplex.Chat.Migrations.M20240402_item_forwarded Simplex.Chat.Migrations.M20240402_item_forwarded
Simplex.Chat.Migrations.M20240430_ui_theme Simplex.Chat.Migrations.M20240430_ui_theme
Simplex.Chat.Migrations.M20240501_chat_deleted
Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
Simplex.Chat.Mobile Simplex.Chat.Mobile
Simplex.Chat.Mobile.File Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.Shared
+187 -87
View File
@@ -46,6 +46,8 @@ import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map) import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList) import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList)
import Data.Ord (Down (..))
import qualified Data.Set as S
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Text.Encoding (decodeLatin1, encodeUtf8)
@@ -89,8 +91,9 @@ import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription) import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription)
import qualified Simplex.FileTransfer.Description as FD import qualified Simplex.FileTransfer.Description as FD
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
import qualified Simplex.FileTransfer.Transport as XFTP
import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent as Agent
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError, withLockMap) import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, ipAddressProtected, temporaryAgentError, withLockMap)
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Lock (withLock)
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
@@ -99,7 +102,7 @@ import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), Migrati
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
import Simplex.Messaging.Client (defaultNetworkConfig) import Simplex.Messaging.Client (ProxyClientError (..), defaultNetworkConfig)
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import qualified Simplex.Messaging.Crypto.File as CF import qualified Simplex.Messaging.Crypto.File as CF
@@ -108,10 +111,11 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P) import Simplex.Messaging.Parsers (base64P)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
import qualified Simplex.Messaging.Protocol as SMP import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import qualified Simplex.Messaging.TMap as TM import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (TransportError (..))
import Simplex.Messaging.Transport.Client (defaultSocksProxy) import Simplex.Messaging.Transport.Client (defaultSocksProxy)
import Simplex.Messaging.Util import Simplex.Messaging.Util
import Simplex.Messaging.Version import Simplex.Messaging.Version
@@ -425,7 +429,7 @@ startReceiveUserFiles user = do
filesToReceive <- withStore' (`getRcvFilesToReceive` user) filesToReceive <- withStore' (`getRcvFilesToReceive` user)
forM_ filesToReceive $ \ft -> forM_ filesToReceive $ \ft ->
flip catchChatError (toView . CRChatError (Just user)) $ flip catchChatError (toView . CRChatError (Just user)) $
toView =<< receiveFile' user ft Nothing Nothing toView =<< receiveFile' user ft False Nothing Nothing
restoreCalls :: CM' () restoreCalls :: CM' ()
restoreCalls = do restoreCalls = do
@@ -698,10 +702,7 @@ processChatCommand' vr = \case
(,) <$> getAChatItem db vr user chatRef itemId <*> liftIO (getChatItemVersions db itemId) (,) <$> getAChatItem db vr user chatRef itemId <*> liftIO (getChatItemVersions db itemId)
let itemVersions = if null versions then maybeToList $ mkItemVersion ci else versions let itemVersions = if null versions then maybeToList $ mkItemVersion ci else versions
memberDeliveryStatuses <- case (cType, dir) of memberDeliveryStatuses <- case (cType, dir) of
(SCTGroup, SMDSnd) -> do (SCTGroup, SMDSnd) -> L.nonEmpty <$> withStore' (`getGroupSndStatuses` itemId)
withStore' (`getGroupSndStatuses` itemId) >>= \case
[] -> pure Nothing
memStatuses -> pure $ Just $ map (uncurry MemberDeliveryStatus) memStatuses
_ -> pure Nothing _ -> pure Nothing
forwardedFromChatItem <- getForwardedFromItem user ci forwardedFromChatItem <- getForwardedFromItem user ci
pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses, forwardedFromChatItem} pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses, forwardedFromChatItem}
@@ -871,15 +872,15 @@ processChatCommand' vr = \case
throwChatError (CECommandError $ "reaction already " <> if add then "added" else "removed") throwChatError (CECommandError $ "reaction already " <> if add then "added" else "removed")
when (add && length rs >= maxMsgReactions) $ when (add && length rs >= maxMsgReactions) $
throwChatError (CECommandError "too many reactions") throwChatError (CECommandError "too many reactions")
APIForwardChatItem (ChatRef toCType toChatId) (ChatRef fromCType fromChatId) itemId -> withUser $ \user -> case toCType of APIForwardChatItem (ChatRef toCType toChatId) (ChatRef fromCType fromChatId) itemId itemTTL -> withUser $ \user -> case toCType of
CTDirect -> do CTDirect -> do
(cm, ciff) <- prepareForward user (cm, ciff) <- prepareForward user
withContactLock "forwardChatItem, to contact" toChatId $ withContactLock "forwardChatItem, to contact" toChatId $
sendContactContentMessage user toChatId False Nothing cm ciff sendContactContentMessage user toChatId False itemTTL cm ciff
CTGroup -> do CTGroup -> do
(cm, ciff) <- prepareForward user (cm, ciff) <- prepareForward user
withGroupLock "forwardChatItem, to group" toChatId $ withGroupLock "forwardChatItem, to group" toChatId $
sendGroupContentMessage user toChatId False Nothing cm ciff sendGroupContentMessage user toChatId False itemTTL cm ciff
CTLocal -> do CTLocal -> do
(cm, ciff) <- prepareForward user (cm, ciff) <- prepareForward user
createNoteFolderContentItem user toChatId cm ciff createNoteFolderContentItem user toChatId cm ciff
@@ -1022,22 +1023,41 @@ processChatCommand' vr = \case
liftIO $ updateNoteFolderUnreadChat db user nf unreadChat liftIO $ updateNoteFolderUnreadChat db user nf unreadChat
ok user ok user
_ -> pure $ chatCmdError (Just user) "not supported" _ -> pure $ chatCmdError (Just user) "not supported"
APIDeleteChat (ChatRef cType chatId) notify -> withUser $ \user@User {userId} -> case cType of APIDeleteChat cRef@(ChatRef cType chatId) cdm -> withUser $ \user@User {userId} -> case cType of
CTDirect -> do CTDirect -> do
ct <- withStore $ \db -> getContact db vr user chatId ct <- withStore $ \db -> getContact db vr user chatId
filesInfo <- withStore' $ \db -> getContactFileInfo db user ct filesInfo <- withStore' $ \db -> getContactFileInfo db user ct
withContactLock "deleteChat direct" chatId . procCmd $ do withContactLock "deleteChat direct" chatId . procCmd $
cancelFilesInProgress user filesInfo case cdm of
deleteFilesLocally filesInfo CDMFull notify -> do
let doSendDel = contactReady ct && contactActive ct && notify cancelFilesInProgress user filesInfo
when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchChatError` const (pure ()) deleteFilesLocally filesInfo
contactConnIds <- map aConnId <$> withStore' (\db -> getContactConnections db vr userId ct) sendDelDeleteConns ct notify
deleteAgentConnectionsAsync' user contactConnIds doSendDel -- functions below are called in separate transactions to prevent crashes on android
-- functions below are called in separate transactions to prevent crashes on android -- (possibly, race condition on integrity check?)
-- (possibly, race condition on integrity check?) withStore' $ \db -> do
withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct deleteContactConnections db user ct
withStore $ \db -> deleteContact db user ct deleteContactFiles db user ct
pure $ CRContactDeleted user ct withStore $ \db -> deleteContact db user ct
pure $ CRContactDeleted user ct
CDMEntity notify -> do
cancelFilesInProgress user filesInfo
sendDelDeleteConns ct notify
ct' <- withStore $ \db -> do
liftIO $ deleteContactConnections db user ct
liftIO $ void $ updateContactStatus db user ct CSDeletedByUser
getContact db vr user chatId
pure $ CRContactDeleted user ct'
CDMMessages -> do
void $ processChatCommand $ APIClearChat cRef
withStore' $ \db -> setContactChatDeleted db user ct True
pure $ CRContactDeleted user ct {chatDeleted = True}
where
sendDelDeleteConns ct notify = do
let doSendDel = contactReady ct && contactActive ct && notify
when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchChatError` const (pure ())
contactConnIds <- map aConnId <$> withStore' (\db -> getContactConnections db vr userId ct)
deleteAgentConnectionsAsync' user contactConnIds doSendDel
CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId . procCmd $ do CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId . procCmd $ do
conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withStore $ \db -> getPendingContactConnection db userId chatId conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withStore $ \db -> getPendingContactConnection db userId chatId
deleteAgentConnectionAsync user acId deleteAgentConnectionAsync user acId
@@ -1554,7 +1574,7 @@ processChatCommand' vr = \case
CPContactAddress (CAPContactViaAddress Contact {contactId}) -> CPContactAddress (CAPContactViaAddress Contact {contactId}) ->
processChatCommand $ APIConnectContactViaAddress userId incognito contactId processChatCommand $ APIConnectContactViaAddress userId incognito contactId
_ -> processChatCommand $ APIConnect userId incognito (Just cReqUri) _ -> processChatCommand $ APIConnect userId incognito (Just cReqUri)
DeleteContact cName -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) True DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) cdm
ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect
APIListContacts userId -> withUserId userId $ \user -> APIListContacts userId -> withUserId userId $ \user ->
CRContactsList user <$> withStore' (\db -> getUserContacts db vr user) CRContactsList user <$> withStore' (\db -> getUserContacts db vr user)
@@ -1608,17 +1628,17 @@ processChatCommand' vr = \case
contactId <- withStore $ \db -> getContactIdByName db user fromContactName contactId <- withStore $ \db -> getContactIdByName db user fromContactName
forwardedItemId <- withStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg forwardedItemId <- withStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg
toChatRef <- getChatRef user toChatName toChatRef <- getChatRef user toChatName
processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTDirect contactId) forwardedItemId processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTDirect contactId) forwardedItemId Nothing
ForwardGroupMessage toChatName fromGroupName fromMemberName_ forwardedMsg -> withUser $ \user -> do ForwardGroupMessage toChatName fromGroupName fromMemberName_ forwardedMsg -> withUser $ \user -> do
groupId <- withStore $ \db -> getGroupIdByName db user fromGroupName groupId <- withStore $ \db -> getGroupIdByName db user fromGroupName
forwardedItemId <- withStore $ \db -> getGroupChatItemIdByText db user groupId fromMemberName_ forwardedMsg forwardedItemId <- withStore $ \db -> getGroupChatItemIdByText db user groupId fromMemberName_ forwardedMsg
toChatRef <- getChatRef user toChatName toChatRef <- getChatRef user toChatName
processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTGroup groupId) forwardedItemId processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTGroup groupId) forwardedItemId Nothing
ForwardLocalMessage toChatName forwardedMsg -> withUser $ \user -> do ForwardLocalMessage toChatName forwardedMsg -> withUser $ \user -> do
folderId <- withStore (`getUserNoteFolderId` user) folderId <- withStore (`getUserNoteFolderId` user)
forwardedItemId <- withStore $ \db -> getLocalChatItemIdByText' db user folderId forwardedMsg forwardedItemId <- withStore $ \db -> getLocalChatItemIdByText' db user folderId forwardedMsg
toChatRef <- getChatRef user toChatName toChatRef <- getChatRef user toChatName
processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTLocal folderId) forwardedItemId processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTLocal folderId) forwardedItemId Nothing
SendMessage (ChatName cType name) msg -> withUser $ \user -> do SendMessage (ChatName cType name) msg -> withUser $ \user -> do
let mc = MCText msg let mc = MCText msg
case cType of case cType of
@@ -1889,7 +1909,7 @@ processChatCommand' vr = \case
processChatCommand $ APILeaveGroup groupId processChatCommand $ APILeaveGroup groupId
DeleteGroup gName -> withUser $ \user -> do DeleteGroup gName -> withUser $ \user -> do
groupId <- withStore $ \db -> getGroupIdByName db user gName groupId <- withStore $ \db -> getGroupIdByName db user gName
processChatCommand $ APIDeleteChat (ChatRef CTGroup groupId) True processChatCommand $ APIDeleteChat (ChatRef CTGroup groupId) (CDMFull True)
ClearGroup gName -> withUser $ \user -> do ClearGroup gName -> withUser $ \user -> do
groupId <- withStore $ \db -> getGroupIdByName db user gName groupId <- withStore $ \db -> getGroupIdByName db user gName
processChatCommand $ APIClearChat (ChatRef CTGroup groupId) processChatCommand $ APIClearChat (ChatRef CTGroup groupId)
@@ -2037,17 +2057,17 @@ processChatCommand' vr = \case
ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardFile chatName fileId -> forwardFile chatName fileId SendFile
ForwardImage chatName fileId -> forwardFile chatName fileId SendImage ForwardImage chatName fileId -> forwardFile chatName fileId SendImage
SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO"
ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> ReceiveFile fileId userApprovedRelays encrypted_ rcvInline_ filePath_ -> withUser $ \_ ->
withFileLock "receiveFile" fileId . procCmd $ do withFileLock "receiveFile" fileId . procCmd $ do
(user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId) (user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId)
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft
receiveFile' user ft' rcvInline_ filePath_ receiveFile' user ft' userApprovedRelays rcvInline_ filePath_
SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do SetFileToReceive fileId userApprovedRelays encrypted_ -> withUser $ \_ -> do
withFileLock "setFileToReceive" fileId . procCmd $ do withFileLock "setFileToReceive" fileId . procCmd $ do
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing
withStore' $ \db -> setRcvFileToReceive db fileId cfArgs withStore' $ \db -> setRcvFileToReceive db fileId userApprovedRelays cfArgs
ok_ ok_
CancelFile fileId -> withUser $ \user@User {userId} -> CancelFile fileId -> withUser $ \user@User {userId} ->
withFileLock "cancelFile" fileId . procCmd $ withFileLock "cancelFile" fileId . procCmd $
@@ -2087,13 +2107,8 @@ processChatCommand' vr = \case
liftIO $ removeFile fsFilePath `catchAll_` pure () liftIO $ removeFile fsFilePath `catchAll_` pure ()
lift . forM_ agentRcvFileId $ \(AgentRcvFileId aFileId) -> lift . forM_ agentRcvFileId $ \(AgentRcvFileId aFileId) ->
withAgent' (`xftpDeleteRcvFile` aFileId) withAgent' (`xftpDeleteRcvFile` aFileId)
ci <- withStore $ \db -> do aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation
liftIO $ do pure $ CRRcvFileCancelled user aci_ ftr
updateCIFileStatus db user fileId CIFSRcvInvitation
updateRcvFileStatus db fileId FSNew
updateRcvFileAgentId db fileId Nothing
lookupChatItemByFileId db vr user fileId
pure $ CRRcvFileCancelled user ci ftr
FileStatus fileId -> withUser $ \user -> do FileStatus fileId -> withUser $ \user -> do
withStore (\db -> lookupChatItemByFileId db vr user fileId) >>= \case withStore (\db -> lookupChatItemByFileId db vr user fileId) >>= \case
Nothing -> do Nothing -> do
@@ -2203,6 +2218,10 @@ processChatCommand' vr = \case
stat (AgentStatsKey {host, clientTs, cmd, res}, count) = stat (AgentStatsKey {host, clientTs, cmd, res}, count) =
map B.unpack [host, clientTs, cmd, res, bshow count] map B.unpack [host, clientTs, cmd, res, bshow count]
ResetAgentStats -> lift (withAgent' resetAgentStats) >> ok_ ResetAgentStats -> lift (withAgent' resetAgentStats) >> ok_
GetAgentMsgCounts -> lift $ do
counts <- map (first decodeLatin1) <$> withAgent' getMsgCounts
let allMsgs = foldl' (\(ts, ds) (_, (t, d)) -> (ts + t, ds + d)) (0, 0) counts
pure CRAgentMsgCounts {msgCounts = ("all", allMsgs) : sortOn (Down . snd) (filter (\(_, (_, d)) -> d /= 0) counts)}
GetAgentSubs -> lift $ summary <$> withAgent' getAgentSubscriptions GetAgentSubs -> lift $ summary <$> withAgent' getAgentSubscriptions
where where
summary SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} = summary SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} =
@@ -3030,18 +3049,18 @@ setFileToEncrypt ft@RcvFileTransfer {fileId} = do
withStore' $ \db -> setFileCryptoArgs db fileId cfArgs withStore' $ \db -> setFileCryptoArgs db fileId cfArgs
pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs} pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs}
receiveFile' :: User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> CM ChatResponse receiveFile' :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM ChatResponse
receiveFile' user ft rcvInline_ filePath_ = do receiveFile' user ft userApprovedRelays rcvInline_ filePath_ = do
(CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchChatError` processError (CRRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchChatError` processError
where where
processError = \case processError = \case
-- TODO AChatItem in Cancelled events -- TODO AChatItem in Cancelled events
ChatErrorAgent (SMP SMP.AUTH) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft ChatErrorAgent (SMP _ SMP.AUTH) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
e -> throwError e e -> throwError e
acceptFileReceive :: User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> CM AChatItem acceptFileReceive :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM AChatItem
acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} rcvInline_ filePath_ = do acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} userApprovedRelays rcvInline_ filePath_ = do
unless (fileStatus == RFSNew) $ case fileStatus of unless (fileStatus == RFSNew) $ case fileStatus of
RFSCancelled _ -> throwChatError $ CEFileCancelled fName RFSCancelled _ -> throwChatError $ CEFileCancelled fName
_ -> throwChatError $ CEFileAlreadyReceiving fName _ -> throwChatError $ CEFileAlreadyReceiving fName
@@ -3055,15 +3074,16 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
filePath <- getRcvFilePath fileId filePath_ fName True filePath <- getRcvFilePath fileId filePath_ fName True
withStore $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnJoined filePath subMode withStore $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnJoined filePath subMode
-- XFTP -- XFTP
(Just XFTPRcvFile {}, _) -> do (Just XFTPRcvFile {userApprovedRelays = approvedBeforeReady}, _) -> do
let userApproved = approvedBeforeReady || userApprovedRelays
filePath <- getRcvFilePath fileId filePath_ fName False filePath <- getRcvFilePath fileId filePath_ fName False
(ci, rfd) <- withStore $ \db -> do (ci, rfd) <- withStore $ \db -> do
-- marking file as accepted and reading description in the same transaction -- marking file as accepted and reading description in the same transaction
-- to prevent race condition with appending description -- to prevent race condition with appending description
ci <- xftpAcceptRcvFT db vr user fileId filePath ci <- xftpAcceptRcvFT db vr user fileId filePath userApproved
rfd <- getRcvFileDescrByRcvFileId db fileId rfd <- getRcvFileDescrByRcvFileId db fileId
pure (ci, rfd) pure (ci, rfd)
receiveViaCompleteFD user fileId rfd cryptoArgs receiveViaCompleteFD user fileId rfd userApproved cryptoArgs
pure ci pure ci
-- group & direct file protocol -- group & direct file protocol
_ -> do _ -> do
@@ -3108,18 +3128,61 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
|| (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks) || (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks)
) )
receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Maybe CryptoFileArgs -> CM () receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Bool -> Maybe CryptoFileArgs -> CM ()
receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} cfArgs = receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} userApprovedRelays cfArgs =
when fileDescrComplete $ do when fileDescrComplete $ do
rd <- parseFileDescription fileDescrText rd <- parseFileDescription fileDescrText
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs if userApprovedRelays
startReceivingFile user fileId then receive' rd True
withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId) else do
let srvs = fileServers rd
unknownSrvs <- getUnknownSrvs srvs
let approved = null unknownSrvs
ifM
((approved ||) <$> ipProtectedForSrvs srvs)
(receive' rd approved)
(relaysNotApproved unknownSrvs)
where
receive' :: ValidFileDescription 'FRecipient -> Bool -> CM ()
receive' rd approved = do
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs approved
startReceivingFile user fileId
withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId)
fileServers :: ValidFileDescription 'FRecipient -> [XFTPServer]
fileServers (FD.ValidFileDescription FD.FileDescription {chunks}) =
S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks
getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer]
getUnknownSrvs srvs = do
ChatConfig {defaultServers = DefaultAgentServers {xftp = defXftp}} <- asks config
storedSrvs <- map (\ServerCfg {server} -> protoServer server) <$> withStore' (`getProtocolServers` user)
let defXftp' = L.map protoServer defXftp
knownSrvs = fromMaybe defXftp' $ nonEmpty storedSrvs
pure $ filter (`notElem` knownSrvs) srvs
ipProtectedForSrvs :: [XFTPServer] -> CM Bool
ipProtectedForSrvs srvs = do
netCfg <- lift $ withAgent' getNetworkConfig
pure $ all (ipAddressProtected netCfg) srvs
relaysNotApproved :: [XFTPServer] -> CM ()
relaysNotApproved unknownSrvs = do
aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation
forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci
throwChatError $ CEFileNotApproved fileId unknownSrvs
resetRcvCIFileStatus :: User -> FileTransferId -> CIFileStatus 'MDRcv -> CM (Maybe AChatItem)
resetRcvCIFileStatus user fileId ciFileStatus = do
vr <- chatVersionRange
withStore $ \db -> do
liftIO $ do
updateCIFileStatus db user fileId ciFileStatus
updateRcvFileStatus db fileId FSNew
updateRcvFileAgentId db fileId Nothing
lookupChatItemByFileId db vr user fileId
receiveViaURI :: User -> FileDescriptionURI -> CryptoFile -> CM RcvFileTransfer receiveViaURI :: User -> FileDescriptionURI -> CryptoFile -> CM RcvFileTransfer
receiveViaURI user@User {userId} FileDescriptionURI {description} cf@CryptoFile {cryptoArgs} = do receiveViaURI user@User {userId} FileDescriptionURI {description} cf@CryptoFile {cryptoArgs} = do
fileId <- withStore $ \db -> createRcvStandaloneFileTransfer db userId cf fileSize chunkSize fileId <- withStore $ \db -> createRcvStandaloneFileTransfer db userId cf fileSize chunkSize
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) description cryptoArgs -- currently the only use case is user migrating via their configured servers, so we pass approvedRelays = True
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) description cryptoArgs True
withStore $ \db -> do withStore $ \db -> do
liftIO $ do liftIO $ do
updateRcvFileStatus db fileId FSConnected updateRcvFileStatus db fileId FSConnected
@@ -3373,7 +3436,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
errorNetworkStatus :: ChatError -> String errorNetworkStatus :: ChatError -> String
errorNetworkStatus = \case errorNetworkStatus = \case
ChatErrorAgent (BROKER _ NETWORK) _ -> "network" ChatErrorAgent (BROKER _ NETWORK) _ -> "network"
ChatErrorAgent (SMP SMP.AUTH) _ -> "contact deleted" ChatErrorAgent (SMP _ SMP.AUTH) _ -> "contact deleted"
e -> show e e -> show e
-- TODO possibly below could be replaced with less noisy events for API -- TODO possibly below could be replaced with less noisy events for API
contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> CM () contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> CM ()
@@ -3789,6 +3852,10 @@ processAgentMsgRcvFile _corrId aFileId msg = do
RFERR e RFERR e
| temporaryAgentError e -> | temporaryAgentError e ->
throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e
| e == XFTP "" XFTP.NOT_APPROVED -> do
aci_ <- resetRcvCIFileStatus user fileId CIFSRcvAborted
agentXFTPDeleteRcvFile aFileId fileId
forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci
| otherwise -> do | otherwise -> do
ci <- withStore $ \db -> do ci <- withStore $ \db -> do
liftIO $ updateFileCancelled db user fileId CIFSRcvError liftIO $ updateFileCancelled db user fileId CIFSRcvError
@@ -3872,7 +3939,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
withAckMessage' agentConnId meta $ withAckMessage' agentConnId meta $
void $ void $
saveDirectRcvMSG conn meta msgBody saveDirectRcvMSG conn meta msgBody
SENT msgId -> SENT msgId _proxy ->
sentMsgDeliveryEvent conn msgId sentMsgDeliveryEvent conn msgId
OK -> OK ->
-- [async agent commands] continuation on receiving OK -- [async agent commands] continuation on receiving OK
@@ -4005,10 +4072,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
notifyMemberConnected gInfo m $ Just ct notifyMemberConnected gInfo m $ Just ct
let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo
when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito True when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito True
SENT msgId -> do SENT msgId proxy -> do
sentMsgDeliveryEvent conn msgId sentMsgDeliveryEvent conn msgId
checkSndInlineFTComplete conn msgId checkSndInlineFTComplete conn msgId
updateDirectItemStatus ct conn msgId $ CISSndSent SSPComplete ci_ <- withStore $ \db -> do
ci_ <- updateDirectItemStatus' db ct conn msgId (CISSndSent SSPComplete)
forM ci_ $ \ci -> liftIO $ setDirectSndChatItemViaProxy db user ct ci (isJust proxy)
forM_ ci_ $ \ci -> toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci)
SWITCH qd phase cStats -> do SWITCH qd phase cStats -> do
toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats) toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats)
when (phase `elem` [SPStarted, SPCompleted]) $ case qd of when (phase `elem` [SPStarted, SPCompleted]) $ case qd of
@@ -4043,13 +4113,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
OK -> OK ->
-- [async agent commands] continuation on receiving OK -- [async agent commands] continuation on receiving OK
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
MWARN msgId err ->
updateDirectItemStatus ct conn msgId (CISSndWarning $ agentSndError err)
MERR msgId err -> do MERR msgId err -> do
updateDirectItemStatus ct conn msgId $ agentErrToItemStatus err updateDirectItemStatus ct conn msgId (CISSndError $ agentSndError err)
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
incAuthErrCounter connEntity conn err incAuthErrCounter connEntity conn err
MERRS msgIds err -> do MERRS msgIds err -> do
-- error cannot be AUTH error here -- error cannot be AUTH error here
updateDirectItemsStatus ct conn (L.toList msgIds) $ agentErrToItemStatus err updateDirectItemsStatus ct conn (L.toList msgIds) (CISSndError $ agentSndError err)
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
ERR err -> do ERR err -> do
toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
@@ -4387,10 +4459,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
RCVD msgMeta msgRcpt -> RCVD msgMeta msgRcpt ->
withAckMessage' agentConnId msgMeta $ withAckMessage' agentConnId msgMeta $
groupMsgReceived gInfo m conn msgMeta msgRcpt groupMsgReceived gInfo m conn msgMeta msgRcpt
SENT msgId -> do SENT msgId proxy -> do
sentMsgDeliveryEvent conn msgId sentMsgDeliveryEvent conn msgId
checkSndInlineFTComplete conn msgId checkSndInlineFTComplete conn msgId
updateGroupItemStatus gInfo m conn msgId $ CISSndSent SSPComplete updateGroupItemStatus gInfo m conn msgId (CISSndSent SSPComplete) (Just $ isJust proxy)
SWITCH qd phase cStats -> do SWITCH qd phase cStats -> do
toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats)
when (phase `elem` [SPStarted, SPCompleted]) $ case qd of when (phase `elem` [SPStarted, SPCompleted]) $ case qd of
@@ -4426,13 +4498,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
OK -> OK ->
-- [async agent commands] continuation on receiving OK -- [async agent commands] continuation on receiving OK
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
MWARN msgId err ->
withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) (CISSndWarning $ agentSndError err)
MERR msgId err -> do MERR msgId err -> do
withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) $ agentErrToItemStatus err withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) (CISSndError $ agentSndError err)
-- group errors are silenced to reduce load on UI event log -- group errors are silenced to reduce load on UI event log
-- toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) -- toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity)
incAuthErrCounter connEntity conn err incAuthErrCounter connEntity conn err
MERRS msgIds err -> do MERRS msgIds err -> do
let newStatus = agentErrToItemStatus err let newStatus = CISSndError $ agentSndError err
-- error cannot be AUTH error here -- error cannot be AUTH error here
withStore' $ \db -> forM_ msgIds $ \msgId -> withStore' $ \db -> forM_ msgIds $ \msgId ->
updateGroupItemErrorStatus db msgId (groupMemberId' m) newStatus `catchAll_` pure () updateGroupItemErrorStatus db msgId (groupMemberId' m) newStatus `catchAll_` pure ()
@@ -4493,13 +4567,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1 updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1
toView $ CRSndFileStart user ci ft toView $ CRSndFileStart user ci ft
sendFileChunk user ft sendFileChunk user ft
SENT msgId -> do SENT msgId _proxy -> do
withStore' $ \db -> updateSndFileChunkSent db ft msgId withStore' $ \db -> updateSndFileChunkSent db ft msgId
unless (fileStatus == FSCancelled) $ sendFileChunk user ft unless (fileStatus == FSCancelled) $ sendFileChunk user ft
MERR _ err -> do MERR _ err -> do
cancelSndFileTransfer user ft True >>= mapM_ (deleteAgentConnectionAsync user) cancelSndFileTransfer user ft True >>= mapM_ (deleteAgentConnectionAsync user)
case err of case err of
SMP SMP.AUTH -> unless (fileStatus == FSCancelled) $ do SMP _ SMP.AUTH -> unless (fileStatus == FSCancelled) $ do
ci <- withStore $ \db -> do ci <- withStore $ \db -> do
liftIO (lookupChatRefByFileId db user fileId) >>= \case liftIO (lookupChatRefByFileId db user fileId) >>= \case
Just (ChatRef CTDirect _) -> liftIO $ updateFileCancelled db user fileId CIFSSndCancelled Just (ChatRef CTDirect _) -> liftIO $ updateFileCancelled db user fileId CIFSSndCancelled
@@ -4654,10 +4728,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
incAuthErrCounter :: ConnectionEntity -> Connection -> AgentErrorType -> CM () incAuthErrCounter :: ConnectionEntity -> Connection -> AgentErrorType -> CM ()
incAuthErrCounter connEntity conn err = do incAuthErrCounter connEntity conn err = do
case err of case err of
SMP SMP.AUTH -> do SMP _ SMP.AUTH -> do
authErrCounter' <- withStore' $ \db -> incConnectionAuthErrCounter db user conn authErrCounter' <- withStore' $ \db -> incConnectionAuthErrCounter db user conn
when (authErrCounter' >= authErrDisableCount) $ do when (authErrCounter' >= authErrDisableCount) $ case connEntity of
toView $ CRConnectionDisabled connEntity RcvDirectMsgConnection ctConn (Just ct) -> do
toView $ CRContactDisabled user ct {activeConn = Just ctConn {authErrCounter = authErrCounter'}}
_ -> toView $ CRConnectionDisabled connEntity
_ -> pure () _ -> pure ()
-- TODO v5.7 / v6.0 - together with deprecating old group protocol establishing direct connections? -- TODO v5.7 / v6.0 - together with deprecating old group protocol establishing direct connections?
@@ -4705,9 +4781,21 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
sentMsgDeliveryEvent Connection {connId} msgId = sentMsgDeliveryEvent Connection {connId} msgId =
withStore' $ \db -> updateSndMsgDeliveryStatus db connId msgId MDSSndSent withStore' $ \db -> updateSndMsgDeliveryStatus db connId msgId MDSSndSent
agentErrToItemStatus :: AgentErrorType -> CIStatus 'MDSnd agentSndError :: AgentErrorType -> SndError
agentErrToItemStatus (SMP AUTH) = CISSndErrorAuth agentSndError = \case
agentErrToItemStatus err = CISSndError . T.unpack . safeDecodeUtf8 $ strEncode err SMP _ AUTH -> SndErrAuth
SMP _ QUOTA -> SndErrQuota
BROKER _ e -> brokerError SndErrRelay e
SMP proxySrv (SMP.PROXY (SMP.BROKER e)) -> brokerError (SndErrProxy proxySrv) e
AP.PROXY proxySrv _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> brokerError (SndErrProxyRelay proxySrv) e
e -> SndErrOther . safeDecodeUtf8 $ strEncode e
where
brokerError srvErr = \case
NETWORK -> SndErrExpired
TIMEOUT -> SndErrExpired
HOST -> srvErr SrvErrHost
SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion
e -> srvErr . SrvErrOther . safeDecodeUtf8 $ strEncode e
badRcvFileChunk :: RcvFileTransfer -> String -> CM () badRcvFileChunk :: RcvFileTransfer -> String -> CM ()
badRcvFileChunk ft err = badRcvFileChunk ft err =
@@ -4819,8 +4907,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> CM () autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> CM ()
autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do
-- ! autoAcceptFileSize is only used in tests
ChatConfig {autoAcceptFileSize = sz} <- asks config ChatConfig {autoAcceptFileSize = sz} <- asks config
when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView when (sz > fileSize) $ receiveFile' user ft False Nothing Nothing >>= toView
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> CM () messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> CM ()
messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr = do messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr = do
@@ -4846,7 +4935,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
ci <- withStore $ \db -> getAChatItemBySharedMsgId db user cd sharedMsgId ci <- withStore $ \db -> getAChatItemBySharedMsgId db user cd sharedMsgId
toView $ CRRcvFileDescrReady user ci ft' rfd toView $ CRRcvFileDescrReady user ci ft' rfd
case (fileStatus, xftpRcvFile) of case (fileStatus, xftpRcvFile) of
(RFSAccepted _, Just XFTPRcvFile {}) -> receiveViaCompleteFD user fileId rfd cryptoArgs (RFSAccepted _, Just XFTPRcvFile {userApprovedRelays}) -> receiveViaCompleteFD user fileId rfd userApprovedRelays cryptoArgs
_ -> pure () _ -> pure ()
processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv)) processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv))
@@ -6036,7 +6125,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure () checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure ()
forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do
withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus
updateGroupItemStatus gInfo m conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete updateGroupItemStatus gInfo m conn agentMsgId (CISSndRcvd msgRcptStatus SSPComplete) Nothing
updateDirectItemsStatus :: Contact -> Connection -> [AgentMsgId] -> CIStatus 'MDSnd -> CM () updateDirectItemsStatus :: Contact -> Connection -> [AgentMsgId] -> CIStatus 'MDSnd -> CM ()
updateDirectItemsStatus ct conn msgIds newStatus = do updateDirectItemsStatus ct conn msgIds newStatus = do
@@ -6073,11 +6162,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
| otherwise -> updateGroupSndStatus db itemId groupMemberId newStatus $> True | otherwise -> updateGroupSndStatus db itemId groupMemberId newStatus $> True
_ -> pure False _ -> pure False
updateGroupItemStatus :: GroupInfo -> GroupMember -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> CM () updateGroupItemStatus :: GroupInfo -> GroupMember -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> Maybe Bool -> CM ()
updateGroupItemStatus gInfo@GroupInfo {groupId} GroupMember {groupMemberId} Connection {connId} msgId newMemStatus = updateGroupItemStatus gInfo@GroupInfo {groupId} GroupMember {groupMemberId} Connection {connId} msgId newMemStatus viaProxy_ =
withStore' (\db -> getGroupChatItemByAgentMsgId db user groupId connId msgId) >>= \case withStore' (\db -> getGroupChatItemByAgentMsgId db user groupId connId msgId) >>= \case
Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _ SSPComplete}}) -> pure () Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _ SSPComplete}}) -> pure ()
Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}}) -> do Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}}) -> do
forM_ viaProxy_ $ \viaProxy -> withStore' $ \db -> setGroupSndViaProxy db itemId groupMemberId viaProxy
memStatusChanged <- updateGroupMemSndStatus itemId groupMemberId newMemStatus memStatusChanged <- updateGroupMemSndStatus itemId groupMemberId newMemStatus
when memStatusChanged $ do when memStatusChanged $ do
memStatusCounts <- withStore' (`getGroupSndStatusCounts` itemId) memStatusCounts <- withStore' (`getGroupSndStatusCounts` itemId)
@@ -6676,7 +6766,7 @@ saveSndChatItem' :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage
saveSndChatItem' user cd msg@SndMessage {sharedMsgId} content ciFile quotedItem itemForwarded itemTimed live = do saveSndChatItem' user cd msg@SndMessage {sharedMsgId} content ciFile quotedItem itemForwarded itemTimed live = do
createdAt <- liftIO getCurrentTime createdAt <- liftIO getCurrentTime
ciId <- withStore' $ \db -> do ciId <- withStore' $ \db -> do
when (ciRequiresAttention content) $ updateChatTs db user cd createdAt when (ciRequiresAttention content || contactChatDeleted cd) $ updateChatTs db user cd createdAt
ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live createdAt ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live createdAt
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
pure ciId pure ciId
@@ -6690,7 +6780,7 @@ saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c
saveRcvChatItem' user cd msg@RcvMessage {forwardedByMember} sharedMsgId_ brokerTs content ciFile itemTimed live = do saveRcvChatItem' user cd msg@RcvMessage {forwardedByMember} sharedMsgId_ brokerTs content ciFile itemTimed live = do
createdAt <- liftIO getCurrentTime createdAt <- liftIO getCurrentTime
(ciId, quotedItem, itemForwarded) <- withStore' $ \db -> do (ciId, quotedItem, itemForwarded) <- withStore' $ \db -> do
when (ciRequiresAttention content) $ updateChatTs db user cd createdAt when (ciRequiresAttention content || contactChatDeleted cd) $ updateChatTs db user cd createdAt
r@(ciId, _, _) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live brokerTs createdAt r@(ciId, _, _) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live brokerTs createdAt
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
pure r pure r
@@ -6700,7 +6790,7 @@ mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ChatItemId
mkChatItem cd ciId content file quotedItem sharedMsgId itemForwarded itemTimed live itemTs forwardedByMember currentTs = mkChatItem cd ciId content file quotedItem sharedMsgId itemForwarded itemTimed live itemTs forwardedByMember currentTs =
let itemText = ciContentToText content let itemText = ciContentToText content
itemStatus = ciCreateStatus content itemStatus = ciCreateStatus content
meta = mkCIMeta ciId content itemText itemStatus sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs meta = mkCIMeta ciId content itemText itemStatus Nothing sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs
in ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file} in ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file}
deleteDirectCI :: MsgDirectionI d => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> CM ChatResponse deleteDirectCI :: MsgDirectionI d => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> CM ChatResponse
@@ -6956,7 +7046,7 @@ createInternalItemsForChats user itemTs_ dirsCIContents = do
where where
updateChat :: DB.Connection -> UTCTime -> ChatDirection c d -> [CIContent d] -> IO () updateChat :: DB.Connection -> UTCTime -> ChatDirection c d -> [CIContent d] -> IO ()
updateChat db createdAt cd contents updateChat db createdAt cd contents
| any ciRequiresAttention contents = updateChatTs db user cd createdAt | any ciRequiresAttention contents || contactChatDeleted cd = updateChatTs db user cd createdAt
| otherwise = pure () | otherwise = pure ()
createACIs :: DB.Connection -> UTCTime -> UTCTime -> ChatDirection c d -> [CIContent d] -> [IO AChatItem] createACIs :: DB.Connection -> UTCTime -> UTCTime -> ChatDirection c d -> [CIContent d] -> [IO AChatItem]
createACIs db itemTs createdAt cd = map $ \content -> do createACIs db itemTs createdAt cd = map $ \content -> do
@@ -7099,12 +7189,12 @@ chatCommandP =
"/_delete item " *> (APIDeleteChatItem <$> chatRefP <* A.space <*> A.decimal <* A.space <*> ciDeleteMode), "/_delete item " *> (APIDeleteChatItem <$> chatRefP <* A.space <*> A.decimal <* A.space <*> ciDeleteMode),
"/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <* A.space <*> A.decimal <* A.space <*> A.decimal), "/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <* A.space <*> A.decimal <* A.space <*> A.decimal),
"/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP), "/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP),
"/_forward " *> (APIForwardChatItem <$> chatRefP <* A.space <*> chatRefP <* A.space <*> A.decimal), "/_forward " *> (APIForwardChatItem <$> chatRefP <* A.space <*> chatRefP <* A.space <*> A.decimal <*> sendMessageTTLP),
"/_read user " *> (APIUserRead <$> A.decimal), "/_read user " *> (APIUserRead <$> A.decimal),
"/read user" $> UserRead, "/read user" $> UserRead,
"/_read chat " *> (APIChatRead <$> chatRefP <*> optional (A.space *> ((,) <$> ("from=" *> A.decimal) <* A.space <*> ("to=" *> A.decimal)))), "/_read chat " *> (APIChatRead <$> chatRefP <*> optional (A.space *> ((,) <$> ("from=" *> A.decimal) <* A.space <*> ("to=" *> A.decimal)))),
"/_unread chat " *> (APIChatUnread <$> chatRefP <* A.space <*> onOffP), "/_unread chat " *> (APIChatUnread <$> chatRefP <* A.space <*> onOffP),
"/_delete " *> (APIDeleteChat <$> chatRefP <*> (A.space *> "notify=" *> onOffP <|> pure True)), "/_delete " *> (APIDeleteChat <$> chatRefP <*> chatDeleteMode),
"/_clear chat " *> (APIClearChat <$> chatRefP), "/_clear chat " *> (APIClearChat <$> chatRefP),
"/_accept" *> (APIAcceptContact <$> incognitoOnOffP <* A.space <*> A.decimal), "/_accept" *> (APIAcceptContact <$> incognitoOnOffP <* A.space <*> A.decimal),
"/_reject " *> (APIRejectContact <$> A.decimal), "/_reject " *> (APIRejectContact <$> A.decimal),
@@ -7210,7 +7300,7 @@ chatCommandP =
("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayName <* A.space <* char_ '@' <*> displayName), ("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayName <* A.space <* char_ '@' <*> displayName),
("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayName), ("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayName),
("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName), ("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName),
("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayName), ("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayName <*> chatDeleteMode),
"/clear *" $> ClearNoteFolder, "/clear *" $> ClearNoteFolder,
"/clear #" *> (ClearGroup <$> displayName), "/clear #" *> (ClearGroup <$> displayName),
"/clear " *> char_ '@' *> (ClearContact <$> displayName), "/clear " *> char_ '@' *> (ClearContact <$> displayName),
@@ -7271,8 +7361,8 @@ chatCommandP =
("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal), ("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal),
("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal), ("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal),
("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath), ("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath),
("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)),
"/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> optional (" encrypt=" *> onOffP)), "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP)),
("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal), ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal),
("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal),
"/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal), "/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal),
@@ -7341,6 +7431,7 @@ chatCommandP =
"/get subs details" $> GetAgentSubsDetails, "/get subs details" $> GetAgentSubsDetails,
"/get workers" $> GetAgentWorkers, "/get workers" $> GetAgentWorkers,
"/get workers details" $> GetAgentWorkersDetails, "/get workers details" $> GetAgentWorkersDetails,
"/get msgs" $> GetAgentMsgCounts,
"//" *> (CustomChatCommand <$> A.takeByteString) "//" *> (CustomChatCommand <$> A.takeByteString)
] ]
where where
@@ -7361,6 +7452,15 @@ chatCommandP =
mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString
msgContentP = "text " *> mcTextP <|> "json " *> jsonP msgContentP = "text " *> mcTextP <|> "json " *> jsonP
ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal
chatDeleteMode =
A.choice
[ " full" *> (CDMFull <$> notifyP),
" entity" *> (CDMEntity <$> notifyP),
" messages" $> CDMMessages,
CDMFull <$> notifyP -- backwards compatible
]
where
notifyP = " notify=" *> onOffP <|> pure True
displayName = safeDecodeUtf8 <$> (quoted "'" <|> takeNameTill isSpace) displayName = safeDecodeUtf8 <$> (quoted "'" <|> takeNameTill isSpace)
where where
takeNameTill p = takeNameTill p =
+24 -5
View File
@@ -9,6 +9,7 @@ import Control.Applicative ((<|>))
import Data.Aeson (FromJSON (..), (.:?)) import Data.Aeson (FromJSON (..), (.:?))
import qualified Data.Aeson as J import qualified Data.Aeson as J
import qualified Data.Aeson.TH as JQ import qualified Data.Aeson.TH as JQ
import Data.Map.Strict (Map)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Text (Text) import Data.Text (Text)
import Simplex.Chat.Types.UITheme import Simplex.Chat.Types.UITheme
@@ -28,6 +29,7 @@ data AppSettings = AppSettings
{ appPlatform :: Maybe AppPlatform, { appPlatform :: Maybe AppPlatform,
networkConfig :: Maybe NetworkConfig, networkConfig :: Maybe NetworkConfig,
privacyEncryptLocalFiles :: Maybe Bool, privacyEncryptLocalFiles :: Maybe Bool,
privacyAskToApproveRelays :: Maybe Bool,
privacyAcceptImages :: Maybe Bool, privacyAcceptImages :: Maybe Bool,
privacyLinkPreviews :: Maybe Bool, privacyLinkPreviews :: Maybe Bool,
privacyShowChatPreviews :: Maybe Bool, privacyShowChatPreviews :: Maybe Bool,
@@ -48,7 +50,9 @@ data AppSettings = AppSettings
uiProfileImageCornerRadius :: Maybe Double, uiProfileImageCornerRadius :: Maybe Double,
uiColorScheme :: Maybe UIColorScheme, uiColorScheme :: Maybe UIColorScheme,
uiDarkColorScheme :: Maybe DarkColorScheme, uiDarkColorScheme :: Maybe DarkColorScheme,
uiThemes :: Maybe UIThemes uiCurrentThemeIds :: Maybe (Map ThemeColorScheme Text),
uiThemes :: Maybe [UITheme],
oneHandUI :: Maybe Bool
} }
deriving (Show) deriving (Show)
@@ -58,6 +62,7 @@ defaultAppSettings =
{ appPlatform = Nothing, { appPlatform = Nothing,
networkConfig = Just defaultNetworkConfig, networkConfig = Just defaultNetworkConfig,
privacyEncryptLocalFiles = Just True, privacyEncryptLocalFiles = Just True,
privacyAskToApproveRelays = Just True,
privacyAcceptImages = Just True, privacyAcceptImages = Just True,
privacyLinkPreviews = Just True, privacyLinkPreviews = Just True,
privacyShowChatPreviews = Just True, privacyShowChatPreviews = Just True,
@@ -78,7 +83,9 @@ defaultAppSettings =
uiProfileImageCornerRadius = Just 22.5, uiProfileImageCornerRadius = Just 22.5,
uiColorScheme = Just UCSSystem, uiColorScheme = Just UCSSystem,
uiDarkColorScheme = Just DCSSimplex, uiDarkColorScheme = Just DCSSimplex,
uiThemes = Nothing uiCurrentThemeIds = Nothing,
uiThemes = Nothing,
oneHandUI = Just True
} }
defaultParseAppSettings :: AppSettings defaultParseAppSettings :: AppSettings
@@ -87,6 +94,7 @@ defaultParseAppSettings =
{ appPlatform = Nothing, { appPlatform = Nothing,
networkConfig = Nothing, networkConfig = Nothing,
privacyEncryptLocalFiles = Nothing, privacyEncryptLocalFiles = Nothing,
privacyAskToApproveRelays = Nothing,
privacyAcceptImages = Nothing, privacyAcceptImages = Nothing,
privacyLinkPreviews = Nothing, privacyLinkPreviews = Nothing,
privacyShowChatPreviews = Nothing, privacyShowChatPreviews = Nothing,
@@ -107,7 +115,9 @@ defaultParseAppSettings =
uiProfileImageCornerRadius = Nothing, uiProfileImageCornerRadius = Nothing,
uiColorScheme = Nothing, uiColorScheme = Nothing,
uiDarkColorScheme = Nothing, uiDarkColorScheme = Nothing,
uiThemes = Nothing uiCurrentThemeIds = Nothing,
uiThemes = Nothing,
oneHandUI = Nothing
} }
combineAppSettings :: AppSettings -> AppSettings -> AppSettings combineAppSettings :: AppSettings -> AppSettings -> AppSettings
@@ -116,6 +126,7 @@ combineAppSettings platformDefaults storedSettings =
{ appPlatform = p appPlatform, { appPlatform = p appPlatform,
networkConfig = p networkConfig, networkConfig = p networkConfig,
privacyEncryptLocalFiles = p privacyEncryptLocalFiles, privacyEncryptLocalFiles = p privacyEncryptLocalFiles,
privacyAskToApproveRelays = p privacyAskToApproveRelays,
privacyAcceptImages = p privacyAcceptImages, privacyAcceptImages = p privacyAcceptImages,
privacyLinkPreviews = p privacyLinkPreviews, privacyLinkPreviews = p privacyLinkPreviews,
privacyShowChatPreviews = p privacyShowChatPreviews, privacyShowChatPreviews = p privacyShowChatPreviews,
@@ -136,7 +147,9 @@ combineAppSettings platformDefaults storedSettings =
uiProfileImageCornerRadius = p uiProfileImageCornerRadius, uiProfileImageCornerRadius = p uiProfileImageCornerRadius,
uiColorScheme = p uiColorScheme, uiColorScheme = p uiColorScheme,
uiDarkColorScheme = p uiDarkColorScheme, uiDarkColorScheme = p uiDarkColorScheme,
uiThemes = p uiThemes uiCurrentThemeIds = p uiCurrentThemeIds,
uiThemes = p uiThemes,
oneHandUI = p oneHandUI
} }
where where
p :: (AppSettings -> Maybe a) -> Maybe a p :: (AppSettings -> Maybe a) -> Maybe a
@@ -157,6 +170,7 @@ instance FromJSON AppSettings where
appPlatform <- p "appPlatform" appPlatform <- p "appPlatform"
networkConfig <- p "networkConfig" networkConfig <- p "networkConfig"
privacyEncryptLocalFiles <- p "privacyEncryptLocalFiles" privacyEncryptLocalFiles <- p "privacyEncryptLocalFiles"
privacyAskToApproveRelays <- p "privacyAskToApproveRelays"
privacyAcceptImages <- p "privacyAcceptImages" privacyAcceptImages <- p "privacyAcceptImages"
privacyLinkPreviews <- p "privacyLinkPreviews" privacyLinkPreviews <- p "privacyLinkPreviews"
privacyShowChatPreviews <- p "privacyShowChatPreviews" privacyShowChatPreviews <- p "privacyShowChatPreviews"
@@ -177,12 +191,15 @@ instance FromJSON AppSettings where
uiProfileImageCornerRadius <- p "uiProfileImageCornerRadius" uiProfileImageCornerRadius <- p "uiProfileImageCornerRadius"
uiColorScheme <- p "uiColorScheme" uiColorScheme <- p "uiColorScheme"
uiDarkColorScheme <- p "uiDarkColorScheme" uiDarkColorScheme <- p "uiDarkColorScheme"
uiCurrentThemeIds <- p "uiCurrentThemeIds"
uiThemes <- p "uiThemes" uiThemes <- p "uiThemes"
oneHandUI <- p "oneHandUI"
pure pure
AppSettings AppSettings
{ appPlatform, { appPlatform,
networkConfig, networkConfig,
privacyEncryptLocalFiles, privacyEncryptLocalFiles,
privacyAskToApproveRelays,
privacyAcceptImages, privacyAcceptImages,
privacyLinkPreviews, privacyLinkPreviews,
privacyShowChatPreviews, privacyShowChatPreviews,
@@ -203,7 +220,9 @@ instance FromJSON AppSettings where
uiProfileImageCornerRadius, uiProfileImageCornerRadius,
uiColorScheme, uiColorScheme,
uiDarkColorScheme, uiDarkColorScheme,
uiThemes uiCurrentThemeIds,
uiThemes,
oneHandUI
} }
where where
p key = v .:? key <|> pure Nothing p key = v .:? key <|> pure Nothing
+16 -6
View File
@@ -81,7 +81,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption)
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON)
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServerWithAuth, userProtocol) import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, XFTPServerWithAuth, userProtocol)
import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.TMap (TMap)
import Simplex.Messaging.Transport (TLS, simplexMQVersion) import Simplex.Messaging.Transport (TLS, simplexMQVersion)
import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Transport.Client (TransportHost)
@@ -291,12 +291,12 @@ data ChatCommand
| APIDeleteChatItem ChatRef ChatItemId CIDeleteMode | APIDeleteChatItem ChatRef ChatItemId CIDeleteMode
| APIDeleteMemberChatItem GroupId GroupMemberId ChatItemId | APIDeleteMemberChatItem GroupId GroupMemberId ChatItemId
| APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction} | APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction}
| APIForwardChatItem {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemId :: ChatItemId} | APIForwardChatItem {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemId :: ChatItemId, ttl :: Maybe Int}
| APIUserRead UserId | APIUserRead UserId
| UserRead | UserRead
| APIChatRead ChatRef (Maybe (ChatItemId, ChatItemId)) | APIChatRead ChatRef (Maybe (ChatItemId, ChatItemId))
| APIChatUnread ChatRef Bool | APIChatUnread ChatRef Bool
| APIDeleteChat ChatRef Bool -- `notify` flag is only applied to direct chats | APIDeleteChat ChatRef ChatDeleteMode -- currently delete mode settings are only applied to direct chats
| APIClearChat ChatRef | APIClearChat ChatRef
| APIAcceptContact IncognitoEnabled Int64 | APIAcceptContact IncognitoEnabled Int64
| APIRejectContact Int64 | APIRejectContact Int64
@@ -395,7 +395,7 @@ data ChatCommand
| Connect IncognitoEnabled (Maybe AConnectionRequestUri) | Connect IncognitoEnabled (Maybe AConnectionRequestUri)
| APIConnectContactViaAddress UserId IncognitoEnabled ContactId | APIConnectContactViaAddress UserId IncognitoEnabled ContactId
| ConnectSimplex IncognitoEnabled -- UserId (not used in UI) | ConnectSimplex IncognitoEnabled -- UserId (not used in UI)
| DeleteContact ContactName | DeleteContact ContactName ChatDeleteMode
| ClearContact ContactName | ClearContact ContactName
| APIListContacts UserId | APIListContacts UserId
| ListContacts | ListContacts
@@ -458,8 +458,8 @@ data ChatCommand
| ForwardFile ChatName FileTransferId | ForwardFile ChatName FileTransferId
| ForwardImage ChatName FileTransferId | ForwardImage ChatName FileTransferId
| SendFileDescription ChatName FilePath | SendFileDescription ChatName FilePath
| ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} | ReceiveFile {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
| SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Maybe Bool} | SetFileToReceive {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool}
| CancelFile FileTransferId | CancelFile FileTransferId
| FileStatus FileTransferId | FileStatus FileTransferId
| ShowProfile -- UserId (not used in UI) | ShowProfile -- UserId (not used in UI)
@@ -501,6 +501,7 @@ data ChatCommand
| GetAgentSubsDetails | GetAgentSubsDetails
| GetAgentWorkers | GetAgentWorkers
| GetAgentWorkersDetails | GetAgentWorkersDetails
| GetAgentMsgCounts
| -- The parser will return this command for strings that start from "//". | -- The parser will return this command for strings that start from "//".
-- This command should be processed in preCmdHook -- This command should be processed in preCmdHook
CustomChatCommand ByteString CustomChatCommand ByteString
@@ -746,6 +747,8 @@ data ChatResponse
| CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary} | CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary}
| CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]} | CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]}
| CRAgentSubsDetails {agentSubs :: SubscriptionsInfo} | CRAgentSubsDetails {agentSubs :: SubscriptionsInfo}
| CRAgentMsgCounts {msgCounts :: [(Text, (Int, Int))]}
| CRContactDisabled {user :: User, contact :: Contact}
| CRConnectionDisabled {connectionEntity :: ConnectionEntity} | CRConnectionDisabled {connectionEntity :: ConnectionEntity}
| CRAgentRcvQueueDeleted {agentConnId :: AgentConnId, server :: SMPServer, agentQueueId :: AgentQueueId, agentError_ :: Maybe AgentErrorType} | CRAgentRcvQueueDeleted {agentConnId :: AgentConnId, server :: SMPServer, agentQueueId :: AgentQueueId, agentError_ :: Maybe AgentErrorType}
| CRAgentConnDeleted {agentConnId :: AgentConnId} | CRAgentConnDeleted {agentConnId :: AgentConnId}
@@ -824,6 +827,12 @@ data ChatListQuery
clqNoFilters :: ChatListQuery clqNoFilters :: ChatListQuery
clqNoFilters = CLQFilters {favorite = False, unread = False} clqNoFilters = CLQFilters {favorite = False, unread = False}
data ChatDeleteMode
= CDMFull {notify :: Bool} -- delete both contact and conversation
| CDMEntity {notify :: Bool} -- delete contact (connection), keep conversation
| CDMMessages -- delete conversation, keep contact - can be re-opened from Contacts view
deriving (Show)
data ConnectionPlan data ConnectionPlan
= CPInvitationLink {invitationLinkPlan :: InvitationLinkPlan} = CPInvitationLink {invitationLinkPlan :: InvitationLinkPlan}
| CPContactAddress {contactAddressPlan :: ContactAddressPlan} | CPContactAddress {contactAddressPlan :: ContactAddressPlan}
@@ -1123,6 +1132,7 @@ data ChatErrorType
| CEFileImageType {filePath :: FilePath} | CEFileImageType {filePath :: FilePath}
| CEFileImageSize {filePath :: FilePath} | CEFileImageSize {filePath :: FilePath}
| CEFileNotReceived {fileId :: FileTransferId} | CEFileNotReceived {fileId :: FileTransferId}
| CEFileNotApproved {fileId :: FileTransferId, unknownServers :: [XFTPServer]}
| CEXFTPRcvFile {fileId :: FileTransferId, agentRcvFileId :: AgentRcvFileId, agentError :: AgentErrorType} | CEXFTPRcvFile {fileId :: FileTransferId, agentRcvFileId :: AgentRcvFileId, agentError :: AgentErrorType}
| CEXFTPSndFile {fileId :: FileTransferId, agentSndFileId :: AgentSndFileId, agentError :: AgentErrorType} | CEXFTPSndFile {fileId :: FileTransferId, agentSndFileId :: AgentSndFileId, agentError :: AgentErrorType}
| CEFallbackToSMPProhibited {fileId :: FileTransferId} | CEFallbackToSMPProhibited {fileId :: FileTransferId}
+91 -13
View File
@@ -29,6 +29,7 @@ import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char (isSpace) import Data.Char (isSpace)
import Data.Int (Int64) import Data.Int (Int64)
import Data.Kind (Constraint) import Data.Kind (Constraint)
import Data.List.NonEmpty (NonEmpty)
import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Maybe (fromMaybe, isJust, isNothing)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
@@ -45,6 +46,7 @@ import Simplex.Chat.Messages.CIContent
import Simplex.Chat.Protocol import Simplex.Chat.Protocol
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Util (textParseJSON)
import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptStatus (..)) import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptStatus (..))
import Simplex.Messaging.Crypto.File (CryptoFile (..)) import Simplex.Messaging.Crypto.File (CryptoFile (..))
import qualified Simplex.Messaging.Crypto.File as CF import qualified Simplex.Messaging.Crypto.File as CF
@@ -277,6 +279,12 @@ toChatInfo = \case
CDLocalSnd l -> LocalChat l CDLocalSnd l -> LocalChat l
CDLocalRcv l -> LocalChat l CDLocalRcv l -> LocalChat l
contactChatDeleted :: ChatDirection c d -> Bool
contactChatDeleted = \case
CDDirectSnd Contact {chatDeleted} -> chatDeleted
CDDirectRcv Contact {chatDeleted} -> chatDeleted
_ -> False
data NewChatItem d = NewChatItem data NewChatItem d = NewChatItem
{ createdByMsgId :: Maybe MessageId, { createdByMsgId :: Maybe MessageId,
itemSent :: SMsgDirection d, itemSent :: SMsgDirection d,
@@ -338,6 +346,7 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta
itemTs :: ChatItemTs, itemTs :: ChatItemTs,
itemText :: Text, itemText :: Text,
itemStatus :: CIStatus d, itemStatus :: CIStatus d,
sentViaProxy :: Maybe Bool,
itemSharedMsgId :: Maybe SharedMsgId, itemSharedMsgId :: Maybe SharedMsgId,
itemForwarded :: Maybe CIForwardedFrom, itemForwarded :: Maybe CIForwardedFrom,
itemDeleted :: Maybe (CIDeleted c), itemDeleted :: Maybe (CIDeleted c),
@@ -352,8 +361,8 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta
} }
deriving (Show) deriving (Show)
mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> UTCTime -> CIMeta c d mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe Bool -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> UTCTime -> CIMeta c d
mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt = mkCIMeta itemId itemContent itemText itemStatus sentViaProxy itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt =
let deletable = case itemContent of let deletable = case itemContent of
CISndMsgContent _ -> CISndMsgContent _ ->
case chatTypeI @c of case chatTypeI @c of
@@ -361,7 +370,7 @@ mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemForwarded it
_ -> diffUTCTime currentTs itemTs < nominalDay && isNothing itemDeleted _ -> diffUTCTime currentTs itemTs < nominalDay && isNothing itemDeleted
_ -> False _ -> False
editable = deletable && isNothing itemForwarded editable = deletable && isNothing itemForwarded
in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, deletable, editable, forwardedByMember, createdAt, updatedAt} in CIMeta {itemId, itemTs, itemText, itemStatus, sentViaProxy, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, deletable, editable, forwardedByMember, createdAt, updatedAt}
dummyMeta :: ChatItemId -> UTCTime -> Text -> CIMeta c 'MDSnd dummyMeta :: ChatItemId -> UTCTime -> Text -> CIMeta c 'MDSnd
dummyMeta itemId ts itemText = dummyMeta itemId ts itemText =
@@ -370,6 +379,7 @@ dummyMeta itemId ts itemText =
itemTs = ts, itemTs = ts,
itemText, itemText,
itemStatus = CISSndNew, itemStatus = CISSndNew,
sentViaProxy = Nothing,
itemSharedMsgId = Nothing, itemSharedMsgId = Nothing,
itemForwarded = Nothing, itemForwarded = Nothing,
itemDeleted = Nothing, itemDeleted = Nothing,
@@ -529,6 +539,7 @@ data CIFileStatus (d :: MsgDirection) where
CIFSRcvInvitation :: CIFileStatus 'MDRcv CIFSRcvInvitation :: CIFileStatus 'MDRcv
CIFSRcvAccepted :: CIFileStatus 'MDRcv CIFSRcvAccepted :: CIFileStatus 'MDRcv
CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv
CIFSRcvAborted :: CIFileStatus 'MDRcv
CIFSRcvComplete :: CIFileStatus 'MDRcv CIFSRcvComplete :: CIFileStatus 'MDRcv
CIFSRcvCancelled :: CIFileStatus 'MDRcv CIFSRcvCancelled :: CIFileStatus 'MDRcv
CIFSRcvError :: CIFileStatus 'MDRcv CIFSRcvError :: CIFileStatus 'MDRcv
@@ -548,6 +559,7 @@ ciFileEnded = \case
CIFSRcvInvitation -> False CIFSRcvInvitation -> False
CIFSRcvAccepted -> False CIFSRcvAccepted -> False
CIFSRcvTransfer {} -> False CIFSRcvTransfer {} -> False
CIFSRcvAborted -> True
CIFSRcvCancelled -> True CIFSRcvCancelled -> True
CIFSRcvComplete -> True CIFSRcvComplete -> True
CIFSRcvError -> True CIFSRcvError -> True
@@ -563,6 +575,7 @@ ciFileLoaded = \case
CIFSRcvInvitation -> False CIFSRcvInvitation -> False
CIFSRcvAccepted -> False CIFSRcvAccepted -> False
CIFSRcvTransfer {} -> False CIFSRcvTransfer {} -> False
CIFSRcvAborted -> False
CIFSRcvCancelled -> False CIFSRcvCancelled -> False
CIFSRcvComplete -> True CIFSRcvComplete -> True
CIFSRcvError -> False CIFSRcvError -> False
@@ -582,6 +595,7 @@ instance MsgDirectionI d => StrEncoding (CIFileStatus d) where
CIFSRcvInvitation -> "rcv_invitation" CIFSRcvInvitation -> "rcv_invitation"
CIFSRcvAccepted -> "rcv_accepted" CIFSRcvAccepted -> "rcv_accepted"
CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total) CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total)
CIFSRcvAborted -> "rcv_aborted"
CIFSRcvComplete -> "rcv_complete" CIFSRcvComplete -> "rcv_complete"
CIFSRcvCancelled -> "rcv_cancelled" CIFSRcvCancelled -> "rcv_cancelled"
CIFSRcvError -> "rcv_error" CIFSRcvError -> "rcv_error"
@@ -604,6 +618,7 @@ instance StrEncoding ACIFileStatus where
"rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation "rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation
"rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted "rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted
"rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer "rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer
"rcv_aborted" -> pure $ AFS SMDRcv CIFSRcvAborted
"rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete "rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete
"rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled "rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled
"rcv_error" -> pure $ AFS SMDRcv CIFSRcvError "rcv_error" -> pure $ AFS SMDRcv CIFSRcvError
@@ -621,6 +636,7 @@ data JSONCIFileStatus
| JCIFSRcvInvitation | JCIFSRcvInvitation
| JCIFSRcvAccepted | JCIFSRcvAccepted
| JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64} | JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64}
| JCIFSRcvAborted
| JCIFSRcvComplete | JCIFSRcvComplete
| JCIFSRcvCancelled | JCIFSRcvCancelled
| JCIFSRcvError | JCIFSRcvError
@@ -636,6 +652,7 @@ jsonCIFileStatus = \case
CIFSRcvInvitation -> JCIFSRcvInvitation CIFSRcvInvitation -> JCIFSRcvInvitation
CIFSRcvAccepted -> JCIFSRcvAccepted CIFSRcvAccepted -> JCIFSRcvAccepted
CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total
CIFSRcvAborted -> JCIFSRcvAborted
CIFSRcvComplete -> JCIFSRcvComplete CIFSRcvComplete -> JCIFSRcvComplete
CIFSRcvCancelled -> JCIFSRcvCancelled CIFSRcvCancelled -> JCIFSRcvCancelled
CIFSRcvError -> JCIFSRcvError CIFSRcvError -> JCIFSRcvError
@@ -651,6 +668,7 @@ aciFileStatusJSON = \case
JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation
JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted
JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total
JCIFSRcvAborted -> AFS SMDRcv CIFSRcvAborted
JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete
JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled
JCIFSRcvError -> AFS SMDRcv CIFSRcvError JCIFSRcvError -> AFS SMDRcv CIFSRcvError
@@ -676,8 +694,9 @@ data CIStatus (d :: MsgDirection) where
CISSndNew :: CIStatus 'MDSnd CISSndNew :: CIStatus 'MDSnd
CISSndSent :: SndCIStatusProgress -> CIStatus 'MDSnd CISSndSent :: SndCIStatusProgress -> CIStatus 'MDSnd
CISSndRcvd :: MsgReceiptStatus -> SndCIStatusProgress -> CIStatus 'MDSnd CISSndRcvd :: MsgReceiptStatus -> SndCIStatusProgress -> CIStatus 'MDSnd
CISSndErrorAuth :: CIStatus 'MDSnd CISSndErrorAuth :: CIStatus 'MDSnd -- deprecated
CISSndError :: String -> CIStatus 'MDSnd CISSndError :: SndError -> CIStatus 'MDSnd
CISSndWarning :: SndError -> CIStatus 'MDSnd
CISRcvNew :: CIStatus 'MDRcv CISRcvNew :: CIStatus 'MDRcv
CISRcvRead :: CIStatus 'MDRcv CISRcvRead :: CIStatus 'MDRcv
CISInvalid :: Text -> CIStatus 'MDSnd CISInvalid :: Text -> CIStatus 'MDSnd
@@ -696,7 +715,8 @@ instance MsgDirectionI d => StrEncoding (CIStatus d) where
CISSndSent sndProgress -> "snd_sent " <> strEncode sndProgress CISSndSent sndProgress -> "snd_sent " <> strEncode sndProgress
CISSndRcvd msgRcptStatus sndProgress -> "snd_rcvd " <> strEncode msgRcptStatus <> " " <> strEncode sndProgress CISSndRcvd msgRcptStatus sndProgress -> "snd_rcvd " <> strEncode msgRcptStatus <> " " <> strEncode sndProgress
CISSndErrorAuth -> "snd_error_auth" CISSndErrorAuth -> "snd_error_auth"
CISSndError e -> "snd_error " <> encodeUtf8 (T.pack e) CISSndError sndErr -> "snd_error " <> strEncode sndErr
CISSndWarning sndErr -> "snd_warning " <> strEncode sndErr
CISRcvNew -> "rcv_new" CISRcvNew -> "rcv_new"
CISRcvRead -> "rcv_read" CISRcvRead -> "rcv_read"
CISInvalid {} -> "invalid" CISInvalid {} -> "invalid"
@@ -714,17 +734,68 @@ instance StrEncoding ACIStatus where
"snd_sent" -> ACIStatus SMDSnd . CISSndSent <$> ((A.space *> strP) <|> pure SSPComplete) "snd_sent" -> ACIStatus SMDSnd . CISSndSent <$> ((A.space *> strP) <|> pure SSPComplete)
"snd_rcvd" -> ACIStatus SMDSnd <$> (CISSndRcvd <$> (A.space *> strP) <*> ((A.space *> strP) <|> pure SSPComplete)) "snd_rcvd" -> ACIStatus SMDSnd <$> (CISSndRcvd <$> (A.space *> strP) <*> ((A.space *> strP) <|> pure SSPComplete))
"snd_error_auth" -> pure $ ACIStatus SMDSnd CISSndErrorAuth "snd_error_auth" -> pure $ ACIStatus SMDSnd CISSndErrorAuth
"snd_error" -> ACIStatus SMDSnd . CISSndError . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeByteString) "snd_error" -> ACIStatus SMDSnd . CISSndError <$> (A.space *> strP)
"snd_warning" -> ACIStatus SMDSnd . CISSndWarning <$> (A.space *> strP)
"rcv_new" -> pure $ ACIStatus SMDRcv CISRcvNew "rcv_new" -> pure $ ACIStatus SMDRcv CISRcvNew
"rcv_read" -> pure $ ACIStatus SMDRcv CISRcvRead "rcv_read" -> pure $ ACIStatus SMDRcv CISRcvRead
_ -> fail "bad status" _ -> fail "bad status"
-- see serverHostError in agent
data SndError
= SndErrAuth
| SndErrQuota
| SndErrExpired -- TIMEOUT/NETWORK errors
| SndErrRelay {srvError :: SrvError} -- BROKER errors (other than TIMEOUT/NETWORK)
| SndErrProxy {proxyServer :: String, srvError :: SrvError} -- SMP PROXY errors
| SndErrProxyRelay {proxyServer :: String, srvError :: SrvError} -- PROXY BROKER errors
| SndErrOther {sndError :: Text} -- other errors
deriving (Eq, Show)
data SrvError
= SrvErrHost
| SrvErrVersion
| SrvErrOther {srvError :: Text}
deriving (Eq, Show)
instance StrEncoding SndError where
strEncode = \case
SndErrAuth -> "auth"
SndErrQuota -> "quota"
SndErrExpired -> "expired"
SndErrRelay srvErr -> "relay " <> strEncode srvErr
SndErrProxy proxy srvErr -> "proxy " <> encodeUtf8 (T.pack proxy) <> " " <> strEncode srvErr
SndErrProxyRelay proxy srvErr -> "proxy_relay " <> encodeUtf8 (T.pack proxy) <> " " <> strEncode srvErr
SndErrOther e -> "other " <> encodeUtf8 e
strP =
A.takeWhile1 (/= ' ') >>= \case
"auth" -> pure SndErrAuth
"quota" -> pure SndErrQuota
"expired" -> pure SndErrExpired
"relay" -> SndErrRelay <$> (A.space *> strP)
"proxy" -> SndErrProxy . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeWhile1 (/= ' ') <* A.space) <*> strP
"proxy_relay" -> SndErrProxyRelay . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeWhile1 (/= ' ') <* A.space) <*> strP
"other" -> SndErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
s -> SndErrOther . safeDecodeUtf8 . (s <>) <$> A.takeByteString -- for backward compatibility with `CISSndError String`
instance StrEncoding SrvError where
strEncode = \case
SrvErrHost -> "host"
SrvErrVersion -> "version"
SrvErrOther e -> "other " <> encodeUtf8 e
strP =
A.takeWhile1 (/= ' ') >>= \case
"host" -> pure SrvErrHost
"version" -> pure SrvErrVersion
"other" -> SrvErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
_ -> fail "bad SrvError"
data JSONCIStatus data JSONCIStatus
= JCISSndNew = JCISSndNew
| JCISSndSent {sndProgress :: SndCIStatusProgress} | JCISSndSent {sndProgress :: SndCIStatusProgress}
| JCISSndRcvd {msgRcptStatus :: MsgReceiptStatus, sndProgress :: SndCIStatusProgress} | JCISSndRcvd {msgRcptStatus :: MsgReceiptStatus, sndProgress :: SndCIStatusProgress}
| JCISSndErrorAuth | JCISSndErrorAuth -- deprecated
| JCISSndError {agentError :: String} | JCISSndError {agentError :: SndError}
| JCISSndWarning {agentError :: SndError}
| JCISRcvNew | JCISRcvNew
| JCISRcvRead | JCISRcvRead
| JCISInvalid {text :: Text} | JCISInvalid {text :: Text}
@@ -736,7 +807,8 @@ jsonCIStatus = \case
CISSndSent sndProgress -> JCISSndSent sndProgress CISSndSent sndProgress -> JCISSndSent sndProgress
CISSndRcvd msgRcptStatus sndProgress -> JCISSndRcvd msgRcptStatus sndProgress CISSndRcvd msgRcptStatus sndProgress -> JCISSndRcvd msgRcptStatus sndProgress
CISSndErrorAuth -> JCISSndErrorAuth CISSndErrorAuth -> JCISSndErrorAuth
CISSndError e -> JCISSndError e CISSndError sndErr -> JCISSndError sndErr
CISSndWarning sndErr -> JCISSndWarning sndErr
CISRcvNew -> JCISRcvNew CISRcvNew -> JCISRcvNew
CISRcvRead -> JCISRcvRead CISRcvRead -> JCISRcvRead
CISInvalid text -> JCISInvalid text CISInvalid text -> JCISInvalid text
@@ -747,7 +819,8 @@ jsonACIStatus = \case
JCISSndSent sndProgress -> ACIStatus SMDSnd $ CISSndSent sndProgress JCISSndSent sndProgress -> ACIStatus SMDSnd $ CISSndSent sndProgress
JCISSndRcvd msgRcptStatus sndProgress -> ACIStatus SMDSnd $ CISSndRcvd msgRcptStatus sndProgress JCISSndRcvd msgRcptStatus sndProgress -> ACIStatus SMDSnd $ CISSndRcvd msgRcptStatus sndProgress
JCISSndErrorAuth -> ACIStatus SMDSnd CISSndErrorAuth JCISSndErrorAuth -> ACIStatus SMDSnd CISSndErrorAuth
JCISSndError e -> ACIStatus SMDSnd $ CISSndError e JCISSndError sndErr -> ACIStatus SMDSnd $ CISSndError sndErr
JCISSndWarning sndErr -> ACIStatus SMDSnd $ CISSndWarning sndErr
JCISRcvNew -> ACIStatus SMDRcv CISRcvNew JCISRcvNew -> ACIStatus SMDRcv CISRcvNew
JCISRcvRead -> ACIStatus SMDRcv CISRcvRead JCISRcvRead -> ACIStatus SMDRcv CISRcvRead
JCISInvalid text -> ACIStatus SMDSnd $ CISInvalid text JCISInvalid text -> ACIStatus SMDSnd $ CISInvalid text
@@ -1034,7 +1107,7 @@ instance TextEncoding CIForwardedFromTag where
data ChatItemInfo = ChatItemInfo data ChatItemInfo = ChatItemInfo
{ itemVersions :: [ChatItemVersion], { itemVersions :: [ChatItemVersion],
memberDeliveryStatuses :: Maybe [MemberDeliveryStatus], memberDeliveryStatuses :: Maybe (NonEmpty MemberDeliveryStatus),
forwardedFromChatItem :: Maybe AChatItem forwardedFromChatItem :: Maybe AChatItem
} }
deriving (Show) deriving (Show)
@@ -1063,7 +1136,8 @@ mkItemVersion ChatItem {content, meta} = version <$> ciMsgContent content
data MemberDeliveryStatus = MemberDeliveryStatus data MemberDeliveryStatus = MemberDeliveryStatus
{ groupMemberId :: GroupMemberId, { groupMemberId :: GroupMemberId,
memberDeliveryStatus :: CIStatus 'MDSnd memberDeliveryStatus :: CIStatus 'MDSnd,
sentViaProxy :: Maybe Bool
} }
deriving (Eq, Show) deriving (Eq, Show)
@@ -1101,6 +1175,10 @@ $(JQ.deriveJSON defaultJSON ''CITimed)
$(JQ.deriveJSON (enumJSON $ dropPrefix "SSP") ''SndCIStatusProgress) $(JQ.deriveJSON (enumJSON $ dropPrefix "SSP") ''SndCIStatusProgress)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SrvErr") ''SrvError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SndErr") ''SndError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCIS") ''JSONCIStatus) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCIS") ''JSONCIStatus)
instance MsgDirectionI d => FromJSON (CIStatus d) where instance MsgDirectionI d => FromJSON (CIStatus d) where
@@ -0,0 +1,18 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20240501_chat_deleted where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240501_chat_deleted :: Query
m20240501_chat_deleted =
[sql|
ALTER TABLE contacts ADD COLUMN chat_deleted INTEGER NOT NULL DEFAULT 0;
|]
down_m20240501_chat_deleted :: Query
down_m20240501_chat_deleted =
[sql|
ALTER TABLE contacts DROP COLUMN chat_deleted;
|]
@@ -0,0 +1,20 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20240510_chat_items_via_proxy where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240510_chat_items_via_proxy :: Query
m20240510_chat_items_via_proxy =
[sql|
ALTER TABLE chat_items ADD COLUMN via_proxy INTEGER;
ALTER TABLE group_snd_item_statuses ADD COLUMN via_proxy INTEGER;
|]
down_m20240510_chat_items_via_proxy :: Query
down_m20240510_chat_items_via_proxy =
[sql|
ALTER TABLE chat_items DROP COLUMN via_proxy;
ALTER TABLE group_snd_item_statuses DROP COLUMN via_proxy;
|]
@@ -0,0 +1,18 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240515_rcv_files_user_approved_relays :: Query
m20240515_rcv_files_user_approved_relays =
[sql|
ALTER TABLE rcv_files ADD COLUMN user_approved_relays INTEGER NOT NULL DEFAULT 0;
|]
down_m20240515_rcv_files_user_approved_relays :: Query
down_m20240515_rcv_files_user_approved_relays =
[sql|
ALTER TABLE rcv_files DROP COLUMN user_approved_relays;
|]
+7 -2
View File
@@ -76,6 +76,7 @@ CREATE TABLE contacts(
contact_status TEXT NOT NULL DEFAULT 'active', contact_status TEXT NOT NULL DEFAULT 'active',
custom_data BLOB, custom_data BLOB,
ui_themes TEXT, ui_themes TEXT,
chat_deleted INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(user_id, local_display_name) FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE ON DELETE CASCADE
@@ -228,7 +229,8 @@ CREATE TABLE rcv_files(
REFERENCES xftp_file_descriptions ON DELETE SET NULL, REFERENCES xftp_file_descriptions ON DELETE SET NULL,
agent_rcv_file_id BLOB NULL, agent_rcv_file_id BLOB NULL,
agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL), agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL),
to_receive INTEGER to_receive INTEGER,
user_approved_relays INTEGER NOT NULL DEFAULT 0
); );
CREATE TABLE snd_file_chunks( CREATE TABLE snd_file_chunks(
file_id INTEGER NOT NULL, file_id INTEGER NOT NULL,
@@ -391,7 +393,8 @@ CREATE TABLE chat_items(
fwd_from_msg_dir INTEGER, fwd_from_msg_dir INTEGER,
fwd_from_contact_id INTEGER REFERENCES contacts ON DELETE SET NULL, fwd_from_contact_id INTEGER REFERENCES contacts ON DELETE SET NULL,
fwd_from_group_id INTEGER REFERENCES groups ON DELETE SET NULL, fwd_from_group_id INTEGER REFERENCES groups ON DELETE SET NULL,
fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL,
via_proxy INTEGER
); );
CREATE TABLE chat_item_messages( CREATE TABLE chat_item_messages(
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
@@ -502,6 +505,8 @@ CREATE TABLE group_snd_item_statuses(
group_snd_item_status TEXT NOT NULL, group_snd_item_status TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')), created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now')) updated_at TEXT NOT NULL DEFAULT(datetime('now'))
,
via_proxy INTEGER
); );
CREATE TABLE IF NOT EXISTS "sent_probes"( CREATE TABLE IF NOT EXISTS "sent_probes"(
sent_probe_id INTEGER PRIMARY KEY, sent_probe_id INTEGER PRIMARY KEY,
+3 -3
View File
@@ -98,19 +98,19 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
[sql| [sql|
SELECT SELECT
c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite,
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.custom_data p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data
FROM contacts c FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0 WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0
|] |]
(userId, contactId) (userId, contactId)
toContact' :: Int64 -> Connection -> [ContactRow'] -> Either StoreError Contact toContact' :: Int64 -> Connection -> [ContactRow'] -> Either StoreError Contact
toContact' contactId conn [(profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData)] = toContact' contactId conn [(profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (contactGroupMemberId, contactGrpInvSent, uiThemes, chatDeleted, customData)] =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
activeConn = Just conn activeConn = Just conn
in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData} in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, chatDeleted, customData}
toContact' _ _ _ = Left $ SEInternalError "referenced contact not found" toContact' _ _ _ = Left $ SEInternalError "referenced contact not found"
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember)
getGroupAndMember_ groupMemberId c = ExceptT $ do getGroupAndMember_ groupMemberId c = ExceptT $ do
+20 -8
View File
@@ -30,7 +30,8 @@ module Simplex.Chat.Store.Direct
getConnReqContactXContactId, getConnReqContactXContactId,
getContactByConnReqHash, getContactByConnReqHash,
createDirectContact, createDirectContact,
deleteContactConnectionsAndFiles, deleteContactConnections,
deleteContactFiles,
deleteContact, deleteContact,
deleteContactWithoutGroups, deleteContactWithoutGroups,
setContactDeleted, setContactDeleted,
@@ -70,6 +71,7 @@ module Simplex.Chat.Store.Direct
resetContactConnInitiated, resetContactConnInitiated,
setContactCustomData, setContactCustomData,
setContactUIThemes, setContactUIThemes,
setContactChatDeleted,
) )
where where
@@ -178,7 +180,7 @@ getContactByConnReqHash db vr user@User {userId} cReqHash =
SELECT SELECT
-- Contact -- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data,
-- Connection -- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -241,12 +243,13 @@ createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p
chatTs = Just currentTs, chatTs = Just currentTs,
contactGroupMemberId = Nothing, contactGroupMemberId = Nothing,
contactGrpInvSent = False, contactGrpInvSent = False,
customData = Nothing, uiThemes = Nothing,
uiThemes = Nothing chatDeleted = False,
customData = Nothing
} }
deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO () deleteContactConnections :: DB.Connection -> User -> Contact -> IO ()
deleteContactConnectionsAndFiles db userId Contact {contactId} = do deleteContactConnections db User {userId} Contact {contactId} = do
DB.execute DB.execute
db db
[sql| [sql|
@@ -258,6 +261,9 @@ deleteContactConnectionsAndFiles db userId Contact {contactId} = do
) )
|] |]
(userId, contactId) (userId, contactId)
deleteContactFiles :: DB.Connection -> User -> Contact -> IO ()
deleteContactFiles db User {userId} Contact {contactId} = do
DB.execute db "DELETE FROM files WHERE user_id = ? AND contact_id = ?" (userId, contactId) DB.execute db "DELETE FROM files WHERE user_id = ? AND contact_id = ?" (userId, contactId)
deleteContact :: DB.Connection -> User -> Contact -> ExceptT StoreError IO () deleteContact :: DB.Connection -> User -> Contact -> ExceptT StoreError IO ()
@@ -600,7 +606,7 @@ createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (V
SELECT SELECT
-- Contact -- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data,
-- Connection -- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -764,6 +770,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
contactGroupMemberId = Nothing, contactGroupMemberId = Nothing,
contactGrpInvSent = False, contactGrpInvSent = False,
uiThemes = Nothing, uiThemes = Nothing,
chatDeleted = False,
customData = Nothing customData = Nothing
} }
@@ -784,7 +791,7 @@ getContact_ db vr user@User {userId} contactId deleted =
SELECT SELECT
-- Contact -- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data,
-- Connection -- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -934,3 +941,8 @@ setContactUIThemes :: DB.Connection -> User -> Contact -> Maybe UIThemeEntityOve
setContactUIThemes db User {userId} Contact {contactId} uiThemes = do setContactUIThemes db User {userId} Contact {contactId} uiThemes = do
updatedAt <- getCurrentTime updatedAt <- getCurrentTime
DB.execute db "UPDATE contacts SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (uiThemes, updatedAt, userId, contactId) DB.execute db "UPDATE contacts SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (uiThemes, updatedAt, userId, contactId)
setContactChatDeleted :: DB.Connection -> User -> Contact -> Bool -> IO ()
setContactChatDeleted db User {userId} Contact {contactId} chatDeleted = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE contacts SET chat_deleted = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (chatDeleted, updatedAt, userId, contactId)
+29 -20
View File
@@ -514,7 +514,7 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_ let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, userApprovedRelays = False}) <$> rfd_
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
fileId <- liftIO $ do fileId <- liftIO $ do
DB.execute DB.execute
@@ -535,7 +535,7 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_ let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, userApprovedRelays = False}) <$> rfd_
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
fileId <- liftIO $ do fileId <- liftIO $ do
DB.execute DB.execute
@@ -676,7 +676,9 @@ getRcvFileTransfer_ db userId fileId = do
[sql| [sql|
SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name, SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name,
f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name, f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name,
f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, r.agent_rcv_file_id, r.agent_rcv_file_deleted, c.connection_id, c.agent_conn_id f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline,
r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays,
c.connection_id, c.agent_conn_id
FROM rcv_files r FROM rcv_files r
JOIN files f USING (file_id) JOIN files f USING (file_id)
LEFT JOIN connections c ON r.file_id = c.rcv_file_id LEFT JOIN connections c ON r.file_id = c.rcv_file_id
@@ -690,9 +692,9 @@ getRcvFileTransfer_ db userId fileId = do
where where
rcvFileTransfer :: rcvFileTransfer ::
Maybe RcvFileDescr -> Maybe RcvFileDescr ->
(FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool) :. (Maybe Int64, Maybe AgentConnId) -> (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool, Bool) :. (Maybe Int64, Maybe AgentConnId) ->
ExceptT StoreError IO RcvFileTransfer ExceptT StoreError IO RcvFileTransfer
rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted) :. (connId_, agentConnId_)) = rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays) :. (connId_, agentConnId_)) =
case contactName_ <|> memberName_ <|> standaloneName_ of case contactName_ <|> memberName_ <|> standaloneName_ of
Nothing -> throwError $ SERcvFileInvalid fileId Nothing -> throwError $ SERcvFileInvalid fileId
Just name -> Just name ->
@@ -709,7 +711,7 @@ getRcvFileTransfer_ db userId fileId = do
ft senderDisplayName fileStatus = ft senderDisplayName fileStatus =
let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing} let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing}
cryptoArgs = CFArgs <$> fileKey <*> fileNonce cryptoArgs = CFArgs <$> fileKey <*> fileNonce
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted}) <$> rfd_ xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays}) <$> rfd_
in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs} in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs}
rfi = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_ rfi = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_
rfi_ = case (filePath_, connId_, agentConnId_) of rfi_ = case (filePath_, connId_, agentConnId_) of
@@ -720,7 +722,7 @@ getRcvFileTransfer_ db userId fileId = do
acceptRcvFileTransfer :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem acceptRcvFileTransfer :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem
acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do
currentTs <- getCurrentTime currentTs <- getCurrentTime
acceptRcvFT_ db user fileId filePath Nothing currentTs acceptRcvFT_ db user fileId filePath False Nothing currentTs
DB.execute DB.execute
db db
"INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?)" "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?)"
@@ -740,33 +742,40 @@ getContactByFileId db vr user@User {userId} fileId = do
acceptRcvInlineFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem acceptRcvInlineFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem
acceptRcvInlineFT db vr user fileId filePath = do acceptRcvInlineFT db vr user fileId filePath = do
liftIO $ acceptRcvFT_ db user fileId filePath (Just IFMOffer) =<< getCurrentTime liftIO $ acceptRcvFT_ db user fileId filePath False (Just IFMOffer) =<< getCurrentTime
getChatItemByFileId db vr user fileId getChatItemByFileId db vr user fileId
startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> Maybe InlineFileMode -> IO () startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> Maybe InlineFileMode -> IO ()
startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline = startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline =
acceptRcvFT_ db user fileId filePath rcvFileInline =<< getCurrentTime acceptRcvFT_ db user fileId filePath False rcvFileInline =<< getCurrentTime
xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> Bool -> ExceptT StoreError IO AChatItem
xftpAcceptRcvFT db vr user fileId filePath = do xftpAcceptRcvFT db vr user fileId filePath userApprovedRelays = do
liftIO $ acceptRcvFT_ db user fileId filePath Nothing =<< getCurrentTime liftIO $ acceptRcvFT_ db user fileId filePath userApprovedRelays Nothing =<< getCurrentTime
getChatItemByFileId db vr user fileId getChatItemByFileId db vr user fileId
acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Maybe InlineFileMode -> UTCTime -> IO () acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Bool -> Maybe InlineFileMode -> UTCTime -> IO ()
acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do acceptRcvFT_ db User {userId} fileId filePath userApprovedRelays rcvFileInline currentTs = do
DB.execute DB.execute
db db
"UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?" "UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?"
(filePath, CIFSRcvAccepted, currentTs, userId, fileId) (filePath, CIFSRcvAccepted, currentTs, userId, fileId)
DB.execute DB.execute
db db
"UPDATE rcv_files SET rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?" "UPDATE rcv_files SET user_approved_relays = ?, rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?"
(rcvFileInline, FSAccepted, currentTs, fileId) (userApprovedRelays, rcvFileInline, FSAccepted, currentTs, fileId)
setRcvFileToReceive :: DB.Connection -> FileTransferId -> Maybe CryptoFileArgs -> IO () setRcvFileToReceive :: DB.Connection -> FileTransferId -> Bool -> Maybe CryptoFileArgs -> IO ()
setRcvFileToReceive db fileId cfArgs_ = do setRcvFileToReceive db fileId userApprovedRelays cfArgs_ = do
currentTs <- getCurrentTime currentTs <- getCurrentTime
DB.execute db "UPDATE rcv_files SET to_receive = 1, updated_at = ? WHERE file_id = ?" (currentTs, fileId) DB.execute
db
[sql|
UPDATE rcv_files
SET to_receive = 1, user_approved_relays = ?, updated_at = ?
WHERE file_id = ?
|]
(userApprovedRelays, currentTs, fileId)
forM_ cfArgs_ $ \cfArgs -> setFileCryptoArgs_ db fileId cfArgs currentTs forM_ cfArgs_ $ \cfArgs -> setFileCryptoArgs_ db fileId cfArgs currentTs
setFileCryptoArgs :: DB.Connection -> FileTransferId -> CryptoFileArgs -> IO () setFileCryptoArgs :: DB.Connection -> FileTransferId -> CryptoFileArgs -> IO ()
@@ -950,7 +959,7 @@ getFileTransferMeta_ db userId fileId =
fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileKey, fileNonce, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_, xftpRedirectFor) = fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileKey, fileNonce, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_, xftpRedirectFor) =
let cryptoArgs = CFArgs <$> fileKey <*> fileNonce let cryptoArgs = CFArgs <$> fileKey <*> fileNonce
xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted, cryptoArgs}) <$> aSndFileId_ xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted, cryptoArgs}) <$> aSndFileId_
in FileTransferMeta {fileId, xftpSndFile, xftpRedirectFor, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_} in FileTransferMeta {fileId, xftpSndFile, xftpRedirectFor, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_}
lookupFileTransferRedirectMeta :: DB.Connection -> User -> Int64 -> IO [FileTransferMeta] lookupFileTransferRedirectMeta :: DB.Connection -> User -> Int64 -> IO [FileTransferMeta]
lookupFileTransferRedirectMeta db User {userId} fileId = do lookupFileTransferRedirectMeta db User {userId} fileId = do
+2 -2
View File
@@ -1960,7 +1960,7 @@ createMemberContact
authErrCounter = 0 authErrCounter = 0
} }
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, uiThemes = Nothing, customData = Nothing} pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, uiThemes = Nothing, chatDeleted = False, customData = Nothing}
getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
getMemberContact db vr user contactId = do getMemberContact db vr user contactId = do
@@ -1997,7 +1997,7 @@ createMemberContactInvited
contactId <- createContactUpdateMember currentTs userPreferences contactId <- createContactUpdateMember currentTs userPreferences
ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, uiThemes = Nothing, customData = Nothing} mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, uiThemes = Nothing, chatDeleted = False, customData = Nothing}
m' = m {memberContactId = Just contactId} m' = m {memberContactId = Just contactId}
pure (mCt', m') pure (mCt', m')
where where
+40 -18
View File
@@ -95,6 +95,7 @@ module Simplex.Chat.Store.Messages
lookupChatItemByFileId, lookupChatItemByFileId,
getChatItemByGroupId, getChatItemByGroupId,
updateDirectChatItemStatus, updateDirectChatItemStatus,
setDirectSndChatItemViaProxy,
getTimedItems, getTimedItems,
getChatItemTTL, getChatItemTTL,
setChatItemTTL, setChatItemTTL,
@@ -108,6 +109,7 @@ module Simplex.Chat.Store.Messages
createGroupSndStatus, createGroupSndStatus,
getGroupSndStatus, getGroupSndStatus,
updateGroupSndStatus, updateGroupSndStatus,
setGroupSndViaProxy,
getGroupSndStatuses, getGroupSndStatuses,
getGroupSndStatusCounts, getGroupSndStatusCounts,
getGroupHistoryItems, getGroupHistoryItems,
@@ -315,7 +317,7 @@ updateChatTs db User {userId} chatDirection chatTs = case toChatInfo chatDirecti
DirectChat Contact {contactId} -> DirectChat Contact {contactId} ->
DB.execute DB.execute
db db
"UPDATE contacts SET chat_ts = ? WHERE user_id = ? AND contact_id = ?" "UPDATE contacts SET chat_ts = ?, chat_deleted = 0 WHERE user_id = ? AND contact_id = ?"
(chatTs, userId, contactId) (chatTs, userId, contactId)
GroupChat GroupInfo {groupId} -> GroupChat GroupInfo {groupId} ->
DB.execute DB.execute
@@ -806,7 +808,7 @@ getLocalChatPreview_ db user (LocalChatPD _ noteFolderId lastItemId_ stats) = do
-- this function can be changed so it never fails, not only avoid failure on invalid json -- this function can be changed so it never fails, not only avoid failure on invalid json
toLocalChatItem :: UTCTime -> ChatItemRow -> Either StoreError (CChatItem 'CTLocal) toLocalChatItem :: UTCTime -> ChatItemRow -> Either StoreError (CChatItem 'CTLocal)
toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) = toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) =
chatItem $ fromRight invalid $ dbParseACIContent itemContentText chatItem $ fromRight invalid $ dbParseACIContent itemContentText
where where
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
@@ -839,7 +841,7 @@ toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentTex
_ -> Just (CIDeleted @CTLocal deletedTs) _ -> Just (CIDeleted @CTLocal deletedTs)
itemEdited' = fromMaybe False itemEdited itemEdited' = fromMaybe False itemEdited
itemForwarded = toCIForwardedFrom forwardedFromRow itemForwarded = toCIForwardedFrom forwardedFromRow
in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt in mkCIMeta itemId content itemText status sentViaProxy sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt
ciTimed :: Maybe CITimed ciTimed :: Maybe CITimed
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
@@ -1407,7 +1409,7 @@ type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool)
type ChatItemForwardedFromRow = (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64) type ChatItemForwardedFromRow = (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64)
type ChatItemRow = type ChatItemRow =
(Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe SharedMsgId) (Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe Bool, Maybe SharedMsgId)
:. (Int, Maybe UTCTime, Maybe Bool, UTCTime, UTCTime) :. (Int, Maybe UTCTime, Maybe Bool, UTCTime, UTCTime)
:. ChatItemForwardedFromRow :. ChatItemForwardedFromRow
:. ChatItemModeRow :. ChatItemModeRow
@@ -1426,7 +1428,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir
-- this function can be changed so it never fails, not only avoid failure on invalid json -- this function can be changed so it never fails, not only avoid failure on invalid json
toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect) toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect)
toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) = toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) =
chatItem $ fromRight invalid $ dbParseACIContent itemContentText chatItem $ fromRight invalid $ dbParseACIContent itemContentText
where where
invalid = ACIContent msgDir $ CIInvalidJSON itemContentText invalid = ACIContent msgDir $ CIInvalidJSON itemContentText
@@ -1459,7 +1461,7 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT
_ -> Just (CIDeleted @CTDirect deletedTs) _ -> Just (CIDeleted @CTDirect deletedTs)
itemEdited' = fromMaybe False itemEdited itemEdited' = fromMaybe False itemEdited
itemForwarded = toCIForwardedFrom forwardedFromRow itemForwarded = toCIForwardedFrom forwardedFromRow
in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt in mkCIMeta itemId content itemText status sentViaProxy sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt
ciTimed :: Maybe CITimed ciTimed :: Maybe CITimed
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
@@ -1483,7 +1485,7 @@ toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction
-- this function can be changed so it never fails, not only avoid failure on invalid json -- this function can be changed so it never fails, not only avoid failure on invalid json
toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. Only (Maybe GroupMemberId) :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup) toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. Only (Maybe GroupMemberId) :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup)
toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. Only forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. Only forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do
chatItem $ fromRight invalid $ dbParseACIContent itemContentText chatItem $ fromRight invalid $ dbParseACIContent itemContentText
where where
member_ = toMaybeGroupMember userContactId memberRow_ member_ = toMaybeGroupMember userContactId memberRow_
@@ -1521,7 +1523,7 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir,
_ -> Just (maybe (CIDeleted @CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_) _ -> Just (maybe (CIDeleted @CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_)
itemEdited' = fromMaybe False itemEdited itemEdited' = fromMaybe False itemEdited
itemForwarded = toCIForwardedFrom forwardedFromRow itemForwarded = toCIForwardedFrom forwardedFromRow
in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt in mkCIMeta itemId content itemText status sentViaProxy sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt
ciTimed :: Maybe CITimed ciTimed :: Maybe CITimed
ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt}
@@ -1600,6 +1602,11 @@ updateDirectChatItemStatus db user@User {userId} ct@Contact {contactId} itemId i
liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, contactId, itemId) liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, contactId, itemId)
pure ci {meta = (meta ci) {itemStatus}} pure ci {meta = (meta ci) {itemStatus}}
setDirectSndChatItemViaProxy :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect 'MDSnd -> Bool -> IO (ChatItem 'CTDirect 'MDSnd)
setDirectSndChatItemViaProxy db User {userId} Contact {contactId} ci viaProxy = do
DB.execute db "UPDATE chat_items SET via_proxy = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (viaProxy, userId, contactId, chatItemId' ci)
pure ci {meta = (meta ci) {sentViaProxy = Just viaProxy}}
updateDirectChatItem :: MsgDirectionI d => DB.Connection -> User -> Contact -> ChatItemId -> CIContent d -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d) updateDirectChatItem :: MsgDirectionI d => DB.Connection -> User -> Contact -> ChatItemId -> CIContent d -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d)
updateDirectChatItem db user ct@Contact {contactId} itemId newContent edited live timed_ msgId_ = do updateDirectChatItem db user ct@Contact {contactId} itemId newContent edited live timed_ msgId_ = do
ci <- liftEither . correctDir =<< getDirectCIWithReactions db user ct itemId ci <- liftEither . correctDir =<< getDirectCIWithReactions db user ct itemId
@@ -1758,7 +1765,7 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do
[sql| [sql|
SELECT SELECT
-- ChatItem -- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id,
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
i.timed_ttl, i.timed_delete_at, i.item_live, i.timed_ttl, i.timed_delete_at, i.item_live,
@@ -2001,7 +2008,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
[sql| [sql|
SELECT SELECT
-- ChatItem -- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id,
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
i.timed_ttl, i.timed_delete_at, i.item_live, i.timed_ttl, i.timed_delete_at, i.item_live,
@@ -2105,7 +2112,7 @@ getLocalChatItem db User {userId} folderId itemId = ExceptT $ do
[sql| [sql|
SELECT SELECT
-- ChatItem -- ChatItem
i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id,
i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at,
i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id,
i.timed_ttl, i.timed_delete_at, i.item_live, i.timed_ttl, i.timed_delete_at, i.item_live,
@@ -2538,16 +2545,31 @@ updateGroupSndStatus db itemId memberId status = do
|] |]
(status, currentTs, itemId, memberId) (status, currentTs, itemId, memberId)
getGroupSndStatuses :: DB.Connection -> ChatItemId -> IO [(GroupMemberId, CIStatus 'MDSnd)] setGroupSndViaProxy :: DB.Connection -> ChatItemId -> GroupMemberId -> Bool -> IO ()
getGroupSndStatuses db itemId = setGroupSndViaProxy db itemId memberId viaProxy =
DB.query DB.execute
db db
[sql| [sql|
SELECT group_member_id, group_snd_item_status UPDATE group_snd_item_statuses
FROM group_snd_item_statuses SET via_proxy = ?
WHERE chat_item_id = ? WHERE chat_item_id = ? AND group_member_id = ?
|] |]
(Only itemId) (viaProxy, itemId, memberId)
getGroupSndStatuses :: DB.Connection -> ChatItemId -> IO [MemberDeliveryStatus]
getGroupSndStatuses db itemId =
map memStatus
<$> DB.query
db
[sql|
SELECT group_member_id, group_snd_item_status, via_proxy
FROM group_snd_item_statuses
WHERE chat_item_id = ?
|]
(Only itemId)
where
memStatus (groupMemberId, memberDeliveryStatus, sentViaProxy) =
MemberDeliveryStatus {groupMemberId, memberDeliveryStatus, sentViaProxy}
getGroupSndStatusCounts :: DB.Connection -> ChatItemId -> IO [(CIStatus 'MDSnd, Int)] getGroupSndStatusCounts :: DB.Connection -> ChatItemId -> IO [(CIStatus 'MDSnd, Int)]
getGroupSndStatusCounts db itemId = getGroupSndStatusCounts db itemId =
+7 -1
View File
@@ -106,6 +106,9 @@ import Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id
import Simplex.Chat.Migrations.M20240324_custom_data import Simplex.Chat.Migrations.M20240324_custom_data
import Simplex.Chat.Migrations.M20240402_item_forwarded import Simplex.Chat.Migrations.M20240402_item_forwarded
import Simplex.Chat.Migrations.M20240430_ui_theme import Simplex.Chat.Migrations.M20240430_ui_theme
import Simplex.Chat.Migrations.M20240501_chat_deleted
import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
import Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)] schemaMigrations :: [(String, Query, Maybe Query)]
@@ -211,7 +214,10 @@ schemaMigrations =
("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id), ("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id),
("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data), ("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data),
("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded), ("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded),
("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme) ("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme),
("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted),
("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy),
("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date

Some files were not shown because too many files have changed in this diff Show More