mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d62d7ddfb | |||
| b58ccedf23 | |||
| a78aeb47c5 | |||
| 01cadefde7 | |||
| c70e7223d9 | |||
| a1d4f52185 | |||
| 373070f388 | |||
| a03ccd0ba6 | |||
| a7982cccf9 | |||
| dccd6631af | |||
| b0516b6440 | |||
| bf91666f07 | |||
| ab47a5a27e | |||
| 23581f59ae | |||
| 291df6e9d0 | |||
| dae0b63c22 | |||
| 0964982782 | |||
| 9eb6034e1a | |||
| c59eb06b8a | |||
| 423fc96638 | |||
| ba203faad4 | |||
| ec7b35adb9 | |||
| d2d450d1d7 | |||
| 94df5d47d1 | |||
| b4caefb17c | |||
| fd791f3fb8 | |||
| 0720292ebf | |||
| a0d6ae15ab | |||
| 3e76a2d49c | |||
| f9cba3f8e0 | |||
| ed3365bb6d | |||
| b2a8797ed4 | |||
| d3e522e169 | |||
| 49e09ca449 | |||
| d1dde3d0c8 | |||
| ef75eceb40 | |||
| 96ce59f330 | |||
| 3655971aec | |||
| 98a67fa2e4 | |||
| 4b13512950 | |||
| dea45e3228 | |||
| 4c0d47bbd4 | |||
| 3129602a78 | |||
| a0c257962e | |||
| 93ae1145bc | |||
| d02668386f | |||
| bd8c8b5a8c | |||
| 4f2102ac6e | |||
| 17bcdaf6c4 | |||
| e359ec02f5 | |||
| a1948d549b | |||
| 396b73b480 | |||
| 331b4633fc | |||
| 29db2b4e9b | |||
| f091384aa7 | |||
| 1a2fad070e | |||
| f40ba6f04d | |||
| 06d61ea73e | |||
| 84d7a77a9f | |||
| 878eea774d | |||
| 6101be5657 | |||
| 4575dc6e30 | |||
| a10dd81a1d | |||
| 554ef4eefc | |||
| 289130832a | |||
| 567e9981be | |||
| 224cfe9e02 | |||
| 1183fe427f | |||
| 0679d023e2 | |||
| c49a1f42e7 | |||
| 4f173215c6 | |||
| 41597e563e | |||
| aa127f0cea | |||
| f9c64ba39c | |||
| 1025fa94a1 | |||
| a59769b91d | |||
| 76248b4878 | |||
| 93a7ddb128 | |||
| bc5af35a3e | |||
| 6d5869e974 | |||
| f0974af03a | |||
| da70aa30b6 | |||
| e38d5bd885 | |||
| 10f41aa1c9 | |||
| 1b081197d4 |
+4
-3
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ struct GroupPreferencesView: View {
|
|||||||
featureSection(.reactions, $preferences.reactions.enable)
|
featureSection(.reactions, $preferences.reactions.enable)
|
||||||
featureSection(.voice, $preferences.voice.enable, $preferences.voice.role)
|
featureSection(.voice, $preferences.voice.enable, $preferences.voice.role)
|
||||||
featureSection(.files, $preferences.files.enable, $preferences.files.role)
|
featureSection(.files, $preferences.files.enable, $preferences.files.role)
|
||||||
// TODO enable simplexLinks preference in 5.8
|
featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
|
||||||
// featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
|
|
||||||
featureSection(.history, $preferences.history.enable)
|
featureSection(.history, $preferences.history.enable)
|
||||||
|
|
||||||
if groupInfo.canEdit {
|
if groupInfo.canEdit {
|
||||||
@@ -102,8 +101,6 @@ struct GroupPreferencesView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: 36)
|
.frame(height: 36)
|
||||||
// remove in v5.8
|
|
||||||
.disabled(true)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
settingsRow(icon, color: color) {
|
settingsRow(icon, color: color) {
|
||||||
|
|||||||
@@ -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
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -7235,7 +7235,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="member connected" xml:space="preserve">
|
<trans-unit id="member connected" xml:space="preserve">
|
||||||
<source>connected</source>
|
<source>connected</source>
|
||||||
<target>kapcsolódva</target>
|
<target>kapcsolódott</target>
|
||||||
<note>rcv group event chat item</note>
|
<note>rcv group event chat item</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="message received" xml:space="preserve">
|
<trans-unit id="message received" xml:space="preserve">
|
||||||
|
|||||||
@@ -5371,7 +5371,7 @@ Hata: %@</target>
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Square, circle, or anything in between." xml:space="preserve">
|
<trans-unit id="Square, circle, or anything in between." xml:space="preserve">
|
||||||
<source>Square, circle, or anything in between.</source>
|
<source>Square, circle, or anything in between.</source>
|
||||||
<target>Kare,daire, veya aralarında herhangi birşey.</target>
|
<target>Kare,daire, veya aralarında herhangi bir şey.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Start chat" xml:space="preserve">
|
<trans-unit id="Start chat" xml:space="preserve">
|
||||||
|
|||||||
@@ -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))")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,11 +115,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 */; };
|
||||||
5CE0E8972BE7F144008D6E06 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8922BE7F144008D6E06 /* libgmp.a */; };
|
|
||||||
5CE0E8982BE7F144008D6E06 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8932BE7F144008D6E06 /* libgmpxx.a */; };
|
|
||||||
5CE0E8992BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8942BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a */; };
|
|
||||||
5CE0E89A2BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8952BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a */; };
|
|
||||||
5CE0E89B2BE7F144008D6E06 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8962BE7F144008D6E06 /* libffi.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 */; };
|
||||||
@@ -144,6 +139,11 @@
|
|||||||
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
|
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
|
||||||
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
|
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
|
||||||
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; };
|
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; };
|
||||||
|
5CEE87942C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE878F2C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a */; };
|
||||||
|
5CEE87952C024F4F00583B8A /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87902C024F4F00583B8A /* libgmp.a */; };
|
||||||
|
5CEE87962C024F4F00583B8A /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87912C024F4F00583B8A /* libgmpxx.a */; };
|
||||||
|
5CEE87972C024F4F00583B8A /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87922C024F4F00583B8A /* libffi.a */; };
|
||||||
|
5CEE87982C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CEE87932C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a */; };
|
||||||
5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; };
|
5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; };
|
||||||
5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; };
|
5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; };
|
||||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
|
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
|
||||||
@@ -420,11 +420,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>"; };
|
||||||
5CE0E8922BE7F144008D6E06 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
|
||||||
5CE0E8932BE7F144008D6E06 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
|
||||||
5CE0E8942BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a"; sourceTree = "<group>"; };
|
|
||||||
5CE0E8952BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a"; sourceTree = "<group>"; };
|
|
||||||
5CE0E8962BE7F144008D6E06 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.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; };
|
||||||
@@ -440,6 +435,11 @@
|
|||||||
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
|
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
|
||||||
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
|
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
|
||||||
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; };
|
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; };
|
||||||
|
5CEE878F2C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||||
|
5CEE87902C024F4F00583B8A /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||||
|
5CEE87912C024F4F00583B8A /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||||
|
5CEE87922C024F4F00583B8A /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||||
|
5CEE87932C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a"; sourceTree = "<group>"; };
|
||||||
5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = "<group>"; };
|
5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = "<group>"; };
|
||||||
5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = "<group>"; };
|
5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = "<group>"; };
|
||||||
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
|
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
|
||||||
@@ -529,13 +529,13 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
5CE0E8992BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a in Frameworks */,
|
5CEE87942C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a in Frameworks */,
|
||||||
5CE0E8982BE7F144008D6E06 /* libgmpxx.a in Frameworks */,
|
5CEE87972C024F4F00583B8A /* libffi.a in Frameworks */,
|
||||||
|
5CEE87962C024F4F00583B8A /* libgmpxx.a in Frameworks */,
|
||||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||||
5CE0E89A2BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a in Frameworks */,
|
5CEE87952C024F4F00583B8A /* libgmp.a in Frameworks */,
|
||||||
5CE0E89B2BE7F144008D6E06 /* libffi.a in Frameworks */,
|
|
||||||
5CE0E8972BE7F144008D6E06 /* libgmp.a in Frameworks */,
|
|
||||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||||
|
5CEE87982C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.a in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -601,11 +601,11 @@
|
|||||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5CE0E8962BE7F144008D6E06 /* libffi.a */,
|
5CEE87922C024F4F00583B8A /* libffi.a */,
|
||||||
5CE0E8922BE7F144008D6E06 /* libgmp.a */,
|
5CEE87902C024F4F00583B8A /* libgmp.a */,
|
||||||
5CE0E8932BE7F144008D6E06 /* libgmpxx.a */,
|
5CEE87912C024F4F00583B8A /* libgmpxx.a */,
|
||||||
5CE0E8942BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a */,
|
5CEE878F2C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G-ghc9.6.3.a */,
|
||||||
5CE0E8952BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a */,
|
5CEE87932C024F4F00583B8A /* libHSsimplex-chat-5.8.0.3-3OkzEeUKATkEGDdUZR1g6G.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 = 213;
|
CURRENT_PROJECT_VERSION = 220;
|
||||||
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.1;
|
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 = 213;
|
CURRENT_PROJECT_VERSION = 220;
|
||||||
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.1;
|
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 = 213;
|
CURRENT_PROJECT_VERSION = 220;
|
||||||
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.1;
|
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 = 213;
|
CURRENT_PROJECT_VERSION = 220;
|
||||||
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.1;
|
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 = 213;
|
CURRENT_PROJECT_VERSION = 220;
|
||||||
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.1;
|
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 = 213;
|
CURRENT_PROJECT_VERSION = 220;
|
||||||
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.1;
|
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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2443,7 +2443,7 @@
|
|||||||
"member %@ changed to %@" = "%1$@ megváltoztatta a nevét erre: %2$@";
|
"member %@ changed to %@" = "%1$@ megváltoztatta a nevét erre: %2$@";
|
||||||
|
|
||||||
/* rcv group event chat item */
|
/* rcv group event chat item */
|
||||||
"member connected" = "kapcsolódva";
|
"member connected" = "kapcsolódott";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre meg fog változni erre: \"%@\". A csoport minden tagja értesítést kap róla.";
|
"Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre meg fog változni erre: \"%@\". A csoport minden tagja értesítést kap róla.";
|
||||||
|
|||||||
@@ -3612,7 +3612,7 @@
|
|||||||
"Somebody" = "Biri";
|
"Somebody" = "Biri";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Square, circle, or anything in between." = "Kare,daire, veya aralarında herhangi birşey.";
|
"Square, circle, or anything in between." = "Kare,daire, veya aralarında herhangi bir şey.";
|
||||||
|
|
||||||
/* chat item text */
|
/* chat item text */
|
||||||
"standard end-to-end encryption" = "standart uçtan uca şifreleme";
|
"standard end-to-end encryption" = "standart uçtan uca şifreleme";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
|
|||||||
import chat.simplex.common.model.ChatController
|
import chat.simplex.common.model.ChatController
|
||||||
import chat.simplex.common.views.helpers.DBMigrationResult
|
import chat.simplex.common.views.helpers.DBMigrationResult
|
||||||
import chat.simplex.common.platform.chatModel
|
import chat.simplex.common.platform.chatModel
|
||||||
import chat.simplex.common.platform.initChatControllerAndRunMigrations
|
import chat.simplex.common.platform.initChatControllerOnStart
|
||||||
import chat.simplex.common.views.helpers.DatabaseUtils
|
import chat.simplex.common.views.helpers.DatabaseUtils
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -60,7 +60,7 @@ class MessagesFetcherWork(
|
|||||||
try {
|
try {
|
||||||
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
|
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
|
||||||
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
|
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
|
||||||
initChatControllerAndRunMigrations()
|
initChatControllerOnStart()
|
||||||
}
|
}
|
||||||
withTimeout(durationSeconds * 1000L) {
|
withTimeout(durationSeconds * 1000L) {
|
||||||
val chatController = ChatController
|
val chatController = ChatController
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.content.Context
|
|||||||
import chat.simplex.common.platform.Log
|
import chat.simplex.common.platform.Log
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.media.AudioManager
|
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -66,6 +65,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
context = this
|
context = this
|
||||||
initHaskell()
|
initHaskell()
|
||||||
initMultiplatform()
|
initMultiplatform()
|
||||||
|
runMigrations()
|
||||||
tmpDir.deleteRecursively()
|
tmpDir.deleteRecursively()
|
||||||
tmpDir.mkdir()
|
tmpDir.mkdir()
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
// It's important, otherwise, user may be locked in undefined state
|
// It's important, otherwise, user may be locked in undefined state
|
||||||
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
|
||||||
} else if (DatabaseUtils.ksAppPassword.get() == null || DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
} else if (DatabaseUtils.ksAppPassword.get() == null || DatabaseUtils.ksSelfDestructPassword.get() == null) {
|
||||||
initChatControllerAndRunMigrations()
|
initChatControllerOnStart()
|
||||||
}
|
}
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
|
||||||
}
|
}
|
||||||
@@ -254,7 +254,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
override fun androidSetNightModeIfSupported() {
|
override fun androidSetNightModeIfSupported() {
|
||||||
if (Build.VERSION.SDK_INT < 31) return
|
if (Build.VERSION.SDK_INT < 31) return
|
||||||
|
|
||||||
val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM.name) {
|
val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM_THEME_NAME) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
CurrentColors.value.colors.isLight
|
CurrentColors.value.colors.isLight
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class SimplexService: Service() {
|
|||||||
isServiceStarted = true
|
isServiceStarted = true
|
||||||
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
|
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
|
||||||
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
|
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
|
||||||
initChatControllerAndRunMigrations()
|
initChatControllerOnStart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ kotlin {
|
|||||||
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
||||||
api("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
|
api("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
|
||||||
api("com.russhwolf:multiplatform-settings:1.1.1")
|
api("com.russhwolf:multiplatform-settings:1.1.1")
|
||||||
api("com.charleskorn.kaml:kaml:0.58.0")
|
api("com.charleskorn.kaml:kaml:0.59.0")
|
||||||
api("org.jetbrains.compose.ui:ui-text:${rootProject.extra["compose.version"] as String}")
|
api("org.jetbrains.compose.ui:ui-text:${rootProject.extra["compose.version"] as String}")
|
||||||
implementation("org.jetbrains.compose.components:components-animatedimage:${rootProject.extra["compose.version"] as String}")
|
implementation("org.jetbrains.compose.components:components-animatedimage:${rootProject.extra["compose.version"] as String}")
|
||||||
//Barcode
|
//Barcode
|
||||||
@@ -53,6 +53,9 @@ kotlin {
|
|||||||
val commonTest by getting {
|
val commonTest by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("test"))
|
implementation(kotlin("test"))
|
||||||
|
implementation(kotlin("test-junit"))
|
||||||
|
implementation(kotlin("test-common"))
|
||||||
|
implementation(kotlin("test-annotations-common"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val androidMain by getting {
|
val androidMain by getting {
|
||||||
|
|||||||
+2
@@ -15,8 +15,10 @@ actual val dataDir: File = androidAppContext.dataDir
|
|||||||
actual val tmpDir: File = androidAppContext.getDir("temp", Application.MODE_PRIVATE)
|
actual val tmpDir: File = androidAppContext.getDir("temp", Application.MODE_PRIVATE)
|
||||||
actual val filesDir: File = File(dataDir.absolutePath + File.separator + "files")
|
actual val filesDir: File = File(dataDir.absolutePath + File.separator + "files")
|
||||||
actual val appFilesDir: File = File(filesDir.absolutePath + File.separator + "app_files")
|
actual val appFilesDir: File = File(filesDir.absolutePath + File.separator + "app_files")
|
||||||
|
actual val wallpapersDir: File = File(filesDir.absolutePath + File.separator + "assets" + File.separator + "wallpapers").also { it.mkdirs() }
|
||||||
actual val coreTmpDir: File = File(filesDir.absolutePath + File.separator + "temp_files")
|
actual val coreTmpDir: File = File(filesDir.absolutePath + File.separator + "temp_files")
|
||||||
actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "files"
|
actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "files"
|
||||||
|
actual val preferencesDir = File(dataDir.absolutePath + File.separator + "shared_prefs")
|
||||||
|
|
||||||
actual val chatDatabaseFileName: String = "files_chat.db"
|
actual val chatDatabaseFileName: String = "files_chat.db"
|
||||||
actual val agentDatabaseFileName: String = "files_agent.db"
|
actual val agentDatabaseFileName: String = "files_agent.db"
|
||||||
|
|||||||
+7
@@ -7,6 +7,8 @@ import android.content.SharedPreferences
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.text.BidiFormatter
|
import android.text.BidiFormatter
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
@@ -14,9 +16,11 @@ import androidx.compose.ui.text.font.FontStyle
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import chat.simplex.common.model.AppPreferences
|
import chat.simplex.common.model.AppPreferences
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
import com.russhwolf.settings.SharedPreferencesSettings
|
import com.russhwolf.settings.SharedPreferencesSettings
|
||||||
|
import dev.icerock.moko.resources.ImageResource
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import dev.icerock.moko.resources.desc.desc
|
import dev.icerock.moko.resources.desc.desc
|
||||||
|
|
||||||
@@ -51,3 +55,6 @@ actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp
|
|||||||
actual fun desktopExpandWindowToWidth(width: Dp) {}
|
actual fun desktopExpandWindowToWidth(width: Dp) {}
|
||||||
|
|
||||||
actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text)
|
actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text)
|
||||||
|
|
||||||
|
actual fun ImageResource.toComposeImageBitmap(): ImageBitmap? =
|
||||||
|
getDrawable(androidAppContext)?.toBitmap()?.asImageBitmap()
|
||||||
|
|||||||
+17
-6
@@ -37,7 +37,7 @@ actual fun ClipboardManager.shareText(text: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun shareFile(text: String, fileSource: CryptoFile) {
|
fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean) {
|
||||||
val uri = if (fileSource.cryptoArgs != null) {
|
val uri = if (fileSource.cryptoArgs != null) {
|
||||||
val tmpFile = File(tmpDir, fileSource.filePath)
|
val tmpFile = File(tmpDir, fileSource.filePath)
|
||||||
tmpFile.deleteOnExit()
|
tmpFile.deleteOnExit()
|
||||||
@@ -55,20 +55,31 @@ actual fun shareFile(text: String, fileSource: CryptoFile) {
|
|||||||
}
|
}
|
||||||
val ext = fileSource.filePath.substringAfterLast(".")
|
val ext = fileSource.filePath.substringAfterLast(".")
|
||||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return
|
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return
|
||||||
val sendIntent: Intent = Intent().apply {
|
val sendIntent: Intent = Intent(if (justOpen) Intent.ACTION_VIEW else Intent.ACTION_SEND).apply {
|
||||||
action = Intent.ACTION_SEND
|
|
||||||
/*if (text.isNotEmpty()) {
|
/*if (text.isNotEmpty()) {
|
||||||
putExtra(Intent.EXTRA_TEXT, text)
|
putExtra(Intent.EXTRA_TEXT, text)
|
||||||
}*/
|
}*/
|
||||||
putExtra(Intent.EXTRA_STREAM, uri.toUri())
|
if (justOpen) {
|
||||||
type = mimeType
|
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
setDataAndType(uri.toUri(), mimeType)
|
||||||
|
} else {
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri.toUri())
|
||||||
|
type = mimeType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||||
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||||
androidAppContext.startActivity(shareIntent)
|
androidAppContext.startActivity(shareIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual fun shareFile(text: String, fileSource: CryptoFile) {
|
||||||
|
openOrShareFile(text, fileSource, justOpen = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun openFile(fileSource: CryptoFile) {
|
||||||
|
openOrShareFile("", fileSource, justOpen = true)
|
||||||
|
}
|
||||||
|
|
||||||
actual fun UriHandler.sendEmail(subject: String, body: CharSequence) {
|
actual fun UriHandler.sendEmail(subject: String, body: CharSequence) {
|
||||||
val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
|
val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
|
||||||
emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject)
|
emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject)
|
||||||
|
|||||||
+8
-23
@@ -19,14 +19,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.*
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
|
||||||
import chat.simplex.common.R
|
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
@@ -34,7 +30,6 @@ import chat.simplex.common.model.ChatModel
|
|||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.helpers.APPLICATION_ID
|
import chat.simplex.common.helpers.APPLICATION_ID
|
||||||
import chat.simplex.common.helpers.saveAppLocale
|
import chat.simplex.common.helpers.saveAppLocale
|
||||||
import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor
|
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.ImageResource
|
import dev.icerock.moko.resources.ImageResource
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
@@ -46,9 +41,8 @@ enum class AppIcon(val image: ImageResource) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) {
|
actual fun AppearanceView(m: ChatModel) {
|
||||||
val appIcon = remember { mutableStateOf(findEnabledIcon()) }
|
val appIcon = remember { mutableStateOf(findEnabledIcon()) }
|
||||||
|
|
||||||
fun setAppIcon(newIcon: AppIcon) {
|
fun setAppIcon(newIcon: AppIcon) {
|
||||||
if (appIcon.value == newIcon) return
|
if (appIcon.value == newIcon) return
|
||||||
val newComponent = ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}")
|
val newComponent = ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}")
|
||||||
@@ -65,18 +59,11 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod
|
|||||||
|
|
||||||
appIcon.value = newIcon
|
appIcon.value = newIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
AppearanceScope.AppearanceLayout(
|
AppearanceScope.AppearanceLayout(
|
||||||
appIcon,
|
appIcon,
|
||||||
m.controller.appPrefs.appLanguage,
|
m.controller.appPrefs.appLanguage,
|
||||||
m.controller.appPrefs.systemDarkTheme,
|
m.controller.appPrefs.systemDarkTheme,
|
||||||
changeIcon = ::setAppIcon,
|
changeIcon = ::setAppIcon,
|
||||||
showSettingsModal = showSettingsModal,
|
|
||||||
editColor = { name, initialColor ->
|
|
||||||
ModalManager.start.showModalCloseable { close ->
|
|
||||||
ColorEditor(name, initialColor, close)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,8 +73,6 @@ fun AppearanceScope.AppearanceLayout(
|
|||||||
languagePref: SharedPreference<String?>,
|
languagePref: SharedPreference<String?>,
|
||||||
systemDarkTheme: SharedPreference<String?>,
|
systemDarkTheme: SharedPreference<String?>,
|
||||||
changeIcon: (AppIcon) -> Unit,
|
changeIcon: (AppIcon) -> Unit,
|
||||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
|
||||||
editColor: (ThemeColor, Color) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
ColumnWithScrollBar(
|
ColumnWithScrollBar(
|
||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
@@ -120,6 +105,13 @@ fun AppearanceScope.AppearanceLayout(
|
|||||||
}
|
}
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SectionDividerSpaced(maxTopPadding = true)
|
||||||
|
ThemesSection(systemDarkTheme)
|
||||||
|
|
||||||
|
SectionDividerSpaced(maxTopPadding = true)
|
||||||
|
ProfileImageSection()
|
||||||
|
|
||||||
SectionDividerSpaced()
|
SectionDividerSpaced()
|
||||||
|
|
||||||
SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
|
SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
|
||||||
@@ -145,11 +137,6 @@ fun AppearanceScope.AppearanceLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SectionDividerSpaced(maxTopPadding = true)
|
|
||||||
ProfileImageSection()
|
|
||||||
|
|
||||||
SectionDividerSpaced(maxTopPadding = true)
|
|
||||||
ThemesSection(systemDarkTheme, showSettingsModal, editColor)
|
|
||||||
SectionBottomSpacer()
|
SectionBottomSpacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,8 +156,6 @@ fun PreviewAppearanceSettings() {
|
|||||||
languagePref = SharedPreference({ null }, {}),
|
languagePref = SharedPreference({ null }, {}),
|
||||||
systemDarkTheme = SharedPreference({ null }, {}),
|
systemDarkTheme = SharedPreference({ null }, {}),
|
||||||
changeIcon = {},
|
changeIcon = {},
|
||||||
showSettingsModal = { {} },
|
|
||||||
editColor = { _, _ -> },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+98
-17
@@ -4,7 +4,7 @@ import androidx.compose.material.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.font.*
|
import androidx.compose.ui.text.font.*
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
@@ -399,6 +399,18 @@ object ChatModel {
|
|||||||
currentUser.value = updated
|
currentUser.value = updated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateCurrentUserUiThemes(rhId: Long?, uiThemes: ThemeModeOverrides?) {
|
||||||
|
val current = currentUser.value ?: return
|
||||||
|
val updated = current.copy(
|
||||||
|
uiThemes = uiThemes
|
||||||
|
)
|
||||||
|
val i = users.indexOfFirst { it.user.userId == current.userId && it.user.remoteHostId == rhId }
|
||||||
|
if (i != -1) {
|
||||||
|
users[i] = users[i].copy(user = updated)
|
||||||
|
}
|
||||||
|
currentUser.value = updated
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem {
|
suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem {
|
||||||
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
|
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
@@ -682,7 +694,8 @@ data class User(
|
|||||||
override val showNtfs: Boolean,
|
override val showNtfs: Boolean,
|
||||||
val sendRcptsContacts: Boolean,
|
val sendRcptsContacts: Boolean,
|
||||||
val sendRcptsSmallGroups: Boolean,
|
val sendRcptsSmallGroups: Boolean,
|
||||||
val viewPwdHash: UserPwdHash?
|
val viewPwdHash: UserPwdHash?,
|
||||||
|
val uiThemes: ThemeModeOverrides? = null,
|
||||||
): NamedChat, UserLike {
|
): NamedChat, UserLike {
|
||||||
override val displayName: String get() = profile.displayName
|
override val displayName: String get() = profile.displayName
|
||||||
override val fullName: String get() = profile.fullName
|
override val fullName: String get() = profile.fullName
|
||||||
@@ -709,6 +722,7 @@ data class User(
|
|||||||
sendRcptsContacts = true,
|
sendRcptsContacts = true,
|
||||||
sendRcptsSmallGroups = false,
|
sendRcptsSmallGroups = false,
|
||||||
viewPwdHash = null,
|
viewPwdHash = null,
|
||||||
|
uiThemes = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1041,16 +1055,21 @@ data class Contact(
|
|||||||
override val updatedAt: Instant,
|
override val updatedAt: Instant,
|
||||||
val chatTs: Instant?,
|
val chatTs: Instant?,
|
||||||
val contactGroupMemberId: Long? = null,
|
val contactGroupMemberId: Long? = null,
|
||||||
val contactGrpInvSent: Boolean
|
val contactGrpInvSent: Boolean,
|
||||||
|
val uiThemes: ThemeModeOverrides? = null,
|
||||||
): SomeChat, NamedChat {
|
): SomeChat, NamedChat {
|
||||||
override val chatType get() = ChatType.Direct
|
override val chatType get() = ChatType.Direct
|
||||||
override val id get() = "@$contactId"
|
override val id get() = "@$contactId"
|
||||||
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
|
||||||
@@ -1109,7 +1128,8 @@ data class Contact(
|
|||||||
createdAt = Clock.System.now(),
|
createdAt = Clock.System.now(),
|
||||||
updatedAt = Clock.System.now(),
|
updatedAt = Clock.System.now(),
|
||||||
chatTs = Clock.System.now(),
|
chatTs = Clock.System.now(),
|
||||||
contactGrpInvSent = false
|
contactGrpInvSent = false,
|
||||||
|
uiThemes = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1150,15 +1170,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1245,7 +1269,8 @@ data class GroupInfo (
|
|||||||
val chatSettings: ChatSettings,
|
val chatSettings: ChatSettings,
|
||||||
override val createdAt: Instant,
|
override val createdAt: Instant,
|
||||||
override val updatedAt: Instant,
|
override val updatedAt: Instant,
|
||||||
val chatTs: Instant?
|
val chatTs: Instant?,
|
||||||
|
val uiThemes: ThemeModeOverrides? = null,
|
||||||
): SomeChat, NamedChat {
|
): SomeChat, NamedChat {
|
||||||
override val chatType get() = ChatType.Group
|
override val chatType get() = ChatType.Group
|
||||||
override val id get() = "#$groupId"
|
override val id get() = "#$groupId"
|
||||||
@@ -1287,7 +1312,8 @@ data class GroupInfo (
|
|||||||
chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
|
chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
|
||||||
createdAt = Clock.System.now(),
|
createdAt = Clock.System.now(),
|
||||||
updatedAt = Clock.System.now(),
|
updatedAt = Clock.System.now(),
|
||||||
chatTs = Clock.System.now()
|
chatTs = Clock.System.now(),
|
||||||
|
uiThemes = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1900,18 +1926,20 @@ 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,
|
||||||
itemDeleted: CIDeleted? = null,
|
itemDeleted: CIDeleted? = null,
|
||||||
itemEdited: Boolean = false,
|
itemEdited: Boolean = false,
|
||||||
itemTimed: CITimed? = null,
|
itemTimed: CITimed? = null,
|
||||||
|
itemLive: Boolean = false,
|
||||||
deletable: Boolean = true,
|
deletable: Boolean = true,
|
||||||
editable: Boolean = true
|
editable: Boolean = true
|
||||||
) =
|
) =
|
||||||
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, itemLive, 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 +2021,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 +2045,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 +2148,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 +2175,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 +2184,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 +2203,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 +2260,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 +2285,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 +2297,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 +2663,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 +2685,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 +2725,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 +2866,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 +2881,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 +3428,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 {
|
||||||
|
|||||||
+212
-52
@@ -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.*
|
||||||
@@ -25,8 +26,7 @@ import kotlinx.coroutines.sync.withLock
|
|||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
import kotlinx.serialization.builtins.MapSerializer
|
import kotlinx.serialization.builtins.*
|
||||||
import kotlinx.serialization.builtins.serializer
|
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@@ -106,6 +106,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 +131,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)
|
||||||
@@ -164,13 +167,20 @@ class AppPreferences {
|
|||||||
val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false)
|
val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false)
|
||||||
val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null)
|
val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null)
|
||||||
|
|
||||||
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name)
|
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM_THEME_NAME)
|
||||||
val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.name)
|
val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.themeName)
|
||||||
val themeOverrides = mkMapPreference(SHARED_PREFS_THEMES, mapOf(), encode = {
|
val currentThemeIds = mkMapPreference(SHARED_PREFS_CURRENT_THEME_IDs, mapOf(), encode = {
|
||||||
|
json.encodeToString(MapSerializer(String.serializer(), String.serializer()), it)
|
||||||
|
}, decode = {
|
||||||
|
json.decodeFromString(MapSerializer(String.serializer(), String.serializer()), it)
|
||||||
|
})
|
||||||
|
// Deprecated. Remove key from preferences in 2025
|
||||||
|
val themeOverridesOld = mkMapPreference(SHARED_PREFS_THEMES_OLD, mapOf(), encode = {
|
||||||
json.encodeToString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
|
json.encodeToString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
|
||||||
}, decode = {
|
}, decode = {
|
||||||
json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
|
jsonCoerceInputValues.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it)
|
||||||
}, settingsThemes)
|
}, settingsThemes)
|
||||||
|
val themeOverrides = mkThemeOverridesPreference()
|
||||||
val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f)
|
val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f)
|
||||||
|
|
||||||
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
|
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
|
||||||
@@ -185,6 +195,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)
|
||||||
@@ -263,6 +275,12 @@ class AppPreferences {
|
|||||||
set = fun(value) = prefs.putString(prefName, encode(value))
|
set = fun(value) = prefs.putString(prefName, encode(value))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun mkThemeOverridesPreference(): SharedPreference<List<ThemeOverrides>> =
|
||||||
|
SharedPreference(
|
||||||
|
get = fun() = themeOverridesStore ?: (readThemeOverrides()).also { themeOverridesStore = it },
|
||||||
|
set = fun(value) { if (writeThemeOverrides(value)) { themeOverridesStore = value } }
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS"
|
const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS"
|
||||||
internal const val SHARED_PREFS_THEMES_ID = "chat.simplex.app.THEMES"
|
internal const val SHARED_PREFS_THEMES_ID = "chat.simplex.app.THEMES"
|
||||||
@@ -288,6 +306,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 +325,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"
|
||||||
@@ -336,8 +357,10 @@ class AppPreferences {
|
|||||||
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
|
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
|
||||||
private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used
|
private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used
|
||||||
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
|
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
|
||||||
|
private const val SHARED_PREFS_CURRENT_THEME_IDs = "CurrentThemeIds"
|
||||||
private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme"
|
private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme"
|
||||||
private const val SHARED_PREFS_THEMES = "Themes"
|
private const val SHARED_PREFS_THEMES_OLD = "Themes"
|
||||||
|
private const val SHARED_PREFS_THEME_OVERRIDES = "ThemeOverrides"
|
||||||
private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius"
|
private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius"
|
||||||
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
|
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
|
||||||
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
|
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
|
||||||
@@ -348,9 +371,12 @@ 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"
|
||||||
|
|
||||||
|
private var themeOverridesStore: List<ThemeOverrides>? = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,8 +461,13 @@ object ChatController {
|
|||||||
Log.d(TAG, "startChatWithTemporaryDatabase")
|
Log.d(TAG, "startChatWithTemporaryDatabase")
|
||||||
val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl)
|
val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl)
|
||||||
apiSetNetworkConfig(netCfg, ctrl)
|
apiSetNetworkConfig(netCfg, ctrl)
|
||||||
apiSetTempFolder(getMigrationTempFilesDirectory().absolutePath, ctrl)
|
apiSetAppFilePaths(
|
||||||
apiSetFilesFolder(getMigrationTempFilesDirectory().absolutePath, ctrl)
|
getMigrationTempFilesDirectory().absolutePath,
|
||||||
|
getMigrationTempFilesDirectory().absolutePath,
|
||||||
|
wallpapersDir.parentFile.absolutePath,
|
||||||
|
remoteHostsDir.absolutePath,
|
||||||
|
ctrl
|
||||||
|
)
|
||||||
apiStartChat(ctrl)
|
apiStartChat(ctrl)
|
||||||
return migrationActiveUser
|
return migrationActiveUser
|
||||||
}
|
}
|
||||||
@@ -655,22 +686,10 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiSetTempFolder(tempFolder: String, ctrl: ChatCtrl? = null) {
|
suspend fun apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, remoteHostsFolder: String, ctrl: ChatCtrl? = null) {
|
||||||
val r = sendCmd(null, CC.SetTempFolder(tempFolder), ctrl)
|
val r = sendCmd(null, CC.ApiSetAppFilePaths(filesFolder, tempFolder, assetsFolder, remoteHostsFolder), ctrl)
|
||||||
if (r is CR.CmdOk) return
|
if (r is CR.CmdOk) return
|
||||||
throw Exception("failed to set temp folder: ${r.responseType} ${r.details}")
|
throw Exception("failed to set app file paths: ${r.responseType} ${r.details}")
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun apiSetFilesFolder(filesFolder: String, ctrl: ChatCtrl? = null) {
|
|
||||||
val r = sendCmd(null, CC.SetFilesFolder(filesFolder), ctrl)
|
|
||||||
if (r is CR.CmdOk) return
|
|
||||||
throw Exception("failed to set files folder: ${r.responseType} ${r.details}")
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun apiSetRemoteHostsFolder(remoteHostsFolder: String) {
|
|
||||||
val r = sendCmd(null, CC.SetRemoteHostsFolder(remoteHostsFolder))
|
|
||||||
if (r is CR.CmdOk) return
|
|
||||||
throw Exception("failed to set remote hosts folder: ${r.responseType} ${r.details}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable))
|
suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable))
|
||||||
@@ -775,8 +794,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1156,6 +1175,20 @@ object ChatController {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun apiSetUserUIThemes(rh: Long?, userId: Long, themes: ThemeModeOverrides?): Boolean {
|
||||||
|
val r = sendCmd(rh, CC.ApiSetUserUIThemes(userId, themes))
|
||||||
|
if (r is CR.CmdOk) return true
|
||||||
|
Log.e(TAG, "apiSetUserUIThemes bad response: ${r.responseType} ${r.details}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun apiSetChatUIThemes(rh: Long?, chatId: ChatId, themes: ThemeModeOverrides?): Boolean {
|
||||||
|
val r = sendCmd(rh, CC.ApiSetChatUIThemes(chatId, themes))
|
||||||
|
if (r is CR.CmdOk) return true
|
||||||
|
Log.e(TAG, "apiSetChatUIThemes bad response: ${r.responseType} ${r.details}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun apiCreateUserAddress(rh: Long?): String? {
|
suspend fun apiCreateUserAddress(rh: Long?): String? {
|
||||||
val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null }
|
val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null }
|
||||||
val r = sendCmd(rh, CC.ApiCreateMyAddress(userId))
|
val r = sendCmd(rh, CC.ApiCreateMyAddress(userId))
|
||||||
@@ -1330,9 +1363,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 +1384,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 +2080,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 +2103,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 +2258,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 +2369,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 +2391,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 +2411,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)
|
||||||
@@ -2387,9 +2460,8 @@ sealed class CC {
|
|||||||
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC()
|
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC()
|
||||||
class StartChat(val mainApp: Boolean): CC()
|
class StartChat(val mainApp: Boolean): CC()
|
||||||
class ApiStopChat: CC()
|
class ApiStopChat: CC()
|
||||||
class SetTempFolder(val tempFolder: String): CC()
|
@Serializable
|
||||||
class SetFilesFolder(val filesFolder: String): CC()
|
class ApiSetAppFilePaths(val appFilesFolder: String, val appTempFolder: String, val appAssetsFolder: String, val appRemoteHostsFolder: String): CC()
|
||||||
class SetRemoteHostsFolder(val remoteHostsFolder: String): CC()
|
|
||||||
class ApiSetEncryptLocalFiles(val enable: Boolean): CC()
|
class ApiSetEncryptLocalFiles(val enable: Boolean): CC()
|
||||||
class ApiExportArchive(val config: ArchiveConfig): CC()
|
class ApiExportArchive(val config: ArchiveConfig): CC()
|
||||||
class ApiImportArchive(val config: ArchiveConfig): CC()
|
class ApiImportArchive(val config: ArchiveConfig): CC()
|
||||||
@@ -2407,7 +2479,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()
|
||||||
@@ -2457,6 +2529,8 @@ sealed class CC {
|
|||||||
class ApiSetContactPrefs(val contactId: Long, val prefs: ChatPreferences): CC()
|
class ApiSetContactPrefs(val contactId: Long, val prefs: ChatPreferences): CC()
|
||||||
class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC()
|
class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC()
|
||||||
class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC()
|
class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC()
|
||||||
|
class ApiSetUserUIThemes(val userId: Long, val themes: ThemeModeOverrides?): CC()
|
||||||
|
class ApiSetChatUIThemes(val chatId: String, val themes: ThemeModeOverrides?): CC()
|
||||||
class ApiCreateMyAddress(val userId: Long): CC()
|
class ApiCreateMyAddress(val userId: Long): CC()
|
||||||
class ApiDeleteMyAddress(val userId: Long): CC()
|
class ApiDeleteMyAddress(val userId: Long): CC()
|
||||||
class ApiShowMyAddress(val userId: Long): CC()
|
class ApiShowMyAddress(val userId: Long): CC()
|
||||||
@@ -2475,7 +2549,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()
|
||||||
@@ -2524,9 +2598,7 @@ sealed class CC {
|
|||||||
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
|
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
|
||||||
is StartChat -> "/_start main=${onOff(mainApp)}"
|
is StartChat -> "/_start main=${onOff(mainApp)}"
|
||||||
is ApiStopChat -> "/_stop"
|
is ApiStopChat -> "/_stop"
|
||||||
is SetTempFolder -> "/_temp_folder $tempFolder"
|
is ApiSetAppFilePaths -> "/set file paths ${json.encodeToString(this)}"
|
||||||
is SetFilesFolder -> "/_files_folder $filesFolder"
|
|
||||||
is SetRemoteHostsFolder -> "/remote_hosts_folder $remoteHostsFolder"
|
|
||||||
is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}"
|
is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}"
|
||||||
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
|
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
|
||||||
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
|
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
|
||||||
@@ -2549,7 +2621,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"
|
||||||
@@ -2603,6 +2678,8 @@ sealed class CC {
|
|||||||
is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}"
|
is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}"
|
||||||
is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}"
|
is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}"
|
||||||
is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}"
|
is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}"
|
||||||
|
is ApiSetUserUIThemes -> "/_set theme user $userId ${if (themes != null) json.encodeToString(themes) else ""}"
|
||||||
|
is ApiSetChatUIThemes -> "/_set theme $chatId ${if (themes != null) json.encodeToString(themes) else ""}"
|
||||||
is ApiCreateMyAddress -> "/_address $userId"
|
is ApiCreateMyAddress -> "/_address $userId"
|
||||||
is ApiDeleteMyAddress -> "/_delete_address $userId"
|
is ApiDeleteMyAddress -> "/_delete_address $userId"
|
||||||
is ApiShowMyAddress -> "/_show_address $userId"
|
is ApiShowMyAddress -> "/_show_address $userId"
|
||||||
@@ -2623,6 +2700,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"
|
||||||
@@ -2666,9 +2744,7 @@ sealed class CC {
|
|||||||
is ApiDeleteUser -> "apiDeleteUser"
|
is ApiDeleteUser -> "apiDeleteUser"
|
||||||
is StartChat -> "startChat"
|
is StartChat -> "startChat"
|
||||||
is ApiStopChat -> "apiStopChat"
|
is ApiStopChat -> "apiStopChat"
|
||||||
is SetTempFolder -> "setTempFolder"
|
is ApiSetAppFilePaths -> "apiSetAppFilePaths"
|
||||||
is SetFilesFolder -> "setFilesFolder"
|
|
||||||
is SetRemoteHostsFolder -> "setRemoteHostsFolder"
|
|
||||||
is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles"
|
is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles"
|
||||||
is ApiExportArchive -> "apiExportArchive"
|
is ApiExportArchive -> "apiExportArchive"
|
||||||
is ApiImportArchive -> "apiImportArchive"
|
is ApiImportArchive -> "apiImportArchive"
|
||||||
@@ -2736,6 +2812,8 @@ sealed class CC {
|
|||||||
is ApiSetContactPrefs -> "apiSetContactPrefs"
|
is ApiSetContactPrefs -> "apiSetContactPrefs"
|
||||||
is ApiSetContactAlias -> "apiSetContactAlias"
|
is ApiSetContactAlias -> "apiSetContactAlias"
|
||||||
is ApiSetConnectionAlias -> "apiSetConnectionAlias"
|
is ApiSetConnectionAlias -> "apiSetConnectionAlias"
|
||||||
|
is ApiSetUserUIThemes -> "apiSetUserUIThemes"
|
||||||
|
is ApiSetChatUIThemes -> "apiSetChatUIThemes"
|
||||||
is ApiCreateMyAddress -> "apiCreateMyAddress"
|
is ApiCreateMyAddress -> "apiCreateMyAddress"
|
||||||
is ApiDeleteMyAddress -> "apiDeleteMyAddress"
|
is ApiDeleteMyAddress -> "apiDeleteMyAddress"
|
||||||
is ApiShowMyAddress -> "apiShowMyAddress"
|
is ApiShowMyAddress -> "apiShowMyAddress"
|
||||||
@@ -3025,9 +3103,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 +3137,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 +3154,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 +3194,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,
|
||||||
@@ -3971,6 +4085,15 @@ val json = Json {
|
|||||||
explicitNulls = false
|
explicitNulls = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Can decode unknown enum to default value specified for this field
|
||||||
|
val jsonCoerceInputValues = Json {
|
||||||
|
prettyPrint = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
encodeDefaults = true
|
||||||
|
explicitNulls = false
|
||||||
|
coerceInputValues = true
|
||||||
|
}
|
||||||
|
|
||||||
val jsonShort = Json {
|
val jsonShort = Json {
|
||||||
prettyPrint = false
|
prettyPrint = false
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
@@ -3981,6 +4104,8 @@ val jsonShort = Json {
|
|||||||
val yaml = Yaml(configuration = YamlConfiguration(
|
val yaml = Yaml(configuration = YamlConfiguration(
|
||||||
strictMode = false,
|
strictMode = false,
|
||||||
encodeDefaults = false,
|
encodeDefaults = false,
|
||||||
|
/** ~5.5 MB limitation since wallpaper is limited by 5 MB, see [saveWallpaperFile] */
|
||||||
|
codePointLimit = 5500000,
|
||||||
))
|
))
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -4175,7 +4300,7 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("rcvStandaloneFileComplete") class RcvStandaloneFileComplete(val user: UserRef, val targetPath: String, val rcvFileTransfer: RcvFileTransfer): CR()
|
@Serializable @SerialName("rcvStandaloneFileComplete") class RcvStandaloneFileComplete(val user: UserRef, val targetPath: String, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||||
@Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR()
|
@Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||||
@Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
|
@Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||||
@Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR()
|
@Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem_: AChatItem?, val agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||||
// sending file events
|
// sending file events
|
||||||
@Serializable @SerialName("sndFileStart") class SndFileStart(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
@Serializable @SerialName("sndFileStart") class SndFileStart(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
||||||
@Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
@Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
|
||||||
@@ -4197,6 +4322,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 +4487,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"
|
||||||
@@ -4501,7 +4628,7 @@ sealed class CR {
|
|||||||
is RcvFileSndCancelled -> withUser(user, json.encodeToString(chatItem))
|
is RcvFileSndCancelled -> withUser(user, json.encodeToString(chatItem))
|
||||||
is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
|
is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
|
||||||
is RcvStandaloneFileComplete -> withUser(user, targetPath)
|
is RcvStandaloneFileComplete -> withUser(user, targetPath)
|
||||||
is RcvFileError -> withUser(user, json.encodeToString(chatItem_))
|
is RcvFileError -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer")
|
||||||
is SndFileCancelled -> json.encodeToString(chatItem_)
|
is SndFileCancelled -> json.encodeToString(chatItem_)
|
||||||
is SndStandaloneFileCreated -> noDetails()
|
is SndStandaloneFileCreated -> noDetails()
|
||||||
is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem))
|
is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem))
|
||||||
@@ -4521,6 +4648,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 +4952,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 +5039,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 +5538,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,
|
||||||
@@ -5425,12 +5556,18 @@ data class AppSettings(
|
|||||||
var androidCallOnLockScreen: AppSettingsLockScreenCalls? = null,
|
var androidCallOnLockScreen: AppSettingsLockScreenCalls? = null,
|
||||||
var iosCallKitEnabled: Boolean? = null,
|
var iosCallKitEnabled: Boolean? = null,
|
||||||
var iosCallKitCallsInRecents: Boolean? = null,
|
var iosCallKitCallsInRecents: Boolean? = null,
|
||||||
|
var uiProfileImageCornerRadius: Float? = null,
|
||||||
|
var uiColorScheme: String? = null,
|
||||||
|
var uiDarkColorScheme: String? = null,
|
||||||
|
var uiCurrentThemeIds: Map<String, String>? = null,
|
||||||
|
var uiThemes: List<ThemeOverrides>? = null,
|
||||||
) {
|
) {
|
||||||
fun prepareForExport(): AppSettings {
|
fun prepareForExport(): AppSettings {
|
||||||
val empty = AppSettings()
|
val empty = 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 }
|
||||||
@@ -5448,6 +5585,11 @@ data class AppSettings(
|
|||||||
if (androidCallOnLockScreen != def.androidCallOnLockScreen) { empty.androidCallOnLockScreen = androidCallOnLockScreen }
|
if (androidCallOnLockScreen != def.androidCallOnLockScreen) { empty.androidCallOnLockScreen = androidCallOnLockScreen }
|
||||||
if (iosCallKitEnabled != def.iosCallKitEnabled) { empty.iosCallKitEnabled = iosCallKitEnabled }
|
if (iosCallKitEnabled != def.iosCallKitEnabled) { empty.iosCallKitEnabled = iosCallKitEnabled }
|
||||||
if (iosCallKitCallsInRecents != def.iosCallKitCallsInRecents) { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents }
|
if (iosCallKitCallsInRecents != def.iosCallKitCallsInRecents) { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents }
|
||||||
|
if (uiProfileImageCornerRadius != def.uiProfileImageCornerRadius) { empty.uiProfileImageCornerRadius = uiProfileImageCornerRadius }
|
||||||
|
if (uiColorScheme != def.uiColorScheme) { empty.uiColorScheme = uiColorScheme }
|
||||||
|
if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme }
|
||||||
|
if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds }
|
||||||
|
if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes }
|
||||||
return empty
|
return empty
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5462,6 +5604,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) }
|
||||||
@@ -5479,6 +5622,11 @@ data class AppSettings(
|
|||||||
androidCallOnLockScreen?.let { def.callOnLockScreen.set(it.toCallOnLockScreen()) }
|
androidCallOnLockScreen?.let { def.callOnLockScreen.set(it.toCallOnLockScreen()) }
|
||||||
iosCallKitEnabled?.let { def.iosCallKitEnabled.set(it) }
|
iosCallKitEnabled?.let { def.iosCallKitEnabled.set(it) }
|
||||||
iosCallKitCallsInRecents?.let { def.iosCallKitCallsInRecents.set(it) }
|
iosCallKitCallsInRecents?.let { def.iosCallKitCallsInRecents.set(it) }
|
||||||
|
uiProfileImageCornerRadius?.let { def.profileImageCornerRadius.set(it) }
|
||||||
|
uiColorScheme?.let { def.currentTheme.set(it) }
|
||||||
|
uiDarkColorScheme?.let { def.systemDarkTheme.set(it) }
|
||||||
|
uiCurrentThemeIds?.let { def.currentThemeIds.set(it) }
|
||||||
|
uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -5486,6 +5634,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,
|
||||||
@@ -5502,7 +5651,12 @@ data class AppSettings(
|
|||||||
confirmDBUpgrades = false,
|
confirmDBUpgrades = false,
|
||||||
androidCallOnLockScreen = AppSettingsLockScreenCalls.SHOW,
|
androidCallOnLockScreen = AppSettingsLockScreenCalls.SHOW,
|
||||||
iosCallKitEnabled = true,
|
iosCallKitEnabled = true,
|
||||||
iosCallKitCallsInRecents = false
|
iosCallKitCallsInRecents = false,
|
||||||
|
uiProfileImageCornerRadius = 22.5f,
|
||||||
|
uiColorScheme = DefaultTheme.SYSTEM_THEME_NAME,
|
||||||
|
uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName,
|
||||||
|
uiCurrentThemeIds = null,
|
||||||
|
uiThemes = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
val current: AppSettings
|
val current: AppSettings
|
||||||
@@ -5511,6 +5665,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(),
|
||||||
@@ -5528,6 +5683,11 @@ data class AppSettings(
|
|||||||
androidCallOnLockScreen = AppSettingsLockScreenCalls.from(def.callOnLockScreen.get()),
|
androidCallOnLockScreen = AppSettingsLockScreenCalls.from(def.callOnLockScreen.get()),
|
||||||
iosCallKitEnabled = def.iosCallKitEnabled.get(),
|
iosCallKitEnabled = def.iosCallKitEnabled.get(),
|
||||||
iosCallKitCallsInRecents = def.iosCallKitCallsInRecents.get(),
|
iosCallKitCallsInRecents = def.iosCallKitCallsInRecents.get(),
|
||||||
|
uiProfileImageCornerRadius = def.profileImageCornerRadius.get(),
|
||||||
|
uiColorScheme = def.currentTheme.get() ?: DefaultTheme.SYSTEM_THEME_NAME,
|
||||||
|
uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName,
|
||||||
|
uiCurrentThemeIds = def.currentThemeIds.get(),
|
||||||
|
uiThemes = def.themeOverrides.get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,6 @@ package com.sd.lib.compose.wheel_picker
|
|||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import chat.simplex.common.ui.theme.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -12,6 +11,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import chat.simplex.common.ui.theme.isInDarkTheme
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default implementation of focus view in vertical.
|
* The default implementation of focus view in vertical.
|
||||||
@@ -76,7 +76,7 @@ fun FWheelPickerFocusHorizontal(
|
|||||||
*/
|
*/
|
||||||
private val DefaultDividerColor: Color
|
private val DefaultDividerColor: Color
|
||||||
@Composable
|
@Composable
|
||||||
get() = (if (isSystemInDarkTheme()) {
|
get() = (if (isInDarkTheme()) {
|
||||||
Color.White
|
Color.White
|
||||||
} else {
|
} else {
|
||||||
Color.Black
|
Color.Black
|
||||||
|
|||||||
+6
@@ -42,6 +42,12 @@ fun runMigrations() {
|
|||||||
ChatController.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name)
|
ChatController.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name)
|
||||||
}
|
}
|
||||||
lastMigration.set(117)
|
lastMigration.set(117)
|
||||||
|
} else if (lastMigration.get() < 203) {
|
||||||
|
// Moving to a different key for storing themes as a List
|
||||||
|
val oldOverrides = ChatController.appPrefs.themeOverridesOld.get().values.toList()
|
||||||
|
ChatController.appPrefs.themeOverrides.set(oldOverrides)
|
||||||
|
ChatController.appPrefs.currentThemeIds.set(oldOverrides.associate { it.base.themeName to it.themeId })
|
||||||
|
lastMigration.set(203)
|
||||||
} else {
|
} else {
|
||||||
lastMigration.set(BuildConfigCommon.ANDROID_VERSION_CODE)
|
lastMigration.set(BuildConfigCommon.ANDROID_VERSION_CODE)
|
||||||
break
|
break
|
||||||
|
|||||||
+8
-7
@@ -43,14 +43,13 @@ val appPreferences: AppPreferences
|
|||||||
|
|
||||||
val chatController: ChatController = ChatController
|
val chatController: ChatController = ChatController
|
||||||
|
|
||||||
fun initChatControllerAndRunMigrations() {
|
fun initChatControllerOnStart() {
|
||||||
withLongRunningApi {
|
withLongRunningApi {
|
||||||
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
|
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
|
||||||
initChatController(startChat = ::showStartChatAfterRestartAlert)
|
initChatController(startChat = ::showStartChatAfterRestartAlert)
|
||||||
} else {
|
} else {
|
||||||
initChatController()
|
initChatController()
|
||||||
}
|
}
|
||||||
runMigrations()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,11 +87,13 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
platform.androidRestartNetworkObserver()
|
platform.androidRestartNetworkObserver()
|
||||||
controller.apiSetTempFolder(coreTmpDir.absolutePath)
|
controller.apiSetAppFilePaths(
|
||||||
controller.apiSetFilesFolder(appFilesDir.absolutePath)
|
appFilesDir.absolutePath,
|
||||||
if (appPlatform.isDesktop) {
|
coreTmpDir.absolutePath,
|
||||||
controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath)
|
wallpapersDir.parentFile.absolutePath,
|
||||||
}
|
remoteHostsDir.absolutePath,
|
||||||
|
ctrl
|
||||||
|
)
|
||||||
controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get())
|
controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get())
|
||||||
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
||||||
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
||||||
|
|||||||
+56
-2
@@ -1,10 +1,12 @@
|
|||||||
package chat.simplex.common.platform
|
package chat.simplex.common.platform
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import chat.simplex.common.model.CIFile
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.model.CryptoFile
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.generalGetString
|
import chat.simplex.common.views.helpers.generalGetString
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
import com.charleskorn.kaml.*
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
@@ -14,8 +16,10 @@ expect val dataDir: File
|
|||||||
expect val tmpDir: File
|
expect val tmpDir: File
|
||||||
expect val filesDir: File
|
expect val filesDir: File
|
||||||
expect val appFilesDir: File
|
expect val appFilesDir: File
|
||||||
|
expect val wallpapersDir: File
|
||||||
expect val coreTmpDir: File
|
expect val coreTmpDir: File
|
||||||
expect val dbAbsolutePrefixPath: String
|
expect val dbAbsolutePrefixPath: String
|
||||||
|
expect val preferencesDir: File
|
||||||
|
|
||||||
expect val chatDatabaseFileName: String
|
expect val chatDatabaseFileName: String
|
||||||
expect val agentDatabaseFileName: String
|
expect val agentDatabaseFileName: String
|
||||||
@@ -78,6 +82,20 @@ fun getAppFilePath(fileName: String): String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getWallpaperFilePath(fileName: String): String {
|
||||||
|
val rh = chatModel.currentRemoteHost.value
|
||||||
|
val s = File.separator
|
||||||
|
val path = if (rh == null) {
|
||||||
|
wallpapersDir.absolutePath + s + fileName
|
||||||
|
} else {
|
||||||
|
remoteHostsDir.absolutePath + s + rh.storePath + s + "simplex_v1_assets" + s + "wallpapers" + s + fileName
|
||||||
|
}
|
||||||
|
File(path).parentFile.mkdirs()
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPreferenceFilePath(fileName: String = "themes.yaml"): String = preferencesDir.absolutePath + File.separator + fileName
|
||||||
|
|
||||||
fun getLoadedFilePath(file: CIFile?): String? {
|
fun getLoadedFilePath(file: CIFile?): String? {
|
||||||
val f = file?.fileSource?.filePath
|
val f = file?.fileSource?.filePath
|
||||||
return if (f != null && file.loaded) {
|
return if (f != null && file.loaded) {
|
||||||
@@ -98,6 +116,42 @@ fun getLoadedFileSource(file: CIFile?): CryptoFile? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun readThemeOverrides(): List<ThemeOverrides> {
|
||||||
|
return try {
|
||||||
|
val file = File(getPreferenceFilePath("themes.yaml"))
|
||||||
|
if (!file.exists()) return emptyList()
|
||||||
|
|
||||||
|
file.inputStream().use {
|
||||||
|
val map = yaml.parseToYamlNode(it).yamlMap
|
||||||
|
val list = map.get<YamlList>("themes")
|
||||||
|
val res = ArrayList<ThemeOverrides>()
|
||||||
|
list?.items?.forEach {
|
||||||
|
try {
|
||||||
|
res.add(yaml.decodeFromYamlNode(ThemeOverrides.serializer(), it))
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "Error while reading specific theme: ${e.stackTraceToString()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.skipDuplicates()
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "Error while reading themes file: ${e.stackTraceToString()}")
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeThemeOverrides(overrides: List<ThemeOverrides>): Boolean =
|
||||||
|
try {
|
||||||
|
File(getPreferenceFilePath("themes.yaml")).outputStream().use {
|
||||||
|
val string = yaml.encodeToString(ThemesFile(themes = overrides))
|
||||||
|
it.bufferedWriter().use { it.write(string) }
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "Error while writing themes file: ${e.stackTraceToString()}")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
private fun fileReady(file: CIFile, filePath: String) =
|
private fun fileReady(file: CIFile, filePath: String) =
|
||||||
File(filePath).exists() &&
|
File(filePath).exists() &&
|
||||||
CIFile.cachedRemoteFileRequests[file.fileSource] != false
|
CIFile.cachedRemoteFileRequests[file.fileSource] != false
|
||||||
|
|||||||
+4
@@ -1,11 +1,13 @@
|
|||||||
package chat.simplex.common.platform
|
package chat.simplex.common.platform
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
|
import dev.icerock.moko.resources.ImageResource
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -31,3 +33,5 @@ expect fun windowWidth(): Dp
|
|||||||
expect fun desktopExpandWindowToWidth(width: Dp)
|
expect fun desktopExpandWindowToWidth(width: Dp)
|
||||||
|
|
||||||
expect fun isRtl(text: CharSequence): Boolean
|
expect fun isRtl(text: CharSequence): Boolean
|
||||||
|
|
||||||
|
expect fun ImageResource.toComposeImageBitmap(): ImageBitmap?
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ expect fun UriHandler.sendEmail(subject: String, body: CharSequence)
|
|||||||
|
|
||||||
expect fun ClipboardManager.shareText(text: String)
|
expect fun ClipboardManager.shareText(text: String)
|
||||||
expect fun shareFile(text: String, fileSource: CryptoFile)
|
expect fun shareFile(text: String, fileSource: CryptoFile)
|
||||||
|
expect fun openFile(fileSource: CryptoFile)
|
||||||
|
|||||||
+1
-12
@@ -31,17 +31,6 @@ val WarningOrange = Color(255, 127, 0, 255)
|
|||||||
val WarningYellow = Color(255, 192, 0, 255)
|
val WarningYellow = Color(255, 192, 0, 255)
|
||||||
val FileLight = Color(183, 190, 199, 255)
|
val FileLight = Color(183, 190, 199, 255)
|
||||||
val FileDark = Color(101, 101, 106, 255)
|
val FileDark = Color(101, 101, 106, 255)
|
||||||
val SentMessageColor = Color(0x1E45B8FF)
|
|
||||||
|
|
||||||
val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black
|
val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black
|
||||||
val NoteFolderIconColor: Color @Composable get() = with(CurrentColors.collectAsState().value.appColors.sentMessage) {
|
val NoteFolderIconColor: Color @Composable get() = MaterialTheme.appColors.primaryVariant2
|
||||||
// Default color looks too light and better to have it here a little bit brighter
|
|
||||||
if (alpha == SentMessageColor.alpha) {
|
|
||||||
copy(min(SentMessageColor.alpha + 0.1f, 1f))
|
|
||||||
} else {
|
|
||||||
// Color is non-standard and theme maker can choose color without alpha at all since the theme bound to dark/light variant,
|
|
||||||
// and it shouldn't be universal
|
|
||||||
this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
+607
-76
@@ -6,53 +6,136 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.model.ChatController
|
import chat.simplex.common.model.ChatController
|
||||||
import chat.simplex.common.platform.isInNightMode
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
|
||||||
|
import chat.simplex.common.ui.theme.ThemeManager.toReadableHex
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
import kotlinx.serialization.Transient
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
enum class DefaultTheme {
|
enum class DefaultTheme {
|
||||||
SYSTEM, LIGHT, DARK, SIMPLEX;
|
LIGHT, DARK, SIMPLEX, BLACK;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SYSTEM_THEME_NAME: String = "SYSTEM"
|
||||||
|
}
|
||||||
|
|
||||||
|
val themeName: String
|
||||||
|
get() = name
|
||||||
|
|
||||||
|
val mode: DefaultThemeMode get() = if (this == LIGHT) DefaultThemeMode.LIGHT else DefaultThemeMode.DARK
|
||||||
|
|
||||||
// Call it only with base theme, not SYSTEM
|
// Call it only with base theme, not SYSTEM
|
||||||
fun hasChangedAnyColor(colors: Colors, appColors: AppColors): Boolean {
|
fun hasChangedAnyColor(overrides: ThemeOverrides?): Boolean {
|
||||||
val palette = when (this) {
|
if (overrides == null) return false
|
||||||
SYSTEM -> return false
|
return overrides.colors != ThemeColors() ||
|
||||||
LIGHT -> LightColorPalette
|
overrides.wallpaper != null && (overrides.wallpaper.background != null || overrides.wallpaper.tint != null)
|
||||||
DARK -> DarkColorPalette
|
|
||||||
SIMPLEX -> SimplexColorPalette
|
|
||||||
}
|
|
||||||
val appPalette = when (this) {
|
|
||||||
SYSTEM -> return false
|
|
||||||
LIGHT -> LightColorPaletteApp
|
|
||||||
DARK -> DarkColorPaletteApp
|
|
||||||
SIMPLEX -> SimplexColorPaletteApp
|
|
||||||
}
|
|
||||||
return colors.primary != palette.primary ||
|
|
||||||
colors.primaryVariant != palette.primaryVariant ||
|
|
||||||
colors.secondary != palette.secondary ||
|
|
||||||
colors.secondaryVariant != palette.secondaryVariant ||
|
|
||||||
colors.background != palette.background ||
|
|
||||||
colors.surface != palette.surface ||
|
|
||||||
appColors != appPalette
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AppColors(
|
@Serializable
|
||||||
val title: Color,
|
enum class DefaultThemeMode {
|
||||||
val sentMessage: Color,
|
@SerialName("light") LIGHT,
|
||||||
val receivedMessage: Color
|
@SerialName("dark") DARK
|
||||||
)
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class AppColors(
|
||||||
|
title: Color,
|
||||||
|
primaryVariant2: Color,
|
||||||
|
sentMessage: Color,
|
||||||
|
sentQuote: Color,
|
||||||
|
receivedMessage: Color,
|
||||||
|
receivedQuote: Color,
|
||||||
|
) {
|
||||||
|
var title by mutableStateOf(title, structuralEqualityPolicy())
|
||||||
|
internal set
|
||||||
|
var primaryVariant2 by mutableStateOf(primaryVariant2, structuralEqualityPolicy())
|
||||||
|
internal set
|
||||||
|
var sentMessage by mutableStateOf(sentMessage, structuralEqualityPolicy())
|
||||||
|
internal set
|
||||||
|
var sentQuote by mutableStateOf(sentQuote, structuralEqualityPolicy())
|
||||||
|
internal set
|
||||||
|
var receivedMessage by mutableStateOf(receivedMessage, structuralEqualityPolicy())
|
||||||
|
internal set
|
||||||
|
var receivedQuote by mutableStateOf(receivedQuote, structuralEqualityPolicy())
|
||||||
|
internal set
|
||||||
|
|
||||||
|
fun copy(
|
||||||
|
title: Color = this.title,
|
||||||
|
primaryVariant2: Color = this.primaryVariant2,
|
||||||
|
sentMessage: Color = this.sentMessage,
|
||||||
|
sentQuote: Color = this.sentQuote,
|
||||||
|
receivedMessage: Color = this.receivedMessage,
|
||||||
|
receivedQuote: Color = this.receivedQuote,
|
||||||
|
): AppColors = AppColors(
|
||||||
|
title,
|
||||||
|
primaryVariant2,
|
||||||
|
sentMessage,
|
||||||
|
sentQuote,
|
||||||
|
receivedMessage,
|
||||||
|
receivedQuote,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return buildString {
|
||||||
|
append("AppColors(")
|
||||||
|
append("title=$title, ")
|
||||||
|
append("primaryVariant2=$primaryVariant2, ")
|
||||||
|
append("sentMessage=$sentMessage, ")
|
||||||
|
append("sentQuote=$sentQuote, ")
|
||||||
|
append("receivedMessage=$receivedMessage, ")
|
||||||
|
append("receivedQuote=$receivedQuote")
|
||||||
|
append(")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class AppWallpaper(
|
||||||
|
background: Color? = null,
|
||||||
|
tint: Color? = null,
|
||||||
|
type: WallpaperType = WallpaperType.Empty,
|
||||||
|
) {
|
||||||
|
var background by mutableStateOf(background, structuralEqualityPolicy())
|
||||||
|
internal set
|
||||||
|
var tint by mutableStateOf(tint, structuralEqualityPolicy())
|
||||||
|
internal set
|
||||||
|
var type by mutableStateOf(type, structuralEqualityPolicy())
|
||||||
|
internal set
|
||||||
|
|
||||||
|
fun copy(
|
||||||
|
background: Color? = this.background,
|
||||||
|
tint: Color? = this.tint,
|
||||||
|
type: WallpaperType = this.type,
|
||||||
|
): AppWallpaper = AppWallpaper(
|
||||||
|
background,
|
||||||
|
tint,
|
||||||
|
type,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return buildString {
|
||||||
|
append("AppWallpaper(")
|
||||||
|
append("background=$background, ")
|
||||||
|
append("tint=$tint, ")
|
||||||
|
append("type=$type")
|
||||||
|
append(")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum class ThemeColor {
|
enum class ThemeColor {
|
||||||
PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, RECEIVED_MESSAGE;
|
PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, SENT_QUOTE, RECEIVED_MESSAGE, RECEIVED_QUOTE, PRIMARY_VARIANT2, WALLPAPER_BACKGROUND, WALLPAPER_TINT;
|
||||||
|
|
||||||
fun fromColors(colors: Colors, appColors: AppColors): Color {
|
fun fromColors(colors: Colors, appColors: AppColors, appWallpaper: AppWallpaper): Color? {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
PRIMARY -> colors.primary
|
PRIMARY -> colors.primary
|
||||||
PRIMARY_VARIANT -> colors.primaryVariant
|
PRIMARY_VARIANT -> colors.primaryVariant
|
||||||
@@ -61,8 +144,13 @@ enum class ThemeColor {
|
|||||||
BACKGROUND -> colors.background
|
BACKGROUND -> colors.background
|
||||||
SURFACE -> colors.surface
|
SURFACE -> colors.surface
|
||||||
TITLE -> appColors.title
|
TITLE -> appColors.title
|
||||||
|
PRIMARY_VARIANT2 -> appColors.primaryVariant2
|
||||||
SENT_MESSAGE -> appColors.sentMessage
|
SENT_MESSAGE -> appColors.sentMessage
|
||||||
|
SENT_QUOTE -> appColors.sentQuote
|
||||||
RECEIVED_MESSAGE -> appColors.receivedMessage
|
RECEIVED_MESSAGE -> appColors.receivedMessage
|
||||||
|
RECEIVED_QUOTE -> appColors.receivedQuote
|
||||||
|
WALLPAPER_BACKGROUND -> appWallpaper.background
|
||||||
|
WALLPAPER_TINT -> appWallpaper.tint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,8 +163,13 @@ enum class ThemeColor {
|
|||||||
BACKGROUND -> generalGetString(MR.strings.color_background)
|
BACKGROUND -> generalGetString(MR.strings.color_background)
|
||||||
SURFACE -> generalGetString(MR.strings.color_surface)
|
SURFACE -> generalGetString(MR.strings.color_surface)
|
||||||
TITLE -> generalGetString(MR.strings.color_title)
|
TITLE -> generalGetString(MR.strings.color_title)
|
||||||
|
PRIMARY_VARIANT2 -> generalGetString(MR.strings.color_primary_variant2)
|
||||||
SENT_MESSAGE -> generalGetString(MR.strings.color_sent_message)
|
SENT_MESSAGE -> generalGetString(MR.strings.color_sent_message)
|
||||||
|
SENT_QUOTE -> generalGetString(MR.strings.color_sent_quote)
|
||||||
RECEIVED_MESSAGE -> generalGetString(MR.strings.color_received_message)
|
RECEIVED_MESSAGE -> generalGetString(MR.strings.color_received_message)
|
||||||
|
RECEIVED_QUOTE -> generalGetString(MR.strings.color_received_quote)
|
||||||
|
WALLPAPER_BACKGROUND -> generalGetString(MR.strings.color_wallpaper_background)
|
||||||
|
WALLPAPER_TINT -> generalGetString(MR.strings.color_wallpaper_tint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,45 +185,232 @@ data class ThemeColors(
|
|||||||
@SerialName("menus")
|
@SerialName("menus")
|
||||||
val surface: String? = null,
|
val surface: String? = null,
|
||||||
val title: String? = null,
|
val title: String? = null,
|
||||||
|
@SerialName("accentVariant2")
|
||||||
|
val primaryVariant2: String? = null,
|
||||||
val sentMessage: String? = null,
|
val sentMessage: String? = null,
|
||||||
|
@SerialName("sentReply")
|
||||||
|
val sentQuote: String? = null,
|
||||||
val receivedMessage: String? = null,
|
val receivedMessage: String? = null,
|
||||||
|
@SerialName("receivedReply")
|
||||||
|
val receivedQuote: String? = null,
|
||||||
) {
|
) {
|
||||||
fun toColors(base: DefaultTheme): Colors {
|
companion object {
|
||||||
|
fun from(colors: Colors, appColors: AppColors): ThemeColors =
|
||||||
|
ThemeColors(
|
||||||
|
primary = colors.primary.toReadableHex(),
|
||||||
|
primaryVariant = colors.primaryVariant.toReadableHex(),
|
||||||
|
secondary = colors.secondary.toReadableHex(),
|
||||||
|
secondaryVariant = colors.secondaryVariant.toReadableHex(),
|
||||||
|
background = colors.background.toReadableHex(),
|
||||||
|
surface = colors.surface.toReadableHex(),
|
||||||
|
title = appColors.title.toReadableHex(),
|
||||||
|
primaryVariant2 = appColors.primaryVariant2.toReadableHex(),
|
||||||
|
sentMessage = appColors.sentMessage.toReadableHex(),
|
||||||
|
sentQuote = appColors.sentQuote.toReadableHex(),
|
||||||
|
receivedMessage = appColors.receivedMessage.toReadableHex(),
|
||||||
|
receivedQuote = appColors.receivedQuote.toReadableHex(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ThemeWallpaper (
|
||||||
|
val preset: String? = null,
|
||||||
|
val scale: Float? = null,
|
||||||
|
val scaleType: WallpaperScaleType? = null,
|
||||||
|
val background: String? = null,
|
||||||
|
val tint: String? = null,
|
||||||
|
val image: String? = null,
|
||||||
|
val imageFile: String? = null,
|
||||||
|
) {
|
||||||
|
fun toAppWallpaper(): AppWallpaper {
|
||||||
|
return AppWallpaper(
|
||||||
|
background = background?.colorFromReadableHex(),
|
||||||
|
tint = tint?.colorFromReadableHex(),
|
||||||
|
type = WallpaperType.from(this) ?: WallpaperType.Empty
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withFilledWallpaperBase64(): ThemeWallpaper {
|
||||||
|
val aw = toAppWallpaper()
|
||||||
|
val type = aw.type
|
||||||
|
return ThemeWallpaper(
|
||||||
|
image = if (type is WallpaperType.Image && type.image != null) resizeImageToStrSize(type.image!!, 5_000_000) else null,
|
||||||
|
imageFile = null,
|
||||||
|
preset = if (type is WallpaperType.Preset) type.filename else null,
|
||||||
|
scale = if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) type.scale else 1f,
|
||||||
|
scaleType = if (type is WallpaperType.Image) type.scaleType else null,
|
||||||
|
background = aw.background?.toReadableHex(),
|
||||||
|
tint = aw.tint?.toReadableHex(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withFilledWallpaperPath(): ThemeWallpaper {
|
||||||
|
val aw = toAppWallpaper()
|
||||||
|
val type = aw.type
|
||||||
|
return ThemeWallpaper(
|
||||||
|
image = null,
|
||||||
|
imageFile = if (type is WallpaperType.Image) type.filename else null,
|
||||||
|
preset = if (type is WallpaperType.Preset) type.filename else null,
|
||||||
|
scale = if (scale == null) null else if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) scale else null,
|
||||||
|
scaleType = if (scaleType == null) null else if (type is WallpaperType.Image) type.scaleType else null,
|
||||||
|
background = aw.background?.toReadableHex(),
|
||||||
|
tint = aw.tint?.toReadableHex(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun importFromString(): ThemeWallpaper =
|
||||||
|
if (preset == null && image != null) {
|
||||||
|
// Need to save image from string and to save its path
|
||||||
|
try {
|
||||||
|
val parsed = base64ToBitmap(image)
|
||||||
|
val filename = saveWallpaperFile(parsed)
|
||||||
|
copy(image = null, imageFile = filename)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error while parsing/copying the image: ${e.stackTraceToString()}")
|
||||||
|
ThemeWallpaper()
|
||||||
|
}
|
||||||
|
} else this
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(type: WallpaperType, background: String?, tint: String?): ThemeWallpaper {
|
||||||
|
return ThemeWallpaper(
|
||||||
|
image = null,
|
||||||
|
imageFile = if (type is WallpaperType.Image) type.filename else null,
|
||||||
|
preset = if (type is WallpaperType.Preset) type.filename else null,
|
||||||
|
scale = if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) type.scale else null,
|
||||||
|
scaleType = if (type is WallpaperType.Image) type.scaleType else null,
|
||||||
|
background = background,
|
||||||
|
tint = tint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ThemesFile(
|
||||||
|
val themes: List<ThemeOverrides> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ThemeOverrides (
|
||||||
|
val themeId: String = UUID.randomUUID().toString(),
|
||||||
|
val base: DefaultTheme,
|
||||||
|
val colors: ThemeColors = ThemeColors(),
|
||||||
|
val wallpaper: ThemeWallpaper? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun isSame(type: WallpaperType?, themeName: String): Boolean =
|
||||||
|
(
|
||||||
|
(wallpaper?.preset != null && type is WallpaperType.Preset && wallpaper.preset == type.filename) ||
|
||||||
|
(wallpaper?.imageFile != null && type is WallpaperType.Image) ||
|
||||||
|
(wallpaper?.preset == null && wallpaper?.imageFile == null && (type == WallpaperType.Empty || type == null))
|
||||||
|
) && base.themeName == themeName
|
||||||
|
|
||||||
|
fun withUpdatedColor(name: ThemeColor, color: String?): ThemeOverrides {
|
||||||
|
return copy(
|
||||||
|
colors = when (name) {
|
||||||
|
ThemeColor.PRIMARY -> colors.copy(primary = color)
|
||||||
|
ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color)
|
||||||
|
ThemeColor.SECONDARY -> colors.copy(secondary = color)
|
||||||
|
ThemeColor.SECONDARY_VARIANT -> colors.copy(secondaryVariant = color)
|
||||||
|
ThemeColor.BACKGROUND -> colors.copy(background = color)
|
||||||
|
ThemeColor.SURFACE -> colors.copy(surface = color)
|
||||||
|
ThemeColor.TITLE -> colors.copy(title = color)
|
||||||
|
ThemeColor.PRIMARY_VARIANT2 -> colors.copy(primaryVariant2 = color)
|
||||||
|
ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color)
|
||||||
|
ThemeColor.SENT_QUOTE -> colors.copy(sentQuote = color)
|
||||||
|
ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color)
|
||||||
|
ThemeColor.RECEIVED_QUOTE -> colors.copy(receivedQuote = color)
|
||||||
|
ThemeColor.WALLPAPER_BACKGROUND -> colors.copy()
|
||||||
|
ThemeColor.WALLPAPER_TINT -> colors.copy()
|
||||||
|
}, wallpaper = when (name) {
|
||||||
|
ThemeColor.WALLPAPER_BACKGROUND -> wallpaper?.copy(background = color)
|
||||||
|
ThemeColor.WALLPAPER_TINT -> wallpaper?.copy(tint = color)
|
||||||
|
else -> wallpaper?.copy()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toColors(base: DefaultTheme, perChatTheme: ThemeColors?, perUserTheme: ThemeColors?, presetWallpaperTheme: ThemeColors?): Colors {
|
||||||
val baseColors = when (base) {
|
val baseColors = when (base) {
|
||||||
DefaultTheme.LIGHT -> LightColorPalette
|
DefaultTheme.LIGHT -> LightColorPalette
|
||||||
DefaultTheme.DARK -> DarkColorPalette
|
DefaultTheme.DARK -> DarkColorPalette
|
||||||
DefaultTheme.SIMPLEX -> SimplexColorPalette
|
DefaultTheme.SIMPLEX -> SimplexColorPalette
|
||||||
// shouldn't be here
|
DefaultTheme.BLACK -> BlackColorPalette
|
||||||
DefaultTheme.SYSTEM -> LightColorPalette
|
|
||||||
}
|
}
|
||||||
return baseColors.copy(
|
return baseColors.copy(
|
||||||
primary = primary?.colorFromReadableHex() ?: baseColors.primary,
|
primary = perChatTheme?.primary?.colorFromReadableHex() ?: perUserTheme?.primary?.colorFromReadableHex() ?: colors.primary?.colorFromReadableHex() ?: presetWallpaperTheme?.primary?.colorFromReadableHex() ?: baseColors.primary,
|
||||||
primaryVariant = primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant,
|
primaryVariant = perChatTheme?.primaryVariant?.colorFromReadableHex() ?: perUserTheme?.primaryVariant?.colorFromReadableHex() ?: colors.primaryVariant?.colorFromReadableHex() ?: presetWallpaperTheme?.primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant,
|
||||||
secondary = secondary?.colorFromReadableHex() ?: baseColors.secondary,
|
secondary = perChatTheme?.secondary?.colorFromReadableHex() ?: perUserTheme?.secondary?.colorFromReadableHex() ?: colors.secondary?.colorFromReadableHex() ?: presetWallpaperTheme?.secondary?.colorFromReadableHex() ?: baseColors.secondary,
|
||||||
secondaryVariant = secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant,
|
secondaryVariant = perChatTheme?.secondaryVariant?.colorFromReadableHex() ?: perUserTheme?.secondaryVariant?.colorFromReadableHex() ?: colors.secondaryVariant?.colorFromReadableHex() ?: presetWallpaperTheme?.secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant,
|
||||||
background = background?.colorFromReadableHex() ?: baseColors.background,
|
background = perChatTheme?.background?.colorFromReadableHex() ?: perUserTheme?.background?.colorFromReadableHex() ?: colors.background?.colorFromReadableHex() ?: presetWallpaperTheme?.background?.colorFromReadableHex() ?: baseColors.background,
|
||||||
surface = surface?.colorFromReadableHex() ?: baseColors.surface,
|
surface = perChatTheme?.surface?.colorFromReadableHex() ?: perUserTheme?.surface?.colorFromReadableHex() ?: colors.surface?.colorFromReadableHex() ?: presetWallpaperTheme?.surface?.colorFromReadableHex() ?: baseColors.surface,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toAppColors(base: DefaultTheme): AppColors {
|
fun toAppColors(base: DefaultTheme, perChatTheme: ThemeColors?, perChatWallpaperType: WallpaperType?, perUserTheme: ThemeColors?, perUserWallpaperType: WallpaperType?, presetWallpaperTheme: ThemeColors?): AppColors {
|
||||||
val baseColors = when (base) {
|
val baseColors = when (base) {
|
||||||
DefaultTheme.LIGHT -> LightColorPaletteApp
|
DefaultTheme.LIGHT -> LightColorPaletteApp
|
||||||
DefaultTheme.DARK -> DarkColorPaletteApp
|
DefaultTheme.DARK -> DarkColorPaletteApp
|
||||||
DefaultTheme.SIMPLEX -> SimplexColorPaletteApp
|
DefaultTheme.SIMPLEX -> SimplexColorPaletteApp
|
||||||
// shouldn't be here
|
DefaultTheme.BLACK -> BlackColorPaletteApp
|
||||||
DefaultTheme.SYSTEM -> LightColorPaletteApp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sentMessageFallback = colors.sentMessage?.colorFromReadableHex() ?: presetWallpaperTheme?.sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage
|
||||||
|
val sentQuoteFallback = colors.sentQuote?.colorFromReadableHex() ?: presetWallpaperTheme?.sentQuote?.colorFromReadableHex() ?: baseColors.sentQuote
|
||||||
|
val receivedMessageFallback = colors.receivedMessage?.colorFromReadableHex() ?: presetWallpaperTheme?.receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage
|
||||||
|
val receivedQuoteFallback = colors.receivedQuote?.colorFromReadableHex() ?: presetWallpaperTheme?.receivedQuote?.colorFromReadableHex() ?: baseColors.receivedQuote
|
||||||
return baseColors.copy(
|
return baseColors.copy(
|
||||||
title = title?.colorFromReadableHex() ?: baseColors.title,
|
title = perChatTheme?.title?.colorFromReadableHex() ?: perUserTheme?.title?.colorFromReadableHex() ?: colors.title?.colorFromReadableHex() ?: presetWallpaperTheme?.title?.colorFromReadableHex() ?: baseColors.title,
|
||||||
sentMessage = sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage,
|
primaryVariant2 = perChatTheme?.primaryVariant2?.colorFromReadableHex() ?: perUserTheme?.primaryVariant2?.colorFromReadableHex() ?: colors.primaryVariant2?.colorFromReadableHex() ?: presetWallpaperTheme?.primaryVariant2?.colorFromReadableHex() ?: baseColors.primaryVariant2,
|
||||||
receivedMessage = receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage,
|
sentMessage = if (perChatTheme?.sentMessage != null) perChatTheme.sentMessage.colorFromReadableHex()
|
||||||
|
else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.sentMessage?.colorFromReadableHex() ?: sentMessageFallback
|
||||||
|
else sentMessageFallback,
|
||||||
|
sentQuote = if (perChatTheme?.sentQuote != null) perChatTheme.sentQuote.colorFromReadableHex()
|
||||||
|
else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.sentQuote?.colorFromReadableHex() ?: sentQuoteFallback
|
||||||
|
else sentQuoteFallback,
|
||||||
|
receivedMessage = if (perChatTheme?.receivedMessage != null) perChatTheme.receivedMessage.colorFromReadableHex()
|
||||||
|
else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.receivedMessage?.colorFromReadableHex() ?: receivedMessageFallback
|
||||||
|
else receivedMessageFallback,
|
||||||
|
receivedQuote = if (perChatTheme?.receivedQuote != null) perChatTheme.receivedQuote.colorFromReadableHex()
|
||||||
|
else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.receivedQuote?.colorFromReadableHex() ?: receivedQuoteFallback
|
||||||
|
else receivedQuoteFallback,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withFilledColors(base: DefaultTheme): ThemeColors {
|
fun toAppWallpaper(themeOverridesForType: WallpaperType?, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverride?, materialBackgroundColor: Color): AppWallpaper {
|
||||||
val c = toColors(base)
|
val mainType = when {
|
||||||
val ac = toAppColors(base)
|
themeOverridesForType != null -> themeOverridesForType
|
||||||
|
// type can be null if override is empty `"wallpaper": "{}"`, in this case no wallpaper is needed, empty.
|
||||||
|
// It's not null to override upper level wallpaper
|
||||||
|
perChatTheme?.wallpaper != null -> perChatTheme.wallpaper.toAppWallpaper().type
|
||||||
|
perUserTheme?.wallpaper != null -> perUserTheme.wallpaper.toAppWallpaper().type
|
||||||
|
else -> wallpaper?.toAppWallpaper()?.type ?: return AppWallpaper()
|
||||||
|
}
|
||||||
|
val first: ThemeWallpaper? = if (mainType.sameType(perChatTheme?.wallpaper?.toAppWallpaper()?.type)) perChatTheme?.wallpaper else null
|
||||||
|
val second: ThemeWallpaper? = if (mainType.sameType(perUserTheme?.wallpaper?.toAppWallpaper()?.type)) perUserTheme?.wallpaper else null
|
||||||
|
val third: ThemeWallpaper? = if (mainType.sameType(this.wallpaper?.toAppWallpaper()?.type)) this.wallpaper else null
|
||||||
|
|
||||||
|
return AppWallpaper(type = when (mainType) {
|
||||||
|
is WallpaperType.Preset -> mainType.copy(
|
||||||
|
scale = mainType.scale ?: first?.scale ?: second?.scale ?: third?.scale
|
||||||
|
)
|
||||||
|
is WallpaperType.Image -> mainType.copy(
|
||||||
|
scale = if (themeOverridesForType == null) mainType.scale ?: first?.scale ?: second?.scale ?: third?.scale else second?.scale ?: third?.scale ?: mainType.scale,
|
||||||
|
scaleType = if (themeOverridesForType == null) mainType.scaleType ?: first?.scaleType ?: second?.scaleType ?: third?.scaleType else second?.scaleType ?: third?.scaleType ?: mainType.scaleType,
|
||||||
|
filename = if (themeOverridesForType == null) mainType.filename else first?.imageFile ?: second?.imageFile ?: third?.imageFile ?: mainType.filename,
|
||||||
|
)
|
||||||
|
is WallpaperType.Empty -> mainType
|
||||||
|
},
|
||||||
|
background = (first?.background ?: second?.background ?: third?.background)?.colorFromReadableHex() ?: mainType.defaultBackgroundColor(base, materialBackgroundColor),
|
||||||
|
tint = (first?.tint ?: second?.tint ?: third?.tint)?.colorFromReadableHex() ?: mainType.defaultTintColor(base)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withFilledColors(base: DefaultTheme, perChatTheme: ThemeColors?, perChatWallpaperType: WallpaperType?, perUserTheme: ThemeColors?, perUserWallpaperType: WallpaperType?, presetWallpaperTheme: ThemeColors?): ThemeColors {
|
||||||
|
val c = toColors(base, perChatTheme, perUserTheme, presetWallpaperTheme)
|
||||||
|
val ac = toAppColors(base, perChatTheme, perChatWallpaperType, perUserTheme, perUserWallpaperType, presetWallpaperTheme)
|
||||||
return ThemeColors(
|
return ThemeColors(
|
||||||
primary = c.primary.toReadableHex(),
|
primary = c.primary.toReadableHex(),
|
||||||
primaryVariant = c.primaryVariant.toReadableHex(),
|
primaryVariant = c.primaryVariant.toReadableHex(),
|
||||||
@@ -139,23 +419,71 @@ data class ThemeColors(
|
|||||||
background = c.background.toReadableHex(),
|
background = c.background.toReadableHex(),
|
||||||
surface = c.surface.toReadableHex(),
|
surface = c.surface.toReadableHex(),
|
||||||
title = ac.title.toReadableHex(),
|
title = ac.title.toReadableHex(),
|
||||||
|
primaryVariant2 = ac.primaryVariant2.toReadableHex(),
|
||||||
sentMessage = ac.sentMessage.toReadableHex(),
|
sentMessage = ac.sentMessage.toReadableHex(),
|
||||||
receivedMessage = ac.receivedMessage.toReadableHex()
|
sentQuote = ac.sentQuote.toReadableHex(),
|
||||||
|
receivedMessage = ac.receivedMessage.toReadableHex(),
|
||||||
|
receivedQuote = ac.receivedQuote.toReadableHex(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.colorFromReadableHex(): Color =
|
fun List<ThemeOverrides>.getTheme(themeId: String?): ThemeOverrides? =
|
||||||
Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong())
|
firstOrNull { it.themeId == themeId }
|
||||||
|
|
||||||
private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb())
|
fun List<ThemeOverrides>.getTheme(themeId: String?, type: WallpaperType?, base: DefaultTheme): ThemeOverrides? =
|
||||||
|
firstOrNull { it.themeId == themeId || it.isSame(type, base.themeName)}
|
||||||
|
|
||||||
|
fun List<ThemeOverrides>.replace(theme: ThemeOverrides): List<ThemeOverrides> {
|
||||||
|
val index = indexOfFirst { it.themeId == theme.themeId ||
|
||||||
|
// prevent situation when two themes has the same type but different theme id (maybe something was changed in prefs by hand)
|
||||||
|
it.isSame(WallpaperType.from(theme.wallpaper), theme.base.themeName)
|
||||||
|
}
|
||||||
|
return if (index != -1) {
|
||||||
|
val a = ArrayList(this)
|
||||||
|
a[index] = theme
|
||||||
|
a
|
||||||
|
} else {
|
||||||
|
this + theme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<ThemeOverrides>.sameTheme(type: WallpaperType?, themeName: String): ThemeOverrides? = firstOrNull { it.isSame(type, themeName) }
|
||||||
|
|
||||||
|
/** See [ThemesTest.testSkipDuplicates] */
|
||||||
|
fun List<ThemeOverrides>.skipDuplicates(): List<ThemeOverrides> {
|
||||||
|
val res = ArrayList<ThemeOverrides>()
|
||||||
|
forEach { theme ->
|
||||||
|
val themeType = WallpaperType.from(theme.wallpaper)
|
||||||
|
if (res.none { it.themeId == theme.themeId || it.isSame(themeType, theme.base.themeName) }) {
|
||||||
|
res.add(theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ThemeOverrides (
|
data class ThemeModeOverrides (
|
||||||
val base: DefaultTheme,
|
val light: ThemeModeOverride? = null,
|
||||||
val colors: ThemeColors
|
val dark: ThemeModeOverride? = null
|
||||||
) {
|
) {
|
||||||
fun withUpdatedColor(name: ThemeColor, color: String): ThemeOverrides {
|
fun preferredMode(darkTheme: Boolean): ThemeModeOverride? = when (darkTheme) {
|
||||||
|
false -> light
|
||||||
|
else -> dark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ThemeModeOverride (
|
||||||
|
val mode: DefaultThemeMode = CurrentColors.value.base.mode,
|
||||||
|
val colors: ThemeColors = ThemeColors(),
|
||||||
|
val wallpaper: ThemeWallpaper? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val type = WallpaperType.from(wallpaper)
|
||||||
|
|
||||||
|
fun withUpdatedColor(name: ThemeColor, color: String?): ThemeModeOverride {
|
||||||
return copy(colors = when (name) {
|
return copy(colors = when (name) {
|
||||||
ThemeColor.PRIMARY -> colors.copy(primary = color)
|
ThemeColor.PRIMARY -> colors.copy(primary = color)
|
||||||
ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color)
|
ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color)
|
||||||
@@ -164,9 +492,97 @@ data class ThemeOverrides (
|
|||||||
ThemeColor.BACKGROUND -> colors.copy(background = color)
|
ThemeColor.BACKGROUND -> colors.copy(background = color)
|
||||||
ThemeColor.SURFACE -> colors.copy(surface = color)
|
ThemeColor.SURFACE -> colors.copy(surface = color)
|
||||||
ThemeColor.TITLE -> colors.copy(title = color)
|
ThemeColor.TITLE -> colors.copy(title = color)
|
||||||
|
ThemeColor.PRIMARY_VARIANT2 -> colors.copy(primaryVariant2 = color)
|
||||||
ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color)
|
ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color)
|
||||||
|
ThemeColor.SENT_QUOTE -> colors.copy(sentQuote = color)
|
||||||
ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color)
|
ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color)
|
||||||
})
|
ThemeColor.RECEIVED_QUOTE -> colors.copy(receivedQuote = color)
|
||||||
|
ThemeColor.WALLPAPER_BACKGROUND -> colors.copy()
|
||||||
|
ThemeColor.WALLPAPER_TINT -> colors.copy()
|
||||||
|
}, wallpaper = when (name) {
|
||||||
|
ThemeColor.WALLPAPER_BACKGROUND -> wallpaper?.copy(background = color)
|
||||||
|
ThemeColor.WALLPAPER_TINT -> wallpaper?.copy(tint = color)
|
||||||
|
else -> wallpaper?.copy()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeSameColors(base: DefaultTheme): ThemeModeOverride {
|
||||||
|
val c = when (base) {
|
||||||
|
DefaultTheme.LIGHT -> LightColorPalette
|
||||||
|
DefaultTheme.DARK -> DarkColorPalette
|
||||||
|
DefaultTheme.SIMPLEX -> SimplexColorPalette
|
||||||
|
DefaultTheme.BLACK -> BlackColorPalette
|
||||||
|
}
|
||||||
|
val ac = when (base) {
|
||||||
|
DefaultTheme.LIGHT -> LightColorPaletteApp
|
||||||
|
DefaultTheme.DARK -> DarkColorPaletteApp
|
||||||
|
DefaultTheme.SIMPLEX -> SimplexColorPaletteApp
|
||||||
|
DefaultTheme.BLACK -> BlackColorPaletteApp
|
||||||
|
}
|
||||||
|
val w = when (val wallpaperType = WallpaperType.from(wallpaper)) {
|
||||||
|
is WallpaperType.Preset -> {
|
||||||
|
val p = PresetWallpaper.from(wallpaperType.filename)
|
||||||
|
ThemeWallpaper(
|
||||||
|
preset = wallpaperType.filename,
|
||||||
|
scale = p?.scale ?: wallpaper?.scale,
|
||||||
|
scaleType = null,
|
||||||
|
background = p?.background?.get(base)?.toReadableHex(),
|
||||||
|
tint = p?.tint?.get(base)?.toReadableHex(),
|
||||||
|
image = null,
|
||||||
|
imageFile = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is WallpaperType.Image -> {
|
||||||
|
ThemeWallpaper(
|
||||||
|
preset = null,
|
||||||
|
scale = null,
|
||||||
|
scaleType = WallpaperScaleType.FILL,
|
||||||
|
background = Color.Transparent.toReadableHex(),
|
||||||
|
tint = Color.Transparent.toReadableHex(),
|
||||||
|
image = null,
|
||||||
|
imageFile = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
ThemeWallpaper()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy(
|
||||||
|
colors = ThemeColors(
|
||||||
|
primary = if (colors.primary?.colorFromReadableHex() != c.primary) colors.primary else null,
|
||||||
|
primaryVariant = if (colors.primaryVariant?.colorFromReadableHex() != c.primaryVariant) colors.primaryVariant else null,
|
||||||
|
secondary = if (colors.secondary?.colorFromReadableHex() != c.secondary) colors.secondary else null,
|
||||||
|
secondaryVariant = if (colors.secondaryVariant?.colorFromReadableHex() != c.secondaryVariant) colors.secondaryVariant else null,
|
||||||
|
background = if (colors.background?.colorFromReadableHex() != c.background) colors.background else null,
|
||||||
|
surface = if (colors.surface?.colorFromReadableHex() != c.surface) colors.surface else null,
|
||||||
|
title = if (colors.title?.colorFromReadableHex() != ac.title) colors.title else null,
|
||||||
|
primaryVariant2 = if (colors.primaryVariant2?.colorFromReadableHex() != ac.primaryVariant2) colors.primary else null,
|
||||||
|
sentMessage = if (colors.sentMessage?.colorFromReadableHex() != ac.sentMessage) colors.sentMessage else null,
|
||||||
|
sentQuote = if (colors.sentQuote?.colorFromReadableHex() != ac.sentQuote) colors.sentQuote else null,
|
||||||
|
receivedMessage = if (colors.receivedMessage?.colorFromReadableHex() != ac.receivedMessage) colors.receivedMessage else null,
|
||||||
|
receivedQuote = if (colors.receivedQuote?.colorFromReadableHex() != ac.receivedQuote) colors.receivedQuote else null,
|
||||||
|
),
|
||||||
|
wallpaper = wallpaper?.copy(
|
||||||
|
preset = wallpaper.preset,
|
||||||
|
scale = if (wallpaper.scale != w.scale) wallpaper.scale else null,
|
||||||
|
scaleType = if (wallpaper.scaleType != w.scaleType) wallpaper.scaleType else null,
|
||||||
|
background = if (wallpaper.background != w.background) wallpaper.background else null,
|
||||||
|
tint = if (wallpaper.tint != w.tint) wallpaper.tint else null,
|
||||||
|
image = wallpaper.image,
|
||||||
|
imageFile = wallpaper.imageFile,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun withFilledAppDefaults(mode: DefaultThemeMode, base: DefaultTheme): ThemeModeOverride =
|
||||||
|
ThemeModeOverride(
|
||||||
|
mode = mode,
|
||||||
|
colors = ThemeOverrides(base = base).withFilledColors(base, null, null, null, null, null),
|
||||||
|
wallpaper = ThemeWallpaper(preset = PresetWallpaper.SCHOOL.filename)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +620,6 @@ val DarkColorPalette = darkColors(
|
|||||||
// background = Color.Black,
|
// background = Color.Black,
|
||||||
surface = Color(0xFF222222),
|
surface = Color(0xFF222222),
|
||||||
// background = Color(0xFF121212),
|
// background = Color(0xFF121212),
|
||||||
// surface = Color(0xFF121212),
|
|
||||||
error = Color.Red,
|
error = Color.Red,
|
||||||
onBackground = Color(0xFFFFFBFA),
|
onBackground = Color(0xFFFFFBFA),
|
||||||
onSurface = Color(0xFFFFFBFA),
|
onSurface = Color(0xFFFFFBFA),
|
||||||
@@ -212,8 +627,11 @@ val DarkColorPalette = darkColors(
|
|||||||
)
|
)
|
||||||
val DarkColorPaletteApp = AppColors(
|
val DarkColorPaletteApp = AppColors(
|
||||||
title = SimplexBlue,
|
title = SimplexBlue,
|
||||||
sentMessage = SentMessageColor,
|
primaryVariant2 = Color(0xFF18262E),
|
||||||
receivedMessage = Color(0x20B1B0B5)
|
sentMessage = Color(0xFF18262E),
|
||||||
|
sentQuote = Color(0xFF1D3847),
|
||||||
|
receivedMessage = Color(0xff262627),
|
||||||
|
receivedQuote = Color(0xff373739),
|
||||||
)
|
)
|
||||||
|
|
||||||
val LightColorPalette = lightColors(
|
val LightColorPalette = lightColors(
|
||||||
@@ -231,8 +649,11 @@ val LightColorPalette = lightColors(
|
|||||||
)
|
)
|
||||||
val LightColorPaletteApp = AppColors(
|
val LightColorPaletteApp = AppColors(
|
||||||
title = SimplexBlue,
|
title = SimplexBlue,
|
||||||
sentMessage = SentMessageColor,
|
primaryVariant2 = Color(0xFFE9F7FF),
|
||||||
receivedMessage = Color(0x20B1B0B5)
|
sentMessage = Color(0xFFE9F7FF),
|
||||||
|
sentQuote = Color(0xFFD6F0FF),
|
||||||
|
receivedMessage = Color(0xfff5f5f6),
|
||||||
|
receivedQuote = Color(0xffececee),
|
||||||
)
|
)
|
||||||
|
|
||||||
val SimplexColorPalette = darkColors(
|
val SimplexColorPalette = darkColors(
|
||||||
@@ -251,11 +672,39 @@ val SimplexColorPalette = darkColors(
|
|||||||
)
|
)
|
||||||
val SimplexColorPaletteApp = AppColors(
|
val SimplexColorPaletteApp = AppColors(
|
||||||
title = Color(0xFF267BE5),
|
title = Color(0xFF267BE5),
|
||||||
sentMessage = SentMessageColor,
|
primaryVariant2 = Color(0xFF172941),
|
||||||
receivedMessage = Color(0x20B1B0B5)
|
sentMessage = Color(0xFF172941),
|
||||||
|
sentQuote = Color(0xFF1C3A57),
|
||||||
|
receivedMessage = Color(0xff25283a),
|
||||||
|
receivedQuote = Color(0xff36394a),
|
||||||
)
|
)
|
||||||
|
|
||||||
val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow(ThemeManager.currentColors(isInNightMode()))
|
val BlackColorPalette = darkColors(
|
||||||
|
primary = Color(0xff0077e0), // If this value changes also need to update #0088ff in string resource files
|
||||||
|
primaryVariant = Color(0xff0077e0),
|
||||||
|
secondary = HighOrLowlight,
|
||||||
|
secondaryVariant = DarkGray,
|
||||||
|
background = Color(0xff070707),
|
||||||
|
surface = Color(0xff161617),
|
||||||
|
// background = Color(0xFF121212),
|
||||||
|
// surface = Color(0xFF121212),
|
||||||
|
error = Color.Red,
|
||||||
|
onBackground = Color(0xFFFFFBFA),
|
||||||
|
onSurface = Color(0xFFFFFBFA),
|
||||||
|
// onError: Color = Color.Black,
|
||||||
|
)
|
||||||
|
val BlackColorPaletteApp = AppColors(
|
||||||
|
title = Color(0xff0077e0),
|
||||||
|
primaryVariant2 = Color(0xff243747),
|
||||||
|
sentMessage = Color(0xFF18262E),
|
||||||
|
sentQuote = Color(0xFF1D3847),
|
||||||
|
receivedMessage = Color(0xff1b1b1b),
|
||||||
|
receivedQuote = Color(0xff29292b),
|
||||||
|
)
|
||||||
|
|
||||||
|
var systemInDarkThemeCurrently: Boolean = isInNightMode()
|
||||||
|
|
||||||
|
val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow(ThemeManager.currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()))
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight
|
fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight
|
||||||
@@ -263,31 +712,113 @@ fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLi
|
|||||||
@Composable
|
@Composable
|
||||||
expect fun isSystemInDarkTheme(): Boolean
|
expect fun isSystemInDarkTheme(): Boolean
|
||||||
|
|
||||||
|
internal val LocalAppColors = staticCompositionLocalOf { LightColorPaletteApp }
|
||||||
|
internal val LocalAppWallpaper = staticCompositionLocalOf { AppWallpaper() }
|
||||||
|
|
||||||
|
val MaterialTheme.appColors: AppColors
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = LocalAppColors.current
|
||||||
|
|
||||||
|
fun AppColors.updateColorsFrom(other: AppColors) {
|
||||||
|
title = other.title
|
||||||
|
primaryVariant2 = other.primaryVariant2
|
||||||
|
sentMessage = other.sentMessage
|
||||||
|
sentQuote = other.sentQuote
|
||||||
|
receivedMessage = other.receivedMessage
|
||||||
|
receivedQuote = other.receivedQuote
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AppWallpaper.updateWallpaperFrom(other: AppWallpaper) {
|
||||||
|
background = other.background
|
||||||
|
tint = other.tint
|
||||||
|
type = other.type
|
||||||
|
}
|
||||||
|
|
||||||
|
val MaterialTheme.wallpaper: AppWallpaper
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = LocalAppWallpaper.current
|
||||||
|
|
||||||
fun reactOnDarkThemeChanges(isDark: Boolean) {
|
fun reactOnDarkThemeChanges(isDark: Boolean) {
|
||||||
if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == isDark) {
|
systemInDarkThemeCurrently = isDark
|
||||||
|
if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.value.colors.isLight == isDark) {
|
||||||
// Change active colors from light to dark and back based on system theme
|
// Change active colors from light to dark and back based on system theme
|
||||||
ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, isDark)
|
ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
|
fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
|
||||||
LaunchedEffect(darkTheme) {
|
// TODO: Fix preview working with dark/light theme
|
||||||
// For preview
|
|
||||||
if (darkTheme != null)
|
// LaunchedEffect(darkTheme) {
|
||||||
CurrentColors.value = ThemeManager.currentColors(darkTheme)
|
// // For preview
|
||||||
}
|
// if (darkTheme != null)
|
||||||
val systemDark = isSystemInDarkTheme()
|
// CurrentColors.value = ThemeManager.currentColors(darkTheme, null, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
|
||||||
LaunchedEffect(systemDark) {
|
// }
|
||||||
reactOnDarkThemeChanges(systemDark)
|
val systemDark = rememberUpdatedState(isSystemInDarkTheme())
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// snapshotFlow vs LaunchedEffect reduce number of recomposes
|
||||||
|
snapshotFlow { systemDark.value }
|
||||||
|
.collect {
|
||||||
|
reactOnDarkThemeChanges(systemDark.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val theme by CurrentColors.collectAsState()
|
val theme by CurrentColors.collectAsState()
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// snapshotFlow vs LaunchedEffect reduce number of recomposes when user is changed or it's themes
|
||||||
|
snapshotFlow { chatModel.currentUser.value?.uiThemes }
|
||||||
|
.collect {
|
||||||
|
ThemeManager.applyTheme(appPrefs.currentTheme.get()!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colors = theme.colors,
|
colors = theme.colors,
|
||||||
typography = Typography,
|
typography = Typography,
|
||||||
shapes = Shapes,
|
shapes = Shapes,
|
||||||
content = {
|
content = {
|
||||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onBackground, content = content)
|
val rememberedAppColors = remember {
|
||||||
|
// Explicitly creating a new object here so we don't mutate the initial [appColors]
|
||||||
|
// provided, and overwrite the values set in it.
|
||||||
|
theme.appColors.copy()
|
||||||
|
}.apply { updateColorsFrom(theme.appColors) }
|
||||||
|
val rememberedWallpaper = remember {
|
||||||
|
// Explicitly creating a new object here so we don't mutate the initial [wallpaper]
|
||||||
|
// provided, and overwrite the values set in it.
|
||||||
|
theme.wallpaper.copy()
|
||||||
|
}.apply { updateWallpaperFrom(theme.wallpaper) }
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalContentColor provides MaterialTheme.colors.onBackground,
|
||||||
|
LocalAppColors provides rememberedAppColors,
|
||||||
|
LocalAppWallpaper provides rememberedWallpaper,
|
||||||
|
content = content)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SimpleXThemeOverride(theme: ThemeManager.ActiveTheme, content: @Composable () -> Unit) {
|
||||||
|
MaterialTheme(
|
||||||
|
colors = theme.colors,
|
||||||
|
typography = Typography,
|
||||||
|
shapes = Shapes,
|
||||||
|
content = {
|
||||||
|
val rememberedAppColors = remember {
|
||||||
|
// Explicitly creating a new object here so we don't mutate the initial [appColors]
|
||||||
|
// provided, and overwrite the values set in it.
|
||||||
|
theme.appColors.copy()
|
||||||
|
}.apply { updateColorsFrom(theme.appColors) }
|
||||||
|
val rememberedWallpaper = remember {
|
||||||
|
// Explicitly creating a new object here so we don't mutate the initial [wallpaper]
|
||||||
|
// provided, and overwrite the values set in it.
|
||||||
|
theme.wallpaper.copy()
|
||||||
|
}.apply { updateWallpaperFrom(theme.wallpaper) }
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalContentColor provides MaterialTheme.colors.onBackground,
|
||||||
|
LocalAppColors provides rememberedAppColors,
|
||||||
|
LocalAppWallpaper provides rememberedWallpaper,
|
||||||
|
content = content)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+184
-109
@@ -1,14 +1,14 @@
|
|||||||
package chat.simplex.common.ui.theme
|
package chat.simplex.common.ui.theme
|
||||||
|
|
||||||
import androidx.compose.material.Colors
|
import androidx.compose.material.Colors
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.model.AppPreferences
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.model.ChatController
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.platform.platform
|
import java.io.File
|
||||||
import chat.simplex.common.views.helpers.generalGetString
|
|
||||||
|
|
||||||
// https://github.com/rsms/inter
|
// https://github.com/rsms/inter
|
||||||
// I place it here because IDEA shows an error (but still works anyway) when this declaration inside Type.kt
|
// I place it here because IDEA shows an error (but still works anyway) when this declaration inside Type.kt
|
||||||
@@ -18,140 +18,215 @@ expect val EmojiFont: FontFamily
|
|||||||
object ThemeManager {
|
object ThemeManager {
|
||||||
private val appPrefs: AppPreferences = ChatController.appPrefs
|
private val appPrefs: AppPreferences = ChatController.appPrefs
|
||||||
|
|
||||||
data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors)
|
data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors, val wallpaper: AppWallpaper = AppWallpaper())
|
||||||
|
|
||||||
private fun systemDarkThemeColors(): Pair<Colors, DefaultTheme> = when (appPrefs.systemDarkTheme.get()) {
|
private fun systemDarkThemeColors(): Pair<Colors, DefaultTheme> = when (appPrefs.systemDarkTheme.get()) {
|
||||||
DefaultTheme.DARK.name -> DarkColorPalette to DefaultTheme.DARK
|
DefaultTheme.DARK.themeName -> DarkColorPalette to DefaultTheme.DARK
|
||||||
DefaultTheme.SIMPLEX.name -> SimplexColorPalette to DefaultTheme.SIMPLEX
|
DefaultTheme.SIMPLEX.themeName -> SimplexColorPalette to DefaultTheme.SIMPLEX
|
||||||
|
DefaultTheme.BLACK.themeName -> BlackColorPalette to DefaultTheme.BLACK
|
||||||
else -> SimplexColorPalette to DefaultTheme.SIMPLEX
|
else -> SimplexColorPalette to DefaultTheme.SIMPLEX
|
||||||
}
|
}
|
||||||
|
|
||||||
fun currentColors(darkForSystemTheme: Boolean): ActiveTheme {
|
private fun nonSystemThemeName(): String {
|
||||||
val themeName = appPrefs.currentTheme.get()!!
|
val themeName = appPrefs.currentTheme.get()!!
|
||||||
val themeOverrides = appPrefs.themeOverrides.get()
|
return if (themeName != DefaultTheme.SYSTEM_THEME_NAME) {
|
||||||
|
|
||||||
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
|
|
||||||
themeName
|
themeName
|
||||||
} else {
|
} else {
|
||||||
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
|
if (systemInDarkThemeCurrently) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.themeName
|
||||||
}
|
}
|
||||||
val theme = themeOverrides[nonSystemThemeName]
|
}
|
||||||
|
|
||||||
|
fun defaultActiveTheme(appSettingsTheme: List<ThemeOverrides>): ThemeOverrides? {
|
||||||
|
val nonSystemThemeName = nonSystemThemeName()
|
||||||
|
val defaultThemeId = appPrefs.currentThemeIds.get()[nonSystemThemeName]
|
||||||
|
return appSettingsTheme.getTheme(defaultThemeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun defaultActiveTheme(perUserTheme: ThemeModeOverrides?, appSettingsTheme: List<ThemeOverrides>): ThemeModeOverride {
|
||||||
|
val perUserTheme = if (!CurrentColors.value.colors.isLight) perUserTheme?.dark else perUserTheme?.light
|
||||||
|
if (perUserTheme != null) {
|
||||||
|
return perUserTheme
|
||||||
|
}
|
||||||
|
val defaultTheme = defaultActiveTheme(appSettingsTheme)
|
||||||
|
return ThemeModeOverride(colors = defaultTheme?.colors ?: ThemeColors(), wallpaper = defaultTheme?.wallpaper)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentColors(themeOverridesForType: WallpaperType?, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?, appSettingsTheme: List<ThemeOverrides>): ActiveTheme {
|
||||||
|
val themeName = appPrefs.currentTheme.get()!!
|
||||||
|
val nonSystemThemeName = nonSystemThemeName()
|
||||||
|
val defaultTheme = defaultActiveTheme(appSettingsTheme)
|
||||||
|
|
||||||
val baseTheme = when (nonSystemThemeName) {
|
val baseTheme = when (nonSystemThemeName) {
|
||||||
DefaultTheme.LIGHT.name -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp)
|
DefaultTheme.LIGHT.themeName -> ActiveTheme(DefaultTheme.LIGHT.themeName, DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.LIGHT)))
|
||||||
DefaultTheme.DARK.name -> Triple(DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp)
|
DefaultTheme.DARK.themeName -> ActiveTheme(DefaultTheme.DARK.themeName, DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.DARK)))
|
||||||
DefaultTheme.SIMPLEX.name -> Triple(DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp)
|
DefaultTheme.SIMPLEX.themeName -> ActiveTheme(DefaultTheme.SIMPLEX.themeName, DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.SIMPLEX)))
|
||||||
else -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp)
|
DefaultTheme.BLACK.themeName -> ActiveTheme(DefaultTheme.BLACK.themeName, DefaultTheme.BLACK, BlackColorPalette, BlackColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.BLACK)))
|
||||||
|
else -> ActiveTheme(DefaultTheme.LIGHT.themeName, DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.LIGHT)))
|
||||||
}
|
}
|
||||||
if (theme == null) {
|
|
||||||
return ActiveTheme(themeName, baseTheme.first, baseTheme.second, baseTheme.third)
|
val perUserTheme = if (baseTheme.colors.isLight) perUserTheme?.light else perUserTheme?.dark
|
||||||
|
val theme = (appSettingsTheme.sameTheme(themeOverridesForType ?: perChatTheme?.type ?: perUserTheme?.type ?: defaultTheme?.wallpaper?.toAppWallpaper()?.type, nonSystemThemeName) ?: defaultTheme)
|
||||||
|
|
||||||
|
if (theme == null && perUserTheme == null && perChatTheme == null && themeOverridesForType == null) {
|
||||||
|
return ActiveTheme(themeName, baseTheme.base, baseTheme.colors, baseTheme.appColors, baseTheme.wallpaper)
|
||||||
}
|
}
|
||||||
return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base))
|
val presetWallpaperTheme = when {
|
||||||
|
perChatTheme?.wallpaper != null -> if (perChatTheme.wallpaper.preset != null) PresetWallpaper.from(perChatTheme.wallpaper.preset)?.colors?.get(baseTheme.base) else null
|
||||||
|
perUserTheme?.wallpaper != null -> if (perUserTheme.wallpaper.preset != null) PresetWallpaper.from(perUserTheme.wallpaper.preset)?.colors?.get(baseTheme.base) else null
|
||||||
|
else -> if (theme?.wallpaper?.preset != null) PresetWallpaper.from(theme.wallpaper.preset)?.colors?.get(baseTheme.base) else null
|
||||||
|
}
|
||||||
|
val themeOrEmpty = theme ?: ThemeOverrides(base = baseTheme.base)
|
||||||
|
val colors = themeOrEmpty.toColors(themeOrEmpty.base, perChatTheme?.colors, perUserTheme?.colors, presetWallpaperTheme)
|
||||||
|
return ActiveTheme(
|
||||||
|
themeName,
|
||||||
|
baseTheme.base,
|
||||||
|
colors,
|
||||||
|
themeOrEmpty.toAppColors(themeOrEmpty.base, perChatTheme?.colors, perChatTheme?.type, perUserTheme?.colors, perUserTheme?.type, presetWallpaperTheme),
|
||||||
|
themeOrEmpty.toAppWallpaper(themeOverridesForType, perChatTheme, perUserTheme, colors.background)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun currentThemeOverridesForExport(darkForSystemTheme: Boolean): ThemeOverrides {
|
fun currentThemeOverridesForExport(perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?): ThemeOverrides {
|
||||||
val themeName = appPrefs.currentTheme.get()!!
|
val current = currentColors(null, perChatTheme, perUserTheme, appPrefs.themeOverrides.get())
|
||||||
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
|
val wType = current.wallpaper.type
|
||||||
themeName
|
val wBackground = current.wallpaper.background
|
||||||
} else {
|
val wTint = current.wallpaper.tint
|
||||||
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
|
return ThemeOverrides(
|
||||||
}
|
themeId = "",
|
||||||
val overrides = appPrefs.themeOverrides.get().toMutableMap()
|
base = current.base,
|
||||||
val nonFilledTheme = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
|
colors = ThemeColors.from(current.colors, current.appColors),
|
||||||
return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base))
|
wallpaper = if (wType !is WallpaperType.Empty) ThemeWallpaper.from(wType, wBackground?.toReadableHex(), wTint?.toReadableHex()).withFilledWallpaperBase64() else null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// colors, default theme enum, localized name of theme
|
fun applyTheme(theme: String) {
|
||||||
fun allThemes(darkForSystemTheme: Boolean): List<Triple<Colors, DefaultTheme, String>> {
|
|
||||||
val allThemes = ArrayList<Triple<Colors, DefaultTheme, String>>()
|
|
||||||
allThemes.add(
|
|
||||||
Triple(
|
|
||||||
if (darkForSystemTheme) systemDarkThemeColors().first else LightColorPalette,
|
|
||||||
DefaultTheme.SYSTEM,
|
|
||||||
generalGetString(MR.strings.theme_system)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
allThemes.add(
|
|
||||||
Triple(
|
|
||||||
LightColorPalette,
|
|
||||||
DefaultTheme.LIGHT,
|
|
||||||
generalGetString(MR.strings.theme_light)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
allThemes.add(
|
|
||||||
Triple(
|
|
||||||
DarkColorPalette,
|
|
||||||
DefaultTheme.DARK,
|
|
||||||
generalGetString(MR.strings.theme_dark)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
allThemes.add(
|
|
||||||
Triple(
|
|
||||||
SimplexColorPalette,
|
|
||||||
DefaultTheme.SIMPLEX,
|
|
||||||
generalGetString(MR.strings.theme_simplex)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return allThemes
|
|
||||||
}
|
|
||||||
|
|
||||||
fun applyTheme(theme: String, darkForSystemTheme: Boolean) {
|
|
||||||
appPrefs.currentTheme.set(theme)
|
appPrefs.currentTheme.set(theme)
|
||||||
CurrentColors.value = currentColors(darkForSystemTheme)
|
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
platform.androidSetNightModeIfSupported()
|
platform.androidSetNightModeIfSupported()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeDarkTheme(theme: String, darkForSystemTheme: Boolean) {
|
fun changeDarkTheme(theme: String) {
|
||||||
appPrefs.systemDarkTheme.set(theme)
|
appPrefs.systemDarkTheme.set(theme)
|
||||||
CurrentColors.value = currentColors(darkForSystemTheme)
|
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveAndApplyThemeColor(name: ThemeColor, color: Color? = null, darkForSystemTheme: Boolean) {
|
fun saveAndApplyThemeColor(baseTheme: DefaultTheme, name: ThemeColor, color: Color? = null, pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) {
|
||||||
val themeName = appPrefs.currentTheme.get()!!
|
val nonSystemThemeName = baseTheme.themeName
|
||||||
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
|
val overrides = pref.get()
|
||||||
themeName
|
val themeId = appPrefs.currentThemeIds.get()[nonSystemThemeName]
|
||||||
} else {
|
val prevValue = overrides.getTheme(themeId) ?: ThemeOverrides(base = baseTheme)
|
||||||
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
|
pref.set(overrides.replace(prevValue.withUpdatedColor(name, color?.toReadableHex())))
|
||||||
|
val themeIds = appPrefs.currentThemeIds.get().toMutableMap()
|
||||||
|
themeIds[nonSystemThemeName] = prevValue.themeId
|
||||||
|
appPrefs.currentThemeIds.set(themeIds)
|
||||||
|
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState<ThemeModeOverride>) {
|
||||||
|
pref.value = pref.value.withUpdatedColor(name, color?.toReadableHex())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveAndApplyWallpaper(baseTheme: DefaultTheme, type: WallpaperType?, pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) {
|
||||||
|
val nonSystemThemeName = baseTheme.themeName
|
||||||
|
val overrides = pref.get()
|
||||||
|
val theme = overrides.sameTheme(type, baseTheme.themeName)
|
||||||
|
val prevValue = theme ?: ThemeOverrides(base = baseTheme)
|
||||||
|
pref.set(overrides.replace(prevValue.copy(wallpaper = if (type != null && type !is WallpaperType.Empty) ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) else null)))
|
||||||
|
val themeIds = appPrefs.currentThemeIds.get().toMutableMap()
|
||||||
|
themeIds[nonSystemThemeName] = prevValue.themeId
|
||||||
|
appPrefs.currentThemeIds.set(themeIds)
|
||||||
|
CurrentColors.value = currentColors( null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyFromSameThemeOverrides(type: WallpaperType?, lowerLevelOverride: ThemeModeOverride?, pref: MutableState<ThemeModeOverride>): Boolean {
|
||||||
|
val overrides = appPrefs.themeOverrides.get()
|
||||||
|
val sameWallpaper = if (lowerLevelOverride?.type?.sameType(type) == true) lowerLevelOverride.wallpaper else overrides.sameTheme(type, CurrentColors.value.base.themeName)?.wallpaper
|
||||||
|
if (sameWallpaper == null) {
|
||||||
|
if (type != null) {
|
||||||
|
pref.value = ThemeModeOverride(wallpaper = ThemeWallpaper.from(type, null, null).copy(scale = null, scaleType = null))
|
||||||
|
} else {
|
||||||
|
// Make an empty wallpaper to override any top level ones
|
||||||
|
pref.value = ThemeModeOverride(wallpaper = ThemeWallpaper())
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
var colorToSet = color
|
var type = sameWallpaper.toAppWallpaper().type
|
||||||
if (colorToSet == null) {
|
if (type is WallpaperType.Image && sameWallpaper.imageFile == type.filename) {
|
||||||
// Setting default color from a base theme
|
// same image file. Needs to be copied first in order to be able to remove the file once it's not needed anymore without affecting main theme override
|
||||||
colorToSet = when(nonSystemThemeName) {
|
val filename = saveWallpaperFile(File(getWallpaperFilePath(type.filename)).toURI())
|
||||||
DefaultTheme.LIGHT.name -> name.fromColors(LightColorPalette, LightColorPaletteApp)
|
if (filename != null) {
|
||||||
DefaultTheme.DARK.name -> name.fromColors(DarkColorPalette, DarkColorPaletteApp)
|
type = WallpaperType.Image(filename, type.scale, type.scaleType)
|
||||||
DefaultTheme.SIMPLEX.name -> name.fromColors(SimplexColorPalette, SimplexColorPaletteApp)
|
} else {
|
||||||
// Will not be here
|
Log.e(TAG, "Error while copying wallpaper from global overrides to chat overrides")
|
||||||
else -> return
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val overrides = appPrefs.themeOverrides.get().toMutableMap()
|
val prevValue = pref.value
|
||||||
val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
|
pref.value = prevValue.copy(
|
||||||
overrides[nonSystemThemeName] = prevValue.withUpdatedColor(name, colorToSet.toReadableHex())
|
colors = ThemeColors(),
|
||||||
appPrefs.themeOverrides.set(overrides)
|
wallpaper = ThemeWallpaper.from(type, null, null).copy(scale = null, scaleType = null)
|
||||||
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
|
)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean) {
|
fun applyWallpaper(type: WallpaperType?, pref: MutableState<ThemeModeOverride>) {
|
||||||
val overrides = appPrefs.themeOverrides.get().toMutableMap()
|
val prevValue = pref.value
|
||||||
val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
|
pref.value = prevValue.copy(
|
||||||
overrides[theme.base.name] = prevValue.copy(colors = theme.colors)
|
wallpaper = if (type != null)
|
||||||
appPrefs.themeOverrides.set(overrides)
|
ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint)
|
||||||
appPrefs.currentTheme.set(theme.base.name)
|
else null
|
||||||
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetAllThemeColors(darkForSystemTheme: Boolean) {
|
fun saveAndApplyThemeOverrides(theme: ThemeOverrides, pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) {
|
||||||
val themeName = appPrefs.currentTheme.get()!!
|
val wallpaper = theme.wallpaper?.importFromString()
|
||||||
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
|
val nonSystemThemeName = theme.base.themeName
|
||||||
themeName
|
val overrides = pref.get()
|
||||||
} else {
|
val prevValue = overrides.getTheme(null, wallpaper?.toAppWallpaper()?.type, theme.base) ?: ThemeOverrides(base = theme.base)
|
||||||
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
|
if (prevValue.wallpaper?.imageFile != null) {
|
||||||
|
File(getWallpaperFilePath(prevValue.wallpaper.imageFile)).delete()
|
||||||
|
}
|
||||||
|
pref.set(overrides.replace(prevValue.copy(base = theme.base, colors = theme.colors, wallpaper = wallpaper)))
|
||||||
|
appPrefs.currentTheme.set(nonSystemThemeName)
|
||||||
|
val currentThemeIds = appPrefs.currentThemeIds.get().toMutableMap()
|
||||||
|
currentThemeIds[nonSystemThemeName] = prevValue.themeId
|
||||||
|
appPrefs.currentThemeIds.set(currentThemeIds)
|
||||||
|
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetAllThemeColors(pref: SharedPreference<List<ThemeOverrides>> = appPrefs.themeOverrides) {
|
||||||
|
val nonSystemThemeName = nonSystemThemeName()
|
||||||
|
val themeId = appPrefs.currentThemeIds.get()[nonSystemThemeName] ?: return
|
||||||
|
val overrides = pref.get()
|
||||||
|
val prevValue = overrides.getTheme(themeId) ?: return
|
||||||
|
pref.set(overrides.replace(prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper?.copy(background = null, tint = null))))
|
||||||
|
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetAllThemeColors(pref: MutableState<ThemeModeOverride>) {
|
||||||
|
val prevValue = pref.value
|
||||||
|
pref.value = prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper?.copy(background = null, tint = null))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeTheme(themeId: String?) {
|
||||||
|
val themes = ArrayList(appPrefs.themeOverrides.get())
|
||||||
|
themes.removeAll { it.themeId == themeId }
|
||||||
|
appPrefs.themeOverrides.set(themes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.colorFromReadableHex(): Color =
|
||||||
|
Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong())
|
||||||
|
|
||||||
|
fun Color.toReadableHex(): String {
|
||||||
|
val s = Integer.toHexString(toArgb())
|
||||||
|
return when {
|
||||||
|
this == Color.Transparent -> "#00ffffff"
|
||||||
|
s.length == 1 -> "#ff$s$s$s$s$s$s"
|
||||||
|
s.length == 2 -> "#ff$s$s$s"
|
||||||
|
s.length == 3 -> "#ff$s$s"
|
||||||
|
s.length == 6 && this.alpha == 0f -> "#00$s"
|
||||||
|
s.length == 6 -> "#ff$s"
|
||||||
|
else -> "#$s"
|
||||||
}
|
}
|
||||||
val overrides = appPrefs.themeOverrides.get().toMutableMap()
|
|
||||||
val prevValue = overrides[nonSystemThemeName] ?: return
|
|
||||||
overrides[nonSystemThemeName] = prevValue.copy(colors = ThemeColors())
|
|
||||||
appPrefs.themeOverrides.set(overrides)
|
|
||||||
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb())
|
|
||||||
|
|||||||
+87
@@ -29,6 +29,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import chat.simplex.common.model.ChatModel.controller
|
import chat.simplex.common.model.ChatModel.controller
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
@@ -41,6 +42,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatInfoView(
|
fun ChatInfoView(
|
||||||
@@ -337,6 +339,16 @@ fun ChatInfoLayout(
|
|||||||
if (cStats != null && cStats.ratchetSyncAllowed) {
|
if (cStats != null && cStats.ratchetSyncAllowed) {
|
||||||
SynchronizeConnectionButton(syncContactConnection)
|
SynchronizeConnectionButton(syncContactConnection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WallpaperButton {
|
||||||
|
ModalManager.end.showModal {
|
||||||
|
val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } }
|
||||||
|
val c = chat.value
|
||||||
|
if (c != null) {
|
||||||
|
ChatWallpaperEditorModal(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// } else if (developerTools) {
|
// } else if (developerTools) {
|
||||||
// SynchronizeConnectionButtonForce(syncContactConnectionForce)
|
// SynchronizeConnectionButtonForce(syncContactConnectionForce)
|
||||||
// }
|
// }
|
||||||
@@ -642,6 +654,15 @@ private fun SendReceiptsOption(currentUser: User, state: State<SendReceipts>, on
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WallpaperButton(onClick: () -> Unit) {
|
||||||
|
SettingsActionItem(
|
||||||
|
painterResource(MR.images.ic_image),
|
||||||
|
stringResource(MR.strings.settings_section_title_chat_theme),
|
||||||
|
click = onClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ClearChatButton(onClick: () -> Unit) {
|
fun ClearChatButton(onClick: () -> Unit) {
|
||||||
SettingsActionItem(
|
SettingsActionItem(
|
||||||
@@ -675,6 +696,72 @@ fun ShareAddressButton(onClick: () -> Unit) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ModalData.ChatWallpaperEditorModal(chat: Chat) {
|
||||||
|
val themes = remember(CurrentColors.collectAsState().value.base) {
|
||||||
|
(chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes
|
||||||
|
?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes
|
||||||
|
?: ThemeModeOverrides()
|
||||||
|
}
|
||||||
|
val globalThemeUsed = remember { stateGetOrPut("globalThemeUsed") { false } }
|
||||||
|
val initialTheme = remember(CurrentColors.collectAsState().value.base) {
|
||||||
|
val preferred = themes.preferredMode(!CurrentColors.value.colors.isLight)
|
||||||
|
globalThemeUsed.value = preferred == null
|
||||||
|
preferred ?: ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
|
}
|
||||||
|
ChatWallpaperEditor(
|
||||||
|
initialTheme,
|
||||||
|
applyToMode = if (themes.light == themes.dark) null else initialTheme.mode,
|
||||||
|
globalThemeUsed = globalThemeUsed,
|
||||||
|
save = { applyToMode, newTheme ->
|
||||||
|
save(applyToMode, newTheme, chatModel.getChat(chat.id) ?: chat)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, chat: Chat) {
|
||||||
|
val unchangedThemes: ThemeModeOverrides = ((chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes ?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes) ?: ThemeModeOverrides()
|
||||||
|
val wallpaperFiles = setOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile)
|
||||||
|
var changedThemes: ThemeModeOverrides? = unchangedThemes
|
||||||
|
val changed = newTheme?.copy(wallpaper = newTheme.wallpaper?.withFilledWallpaperPath())
|
||||||
|
changedThemes = when (applyToMode) {
|
||||||
|
null -> changedThemes?.copy(light = changed?.copy(mode = DefaultThemeMode.LIGHT), dark = changed?.copy(mode = DefaultThemeMode.DARK))
|
||||||
|
DefaultThemeMode.LIGHT -> changedThemes?.copy(light = changed?.copy(mode = applyToMode))
|
||||||
|
DefaultThemeMode.DARK -> changedThemes?.copy(dark = changed?.copy(mode = applyToMode))
|
||||||
|
}
|
||||||
|
changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) {
|
||||||
|
val light = changedThemes.light
|
||||||
|
val dark = changedThemes.dark
|
||||||
|
val currentMode = CurrentColors.value.base.mode
|
||||||
|
// same image file for both modes, copy image to make them as different files
|
||||||
|
if (light?.wallpaper?.imageFile != null && dark?.wallpaper?.imageFile != null && light.wallpaper.imageFile == dark.wallpaper.imageFile) {
|
||||||
|
val imageFile = if (currentMode == DefaultThemeMode.LIGHT) {
|
||||||
|
dark.wallpaper.imageFile
|
||||||
|
} else {
|
||||||
|
light.wallpaper.imageFile
|
||||||
|
}
|
||||||
|
val filePath = saveWallpaperFile(File(getWallpaperFilePath(imageFile)).toURI())
|
||||||
|
changedThemes = if (currentMode == DefaultThemeMode.LIGHT) {
|
||||||
|
changedThemes.copy(dark = dark.copy(wallpaper = dark.wallpaper.copy(imageFile = filePath)))
|
||||||
|
} else {
|
||||||
|
changedThemes.copy(light = light.copy(wallpaper = light.wallpaper.copy(imageFile = filePath)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changedThemes
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val wallpaperFilesToDelete = wallpaperFiles - changedThemes?.light?.wallpaper?.imageFile - changedThemes?.dark?.wallpaper?.imageFile
|
||||||
|
wallpaperFilesToDelete.forEach(::removeWallpaperFile)
|
||||||
|
|
||||||
|
if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) {
|
||||||
|
if (chat.chatInfo is ChatInfo.Direct) {
|
||||||
|
chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes)))
|
||||||
|
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||||
|
chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi {
|
private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi {
|
||||||
val chatRh = chat.remoteHostId
|
val chatRh = chat.remoteHostId
|
||||||
chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let {
|
chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let {
|
||||||
|
|||||||
+18
-6
@@ -42,7 +42,7 @@ sealed class CIInfoTab {
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
|
||||||
val sent = ci.chatDir.sent
|
val sent = ci.chatDir.sent
|
||||||
val appColors = CurrentColors.collectAsState().value.appColors
|
val appColors = MaterialTheme.appColors
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val selection = remember { mutableStateOf<CIInfoTab>(CIInfoTab.History) }
|
val selection = remember { mutableStateOf<CIInfoTab>(CIInfoTab.History) }
|
||||||
|
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+377
-351
@@ -5,25 +5,24 @@ import androidx.compose.foundation.*
|
|||||||
import androidx.compose.foundation.gestures.*
|
import androidx.compose.foundation.gestures.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.*
|
import androidx.compose.foundation.lazy.*
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.mapSaver
|
import androidx.compose.runtime.saveable.mapSaver
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.*
|
import androidx.compose.ui.*
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.platform.*
|
import androidx.compose.ui.platform.*
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.intl.Locale
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import androidx.compose.ui.text.*
|
import androidx.compose.ui.text.*
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import chat.simplex.common.model.ChatModel.controller
|
import chat.simplex.common.model.ChatModel.controller
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.call.*
|
import chat.simplex.common.views.call.*
|
||||||
@@ -118,368 +117,377 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
when (chat.chatInfo) {
|
when (chat.chatInfo) {
|
||||||
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
|
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
|
||||||
ChatLayout(
|
val perChatTheme = remember(chat.chatInfo, CurrentColors.value.base) { if (chat.chatInfo is ChatInfo.Direct) chat.chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chat.chatInfo is ChatInfo.Group) chat.chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null }
|
||||||
chat,
|
val overrides = if (perChatTheme != null) ThemeManager.currentColors(null, perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) else null
|
||||||
unreadCount,
|
SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) {
|
||||||
composeState,
|
ChatLayout(
|
||||||
composeView = {
|
chat,
|
||||||
Column(
|
unreadCount,
|
||||||
Modifier.fillMaxWidth(),
|
composeState,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
composeView = {
|
||||||
) {
|
Column(
|
||||||
if (
|
Modifier.fillMaxWidth(),
|
||||||
chat.chatInfo is ChatInfo.Direct
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
&& !chat.chatInfo.contact.ready
|
|
||||||
&& chat.chatInfo.contact.active
|
|
||||||
&& !chat.chatInfo.contact.nextSendGrpInv
|
|
||||||
) {
|
) {
|
||||||
Text(
|
if (
|
||||||
generalGetString(MR.strings.contact_connection_pending),
|
chat.chatInfo is ChatInfo.Direct
|
||||||
Modifier.padding(top = 4.dp),
|
&& !chat.chatInfo.contact.ready
|
||||||
fontSize = 14.sp,
|
&& chat.chatInfo.contact.active
|
||||||
color = MaterialTheme.colors.secondary
|
&& !chat.chatInfo.contact.nextSendGrpInv
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
generalGetString(MR.strings.contact_connection_pending),
|
||||||
|
Modifier.padding(top = 4.dp),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ComposeView(
|
||||||
|
chatModel, chat, composeState, attachmentOption,
|
||||||
|
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ComposeView(
|
},
|
||||||
chatModel, chat, composeState, attachmentOption,
|
attachmentOption,
|
||||||
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
|
attachmentBottomSheetState,
|
||||||
)
|
searchText,
|
||||||
}
|
useLinkPreviews = useLinkPreviews,
|
||||||
},
|
linkMode = chatModel.simplexLinkMode.value,
|
||||||
attachmentOption,
|
back = {
|
||||||
attachmentBottomSheetState,
|
hideKeyboard(view)
|
||||||
searchText,
|
AudioPlayer.stop()
|
||||||
useLinkPreviews = useLinkPreviews,
|
chatModel.chatId.value = null
|
||||||
linkMode = chatModel.simplexLinkMode.value,
|
chatModel.groupMembers.clear()
|
||||||
back = {
|
},
|
||||||
hideKeyboard(view)
|
info = {
|
||||||
AudioPlayer.stop()
|
if (ModalManager.end.hasModalsOpen()) {
|
||||||
chatModel.chatId.value = null
|
ModalManager.end.closeModals()
|
||||||
chatModel.groupMembers.clear()
|
return@ChatLayout
|
||||||
},
|
|
||||||
info = {
|
|
||||||
if (ModalManager.end.hasModalsOpen()) {
|
|
||||||
ModalManager.end.closeModals()
|
|
||||||
return@ChatLayout
|
|
||||||
}
|
|
||||||
hideKeyboard(view)
|
|
||||||
withBGApi {
|
|
||||||
// The idea is to preload information before showing a modal because large groups can take time to load all members
|
|
||||||
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null
|
|
||||||
var preloadedCode: String? = null
|
|
||||||
var preloadedLink: Pair<String, GroupMemberRole>? = null
|
|
||||||
if (chat.chatInfo is ChatInfo.Direct) {
|
|
||||||
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
|
||||||
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
|
||||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
|
||||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
|
||||||
preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
|
||||||
}
|
}
|
||||||
ModalManager.end.showModalCloseable(true) { close ->
|
hideKeyboard(view)
|
||||||
val chat = remember { activeChat }.value
|
|
||||||
if (chat?.chatInfo is ChatInfo.Direct) {
|
|
||||||
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
|
||||||
var code: String? by remember { mutableStateOf(preloadedCode) }
|
|
||||||
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
|
|
||||||
contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
|
||||||
preloadedContactInfo = contactInfo
|
|
||||||
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
|
||||||
preloadedCode = code
|
|
||||||
}
|
|
||||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
|
||||||
} else if (chat?.chatInfo is ChatInfo.Group) {
|
|
||||||
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
|
||||||
KeyChangeEffect(chat.id) {
|
|
||||||
setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
|
|
||||||
link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
|
||||||
preloadedLink = link
|
|
||||||
}
|
|
||||||
GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, {
|
|
||||||
link = it
|
|
||||||
preloadedLink = it
|
|
||||||
}, close)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
|
||||||
hideKeyboard(view)
|
|
||||||
withBGApi {
|
|
||||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
|
||||||
val stats = r?.second
|
|
||||||
val (_, code) = if (member.memberActive) {
|
|
||||||
val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId)
|
|
||||||
member to memCode?.second
|
|
||||||
} else {
|
|
||||||
member to null
|
|
||||||
}
|
|
||||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
|
||||||
ModalManager.end.closeModals()
|
|
||||||
ModalManager.end.showModalCloseable(true) { close ->
|
|
||||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
|
||||||
GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loadPrevMessages = {
|
|
||||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
|
||||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
|
|
||||||
val firstId = chatModel.chatItems.value.firstOrNull()?.id
|
|
||||||
if (c != null && firstId != null) {
|
|
||||||
withBGApi {
|
withBGApi {
|
||||||
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
|
// The idea is to preload information before showing a modal because large groups can take time to load all members
|
||||||
|
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null
|
||||||
|
var preloadedCode: String? = null
|
||||||
|
var preloadedLink: Pair<String, GroupMemberRole>? = null
|
||||||
|
if (chat.chatInfo is ChatInfo.Direct) {
|
||||||
|
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||||
|
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||||
|
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||||
|
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||||
|
preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||||
|
}
|
||||||
|
ModalManager.end.showModalCloseable(true) { close ->
|
||||||
|
val chat = remember { activeChat }.value
|
||||||
|
if (chat?.chatInfo is ChatInfo.Direct) {
|
||||||
|
var contactInfo: Pair<ConnectionStats?, Profile?>? by remember { mutableStateOf(preloadedContactInfo) }
|
||||||
|
var code: String? by remember { mutableStateOf(preloadedCode) }
|
||||||
|
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
|
||||||
|
contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||||
|
preloadedContactInfo = contactInfo
|
||||||
|
code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second
|
||||||
|
preloadedCode = code
|
||||||
|
}
|
||||||
|
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||||
|
} else if (chat?.chatInfo is ChatInfo.Group) {
|
||||||
|
var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) }
|
||||||
|
KeyChangeEffect(chat.id) {
|
||||||
|
setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel)
|
||||||
|
link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId)
|
||||||
|
preloadedLink = link
|
||||||
|
}
|
||||||
|
GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, {
|
||||||
|
link = it
|
||||||
|
preloadedLink = it
|
||||||
|
}, close)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
||||||
deleteMessage = { itemId, mode ->
|
hideKeyboard(view)
|
||||||
withBGApi {
|
withBGApi {
|
||||||
val cInfo = chat.chatInfo
|
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||||
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
|
val stats = r?.second
|
||||||
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
|
val (_, code) = if (member.memberActive) {
|
||||||
val groupInfo = toModerate?.first
|
val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId)
|
||||||
val groupMember = toModerate?.second
|
member to memCode?.second
|
||||||
val deletedChatItem: ChatItem?
|
} else {
|
||||||
val toChatItem: ChatItem?
|
member to null
|
||||||
if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
|
}
|
||||||
val r = chatModel.controller.apiDeleteMemberChatItem(
|
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||||
chatRh,
|
ModalManager.end.closeModals()
|
||||||
groupId = groupInfo.groupId,
|
ModalManager.end.showModalCloseable(true) { close ->
|
||||||
groupMemberId = groupMember.groupMemberId,
|
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||||
itemId = itemId
|
GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close)
|
||||||
)
|
}
|
||||||
deletedChatItem = r?.first
|
}
|
||||||
toChatItem = r?.second
|
}
|
||||||
} else {
|
},
|
||||||
val r = chatModel.controller.apiDeleteChatItem(
|
loadPrevMessages = {
|
||||||
chatRh,
|
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||||
|
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
|
||||||
|
val firstId = chatModel.chatItems.value.firstOrNull()?.id
|
||||||
|
if (c != null && firstId != null) {
|
||||||
|
withBGApi {
|
||||||
|
apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteMessage = { itemId, mode ->
|
||||||
|
withBGApi {
|
||||||
|
val cInfo = chat.chatInfo
|
||||||
|
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
|
||||||
|
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
|
||||||
|
val groupInfo = toModerate?.first
|
||||||
|
val groupMember = toModerate?.second
|
||||||
|
val deletedChatItem: ChatItem?
|
||||||
|
val toChatItem: ChatItem?
|
||||||
|
if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
|
||||||
|
val r = chatModel.controller.apiDeleteMemberChatItem(
|
||||||
|
chatRh,
|
||||||
|
groupId = groupInfo.groupId,
|
||||||
|
groupMemberId = groupMember.groupMemberId,
|
||||||
|
itemId = itemId
|
||||||
|
)
|
||||||
|
deletedChatItem = r?.first
|
||||||
|
toChatItem = r?.second
|
||||||
|
} else {
|
||||||
|
val r = chatModel.controller.apiDeleteChatItem(
|
||||||
|
chatRh,
|
||||||
|
type = cInfo.chatType,
|
||||||
|
id = cInfo.apiId,
|
||||||
|
itemId = itemId,
|
||||||
|
mode = mode
|
||||||
|
)
|
||||||
|
deletedChatItem = r?.deletedChatItem?.chatItem
|
||||||
|
toChatItem = r?.toChatItem?.chatItem
|
||||||
|
}
|
||||||
|
if (toChatItem == null && deletedChatItem != null) {
|
||||||
|
chatModel.removeChatItem(chatRh, cInfo, deletedChatItem)
|
||||||
|
} else if (toChatItem != null) {
|
||||||
|
chatModel.upsertChatItem(chatRh, cInfo, toChatItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteMessages = { itemIds ->
|
||||||
|
if (itemIds.isNotEmpty()) {
|
||||||
|
val chatInfo = chat.chatInfo
|
||||||
|
withBGApi {
|
||||||
|
val deletedItems: ArrayList<ChatItem> = arrayListOf()
|
||||||
|
for (itemId in itemIds) {
|
||||||
|
val di = chatModel.controller.apiDeleteChatItem(
|
||||||
|
chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
|
||||||
|
)?.deletedChatItem?.chatItem
|
||||||
|
if (di != null) {
|
||||||
|
deletedItems.add(di)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (di in deletedItems) {
|
||||||
|
chatModel.removeChatItem(chatRh, chatInfo, di)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
receiveFile = { fileId ->
|
||||||
|
withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) }
|
||||||
|
},
|
||||||
|
cancelFile = { fileId ->
|
||||||
|
withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) }
|
||||||
|
},
|
||||||
|
joinGroup = { groupId, onComplete ->
|
||||||
|
withBGApi {
|
||||||
|
chatModel.controller.apiJoinGroup(chatRh, groupId)
|
||||||
|
onComplete.invoke()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startCall = out@{ media ->
|
||||||
|
withBGApi {
|
||||||
|
val cInfo = chat.chatInfo
|
||||||
|
if (cInfo is ChatInfo.Direct) {
|
||||||
|
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
||||||
|
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||||
|
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||||
|
chatModel.showCallView.value = true
|
||||||
|
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
endCall = {
|
||||||
|
val call = chatModel.activeCall.value
|
||||||
|
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
||||||
|
},
|
||||||
|
acceptCall = { contact ->
|
||||||
|
hideKeyboard(view)
|
||||||
|
withBGApi {
|
||||||
|
val invitation = chatModel.callInvitations.remove(contact.id)
|
||||||
|
?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id }
|
||||||
|
if (invitation == null) {
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
|
||||||
|
} else {
|
||||||
|
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
acceptFeature = { contact, feature, param ->
|
||||||
|
withBGApi {
|
||||||
|
chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openDirectChat = { contactId ->
|
||||||
|
withBGApi {
|
||||||
|
openDirectChat(chatRh, contactId, chatModel)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
forwardItem = { cItem, cInfo ->
|
||||||
|
chatModel.chatId.value = null
|
||||||
|
chatModel.sharedContent.value = SharedContent.Forward(cInfo, cItem)
|
||||||
|
},
|
||||||
|
updateContactStats = { contact ->
|
||||||
|
withBGApi {
|
||||||
|
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
||||||
|
if (r != null) {
|
||||||
|
val contactStats = r.first
|
||||||
|
if (contactStats != null)
|
||||||
|
chatModel.updateContactConnectionStats(chatRh, contact, contactStats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateMemberStats = { groupInfo, member ->
|
||||||
|
withBGApi {
|
||||||
|
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||||
|
if (r != null) {
|
||||||
|
val memStats = r.second
|
||||||
|
if (memStats != null) {
|
||||||
|
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
syncContactConnection = { contact ->
|
||||||
|
withBGApi {
|
||||||
|
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
|
||||||
|
if (cStats != null) {
|
||||||
|
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
syncMemberConnection = { groupInfo, member ->
|
||||||
|
withBGApi {
|
||||||
|
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
|
||||||
|
if (r != null) {
|
||||||
|
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
findModelChat = { chatId ->
|
||||||
|
chatModel.getChat(chatId)
|
||||||
|
},
|
||||||
|
findModelMember = { memberId ->
|
||||||
|
chatModel.groupMembers.find { it.id == memberId }
|
||||||
|
},
|
||||||
|
setReaction = { cInfo, cItem, add, reaction ->
|
||||||
|
withBGApi {
|
||||||
|
val updatedCI = chatModel.controller.apiChatItemReaction(
|
||||||
|
rh = chatRh,
|
||||||
type = cInfo.chatType,
|
type = cInfo.chatType,
|
||||||
id = cInfo.apiId,
|
id = cInfo.apiId,
|
||||||
itemId = itemId,
|
itemId = cItem.id,
|
||||||
mode = mode
|
add = add,
|
||||||
|
reaction = reaction
|
||||||
)
|
)
|
||||||
deletedChatItem = r?.deletedChatItem?.chatItem
|
if (updatedCI != null) {
|
||||||
toChatItem = r?.toChatItem?.chatItem
|
chatModel.updateChatItem(cInfo, updatedCI)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (toChatItem == null && deletedChatItem != null) {
|
},
|
||||||
chatModel.removeChatItem(chatRh, cInfo, deletedChatItem)
|
showItemDetails = { cInfo, cItem ->
|
||||||
} else if (toChatItem != null) {
|
suspend fun loadChatItemInfo(): ChatItemInfo? {
|
||||||
chatModel.upsertChatItem(chatRh, cInfo, toChatItem)
|
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
|
||||||
}
|
if (ciInfo != null) {
|
||||||
}
|
if (chat.chatInfo is ChatInfo.Group) {
|
||||||
},
|
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
||||||
deleteMessages = { itemIds ->
|
|
||||||
if (itemIds.isNotEmpty()) {
|
|
||||||
val chatInfo = chat.chatInfo
|
|
||||||
withBGApi {
|
|
||||||
val deletedItems: ArrayList<ChatItem> = arrayListOf()
|
|
||||||
for (itemId in itemIds) {
|
|
||||||
val di = chatModel.controller.apiDeleteChatItem(
|
|
||||||
chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal
|
|
||||||
)?.deletedChatItem?.chatItem
|
|
||||||
if (di != null) {
|
|
||||||
deletedItems.add(di)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (di in deletedItems) {
|
return ciInfo
|
||||||
chatModel.removeChatItem(chatRh, chatInfo, di)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
withBGApi {
|
||||||
},
|
var initialCiInfo = loadChatItemInfo() ?: return@withBGApi
|
||||||
receiveFile = { fileId ->
|
ModalManager.end.closeModals()
|
||||||
withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) }
|
ModalManager.end.showModalCloseable(endButtons = {
|
||||||
},
|
ShareButton {
|
||||||
cancelFile = { fileId ->
|
clipboard.shareText(itemInfoShareText(chatModel, cItem, initialCiInfo, chatModel.controller.appPrefs.developerTools.get()))
|
||||||
withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) }
|
}
|
||||||
},
|
}) { close ->
|
||||||
joinGroup = { groupId, onComplete ->
|
var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) }
|
||||||
withBGApi {
|
ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
||||||
chatModel.controller.apiJoinGroup(chatRh, groupId)
|
LaunchedEffect(cItem.id) {
|
||||||
onComplete.invoke()
|
withContext(Dispatchers.Default) {
|
||||||
}
|
for (apiResp in controller.messagesChannel) {
|
||||||
},
|
val msg = apiResp.resp
|
||||||
startCall = out@{ media ->
|
if (apiResp.remoteHostId == chatRh &&
|
||||||
withBGApi {
|
msg is CR.ChatItemStatusUpdated &&
|
||||||
val cInfo = chat.chatInfo
|
msg.chatItem.chatItem.id == cItem.id
|
||||||
if (cInfo is ChatInfo.Direct) {
|
) {
|
||||||
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId)
|
ciInfo = loadChatItemInfo() ?: return@withContext
|
||||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
initialCiInfo = ciInfo
|
||||||
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
}
|
||||||
chatModel.showCallView.value = true
|
|
||||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
endCall = {
|
|
||||||
val call = chatModel.activeCall.value
|
|
||||||
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
|
||||||
},
|
|
||||||
acceptCall = { contact ->
|
|
||||||
hideKeyboard(view)
|
|
||||||
withBGApi {
|
|
||||||
val invitation = chatModel.callInvitations.remove(contact.id)
|
|
||||||
?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id }
|
|
||||||
if (invitation == null) {
|
|
||||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
|
|
||||||
} else {
|
|
||||||
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
acceptFeature = { contact, feature, param ->
|
|
||||||
withBGApi {
|
|
||||||
chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openDirectChat = { contactId ->
|
|
||||||
withBGApi {
|
|
||||||
openDirectChat(chatRh, contactId, chatModel)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateContactStats = { contact ->
|
|
||||||
withBGApi {
|
|
||||||
val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId)
|
|
||||||
if (r != null) {
|
|
||||||
val contactStats = r.first
|
|
||||||
if (contactStats != null)
|
|
||||||
chatModel.updateContactConnectionStats(chatRh, contact, contactStats)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateMemberStats = { groupInfo, member ->
|
|
||||||
withBGApi {
|
|
||||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
|
||||||
if (r != null) {
|
|
||||||
val memStats = r.second
|
|
||||||
if (memStats != null) {
|
|
||||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
syncContactConnection = { contact ->
|
|
||||||
withBGApi {
|
|
||||||
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
|
|
||||||
if (cStats != null) {
|
|
||||||
chatModel.updateContactConnectionStats(chatRh, contact, cStats)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
syncMemberConnection = { groupInfo, member ->
|
|
||||||
withBGApi {
|
|
||||||
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
|
|
||||||
if (r != null) {
|
|
||||||
chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
findModelChat = { chatId ->
|
|
||||||
chatModel.getChat(chatId)
|
|
||||||
},
|
|
||||||
findModelMember = { memberId ->
|
|
||||||
chatModel.groupMembers.find { it.id == memberId }
|
|
||||||
},
|
|
||||||
setReaction = { cInfo, cItem, add, reaction ->
|
|
||||||
withBGApi {
|
|
||||||
val updatedCI = chatModel.controller.apiChatItemReaction(
|
|
||||||
rh = chatRh,
|
|
||||||
type = cInfo.chatType,
|
|
||||||
id = cInfo.apiId,
|
|
||||||
itemId = cItem.id,
|
|
||||||
add = add,
|
|
||||||
reaction = reaction
|
|
||||||
)
|
|
||||||
if (updatedCI != null) {
|
|
||||||
chatModel.updateChatItem(cInfo, updatedCI)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showItemDetails = { cInfo, cItem ->
|
|
||||||
suspend fun loadChatItemInfo(): ChatItemInfo? {
|
|
||||||
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
|
|
||||||
if (ciInfo != null) {
|
|
||||||
if (chat.chatInfo is ChatInfo.Group) {
|
|
||||||
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ciInfo
|
|
||||||
}
|
|
||||||
withBGApi {
|
|
||||||
var initialCiInfo = loadChatItemInfo() ?: return@withBGApi
|
|
||||||
ModalManager.end.closeModals()
|
|
||||||
ModalManager.end.showModalCloseable(endButtons = {
|
|
||||||
ShareButton {
|
|
||||||
clipboard.shareText(itemInfoShareText(chatModel, cItem, initialCiInfo, chatModel.controller.appPrefs.developerTools.get()))
|
|
||||||
}
|
|
||||||
}) { close ->
|
|
||||||
var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) }
|
|
||||||
ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
|
|
||||||
LaunchedEffect(cItem.id) {
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
for (apiResp in controller.messagesChannel) {
|
|
||||||
val msg = apiResp.resp
|
|
||||||
if (apiResp.remoteHostId == chatRh &&
|
|
||||||
msg is CR.ChatItemStatusUpdated &&
|
|
||||||
msg.chatItem.chatItem.id == cItem.id
|
|
||||||
) {
|
|
||||||
ciInfo = loadChatItemInfo() ?: return@withContext
|
|
||||||
initialCiInfo = ciInfo
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
KeyChangeEffect(chatModel.chatId.value) {
|
||||||
KeyChangeEffect(chatModel.chatId.value) {
|
close()
|
||||||
close()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
addMembers = { groupInfo ->
|
||||||
addMembers = { groupInfo ->
|
hideKeyboard(view)
|
||||||
hideKeyboard(view)
|
withBGApi {
|
||||||
withBGApi {
|
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
ModalManager.end.closeModals()
|
||||||
ModalManager.end.closeModals()
|
ModalManager.end.showModalCloseable(true) { close ->
|
||||||
ModalManager.end.showModalCloseable(true) { close ->
|
AddGroupMembersView(chatRh, groupInfo, false, chatModel, close)
|
||||||
AddGroupMembersView(chatRh, groupInfo, false, chatModel, close)
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
openGroupLink = { groupInfo ->
|
||||||
openGroupLink = { groupInfo ->
|
hideKeyboard(view)
|
||||||
hideKeyboard(view)
|
withBGApi {
|
||||||
withBGApi {
|
val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId)
|
||||||
val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId)
|
ModalManager.end.closeModals()
|
||||||
ModalManager.end.closeModals()
|
ModalManager.end.showModalCloseable(true) {
|
||||||
ModalManager.end.showModalCloseable(true) {
|
GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
|
||||||
GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
markRead = { range, unreadCountAfter ->
|
||||||
markRead = { range, unreadCountAfter ->
|
chatModel.markChatItemsRead(chat, range, unreadCountAfter)
|
||||||
chatModel.markChatItemsRead(chat, range, unreadCountAfter)
|
ntfManager.cancelNotificationsForChat(chat.id)
|
||||||
ntfManager.cancelNotificationsForChat(chat.id)
|
withBGApi {
|
||||||
withBGApi {
|
chatModel.controller.apiChatRead(
|
||||||
chatModel.controller.apiChatRead(
|
chatRh,
|
||||||
chatRh,
|
chat.chatInfo.chatType,
|
||||||
chat.chatInfo.chatType,
|
chat.chatInfo.apiId,
|
||||||
chat.chatInfo.apiId,
|
range
|
||||||
range
|
)
|
||||||
)
|
}
|
||||||
}
|
},
|
||||||
},
|
changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) },
|
||||||
changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) },
|
onSearchValueChanged = { value ->
|
||||||
onSearchValueChanged = { value ->
|
if (searchText.value == value) return@ChatLayout
|
||||||
if (searchText.value == value) return@ChatLayout
|
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
||||||
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
|
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout
|
||||||
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout
|
withBGApi {
|
||||||
withBGApi {
|
apiFindMessages(c, chatModel, value)
|
||||||
apiFindMessages(c, chatModel, value)
|
searchText.value = value
|
||||||
searchText.value = value
|
}
|
||||||
}
|
},
|
||||||
},
|
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 -> {
|
||||||
val close = { chatModel.chatId.value = null }
|
val close = { chatModel.chatId.value = null }
|
||||||
@@ -533,6 +541,7 @@ fun ChatLayout(
|
|||||||
acceptCall: (Contact) -> Unit,
|
acceptCall: (Contact) -> Unit,
|
||||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||||
openDirectChat: (Long) -> Unit,
|
openDirectChat: (Long) -> Unit,
|
||||||
|
forwardItem: (ChatInfo, ChatItem) -> Unit,
|
||||||
updateContactStats: (Contact) -> Unit,
|
updateContactStats: (Contact) -> Unit,
|
||||||
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
||||||
syncContactConnection: (Contact) -> Unit,
|
syncContactConnection: (Contact) -> Unit,
|
||||||
@@ -548,6 +557,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 } }
|
||||||
@@ -596,17 +606,27 @@ fun ChatLayout(
|
|||||||
floatingActionButton = { floatingButton.value() },
|
floatingActionButton = { floatingButton.value() },
|
||||||
contentColor = LocalContentColor.current,
|
contentColor = LocalContentColor.current,
|
||||||
drawerContentColor = LocalContentColor.current,
|
drawerContentColor = LocalContentColor.current,
|
||||||
|
backgroundColor = Color.Unspecified
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
|
val wallpaperImage = MaterialTheme.wallpaper.type.image
|
||||||
|
val wallpaperType = MaterialTheme.wallpaper.type
|
||||||
|
val backgroundColor = MaterialTheme.wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, MaterialTheme.colors.background)
|
||||||
|
val tintColor = MaterialTheme.wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base)
|
||||||
BoxWithConstraints(Modifier
|
BoxWithConstraints(Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colors.background)
|
||||||
|
.then(if (wallpaperImage != null)
|
||||||
|
Modifier.drawBehind { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) }
|
||||||
|
else
|
||||||
|
Modifier)
|
||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
) {
|
) {
|
||||||
ChatItemsList(
|
ChatItemsList(
|
||||||
chat, unreadCount, composeState, searchValue,
|
chat, unreadCount, composeState, searchValue,
|
||||||
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
|
||||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
|
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
|
||||||
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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -873,6 +893,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
acceptCall: (Contact) -> Unit,
|
acceptCall: (Contact) -> Unit,
|
||||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||||
openDirectChat: (Long) -> Unit,
|
openDirectChat: (Long) -> Unit,
|
||||||
|
forwardItem: (ChatInfo, ChatItem) -> Unit,
|
||||||
updateContactStats: (Contact) -> Unit,
|
updateContactStats: (Contact) -> Unit,
|
||||||
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
||||||
syncContactConnection: (Contact) -> Unit,
|
syncContactConnection: (Contact) -> Unit,
|
||||||
@@ -885,6 +906,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 +997,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, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1259,7 +1281,7 @@ val MEMBER_IMAGE_SIZE: Dp = 38.dp
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MemberImage(member: GroupMember) {
|
fun MemberImage(member: GroupMember) {
|
||||||
ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image)
|
ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image, backgroundColor = MaterialTheme.colors.background)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1528,6 +1550,7 @@ fun PreviewChatLayout() {
|
|||||||
acceptCall = { _ -> },
|
acceptCall = { _ -> },
|
||||||
acceptFeature = { _, _, _ -> },
|
acceptFeature = { _, _, _ -> },
|
||||||
openDirectChat = { _ -> },
|
openDirectChat = { _ -> },
|
||||||
|
forwardItem = { _, _ -> },
|
||||||
updateContactStats = { },
|
updateContactStats = { },
|
||||||
updateMemberStats = { _, _ -> },
|
updateMemberStats = { _, _ -> },
|
||||||
syncContactConnection = { },
|
syncContactConnection = { },
|
||||||
@@ -1543,6 +1566,7 @@ fun PreviewChatLayout() {
|
|||||||
onSearchValueChanged = {},
|
onSearchValueChanged = {},
|
||||||
onComposed = {},
|
onComposed = {},
|
||||||
developerTools = false,
|
developerTools = false,
|
||||||
|
showViaProxy = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1600,6 +1624,7 @@ fun PreviewGroupChatLayout() {
|
|||||||
acceptCall = { _ -> },
|
acceptCall = { _ -> },
|
||||||
acceptFeature = { _, _, _ -> },
|
acceptFeature = { _, _, _ -> },
|
||||||
openDirectChat = { _ -> },
|
openDirectChat = { _ -> },
|
||||||
|
forwardItem = { _, _ -> },
|
||||||
updateContactStats = { },
|
updateContactStats = { },
|
||||||
updateMemberStats = { _, _ -> },
|
updateMemberStats = { _, _ -> },
|
||||||
syncContactConnection = { },
|
syncContactConnection = { },
|
||||||
@@ -1615,6 +1640,7 @@ fun PreviewGroupChatLayout() {
|
|||||||
onSearchValueChanged = {},
|
onSearchValueChanged = {},
|
||||||
onComposed = {},
|
onComposed = {},
|
||||||
developerTools = false,
|
developerTools = false,
|
||||||
|
showViaProxy = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ComposeContextInvitingContactMemberView() {
|
fun ComposeContextInvitingContactMemberView() {
|
||||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
val sentColor = MaterialTheme.appColors.sentMessage
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.height(60.dp)
|
.height(60.dp)
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ import chat.simplex.res.MR
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) {
|
fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) {
|
||||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
val sentColor = MaterialTheme.appColors.sentMessage
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.height(60.dp)
|
.height(60.dp)
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ import chat.simplex.common.views.helpers.UploadContent
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) {
|
fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) {
|
||||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
val sentColor = MaterialTheme.appColors.sentMessage
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
|
|||||||
+8
-7
@@ -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
|
||||||
@@ -775,7 +776,7 @@ fun ComposeView(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MsgNotAllowedView(reason: String, icon: Painter) {
|
fun MsgNotAllowedView(reason: String, icon: Painter) {
|
||||||
val color = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val color = MaterialTheme.appColors.receivedMessage
|
||||||
Row(Modifier.padding(top = 5.dp).fillMaxWidth().background(color).padding(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF * 1.5f), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.padding(top = 5.dp).fillMaxWidth().background(color).padding(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF * 1.5f), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(icon, null, tint = MaterialTheme.colors.secondary)
|
Icon(icon, null, tint = MaterialTheme.colors.secondary)
|
||||||
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
|
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
|
||||||
@@ -861,7 +862,7 @@ fun ComposeView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
modifier = Modifier.background(MaterialTheme.colors.background).padding(end = 8.dp),
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.Bottom,
|
||||||
) {
|
) {
|
||||||
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
|
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
|
||||||
@@ -973,7 +974,7 @@ fun ComposeView(
|
|||||||
val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) }
|
val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) }
|
||||||
val sendButtonColor =
|
val sendButtonColor =
|
||||||
if (chat.chatInfo.incognito)
|
if (chat.chatInfo.incognito)
|
||||||
if (isSystemInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F)
|
if (isInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F)
|
||||||
else MaterialTheme.colors.primary
|
else MaterialTheme.colors.primary
|
||||||
SendMsgView(
|
SendMsgView(
|
||||||
composeState,
|
composeState,
|
||||||
|
|||||||
+1
-1
@@ -35,7 +35,7 @@ fun ComposeVoiceView(
|
|||||||
) {
|
) {
|
||||||
val progress = rememberSaveable { mutableStateOf(0) }
|
val progress = rememberSaveable { mutableStateOf(0) }
|
||||||
val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) }
|
val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) }
|
||||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
val sentColor = MaterialTheme.appColors.sentMessage
|
||||||
Box {
|
Box {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
+2
-2
@@ -32,8 +32,8 @@ fun ContactPreferencesView(
|
|||||||
) {
|
) {
|
||||||
val contact = remember { derivedStateOf { (m.getContactChat(contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }
|
val contact = remember { derivedStateOf { (m.getContactChat(contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }
|
||||||
val ct = contact.value ?: return
|
val ct = contact.value ?: return
|
||||||
var featuresAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(contactUserPrefsToFeaturesAllowed(ct.mergedPreferences)) }
|
var featuresAllowed by rememberSaveable(ct, user, stateSaver = serializableSaver()) { mutableStateOf(contactUserPrefsToFeaturesAllowed(ct.mergedPreferences)) }
|
||||||
var currentFeaturesAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(featuresAllowed) }
|
var currentFeaturesAllowed by rememberSaveable(ct, user, stateSaver = serializableSaver()) { mutableStateOf(featuresAllowed) }
|
||||||
|
|
||||||
fun savePrefs(afterSave: () -> Unit = {}) {
|
fun savePrefs(afterSave: () -> Unit = {}) {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
|
|||||||
+2
-2
@@ -30,8 +30,8 @@ fun ContextItemView(
|
|||||||
cancelContextItem: () -> Unit
|
cancelContextItem: () -> Unit
|
||||||
) {
|
) {
|
||||||
val sent = contextItem.chatDir.sent
|
val sent = contextItem.chatDir.sent
|
||||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
val sentColor = MaterialTheme.appColors.sentMessage
|
||||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageText(attachment: ImageResource?, lines: Int) {
|
fun MessageText(attachment: ImageResource?, lines: Int) {
|
||||||
|
|||||||
+1
-1
@@ -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 &&
|
||||||
|
|||||||
+10
-1
@@ -9,7 +9,6 @@ import SectionSpacer
|
|||||||
import SectionTextFooter
|
import SectionTextFooter
|
||||||
import SectionView
|
import SectionView
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.*
|
import androidx.compose.foundation.lazy.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
@@ -233,6 +232,16 @@ fun GroupChatInfoLayout(
|
|||||||
} else {
|
} else {
|
||||||
SendReceiptsOptionDisabled()
|
SendReceiptsOptionDisabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WallpaperButton {
|
||||||
|
ModalManager.end.showModal {
|
||||||
|
val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } }
|
||||||
|
val c = chat.value
|
||||||
|
if (c != null) {
|
||||||
|
ChatWallpaperEditorModal(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs))
|
SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs))
|
||||||
SectionDividerSpaced(maxTopPadding = true)
|
SectionDividerSpaced(maxTopPadding = true)
|
||||||
|
|||||||
+6
-9
@@ -123,13 +123,12 @@ private fun GroupPreferencesLayout(
|
|||||||
applyPrefs(preferences.copy(files = RoleGroupPreference(enable = enable, role)))
|
applyPrefs(preferences.copy(files = RoleGroupPreference(enable = enable, role)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO enable simplexLinks preference in 5.8
|
SectionDividerSpaced(true, maxBottomPadding = false)
|
||||||
// SectionDividerSpaced(true, maxBottomPadding = false)
|
val allowSimplexLinks = remember(preferences) { mutableStateOf(preferences.simplexLinks.enable) }
|
||||||
// val allowSimplexLinks = remember(preferences) { mutableStateOf(preferences.simplexLinks.enable) }
|
val simplexLinksRole = remember(preferences) { mutableStateOf(preferences.simplexLinks.role) }
|
||||||
// val simplexLinksRole = remember(preferences) { mutableStateOf(preferences.simplexLinks.role) }
|
FeatureSection(GroupFeature.SimplexLinks, allowSimplexLinks, simplexLinksRole, groupInfo, preferences, onTTLUpdated) { enable, role ->
|
||||||
// FeatureSection(GroupFeature.SimplexLinks, allowSimplexLinks, simplexLinksRole, groupInfo, preferences, onTTLUpdated) { enable, role ->
|
applyPrefs(preferences.copy(simplexLinks = RoleGroupPreference(enable = enable, role)))
|
||||||
// applyPrefs(preferences.copy(simplexLinks = RoleGroupPreference(enable = enable, role)))
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
SectionDividerSpaced(true, maxBottomPadding = false)
|
SectionDividerSpaced(true, maxBottomPadding = false)
|
||||||
val enableHistory = remember(preferences) { mutableStateOf(preferences.history.enable) }
|
val enableHistory = remember(preferences) { mutableStateOf(preferences.history.enable) }
|
||||||
@@ -189,8 +188,6 @@ private fun FeatureSection(
|
|||||||
generalGetString(MR.strings.feature_enabled_for),
|
generalGetString(MR.strings.feature_enabled_for),
|
||||||
featureRoles,
|
featureRoles,
|
||||||
enableForRole,
|
enableForRole,
|
||||||
// remove in v5.8
|
|
||||||
enabled = remember { mutableStateOf(false) },
|
|
||||||
onSelected = { value ->
|
onSelected = { value ->
|
||||||
onSelected(enableFeature.value, value)
|
onSelected(enableFeature.value, value)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-7
@@ -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,13 @@ fun CIFileView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
Modifier.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),
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.Bottom,
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
|
|||||||
+6
-5
@@ -83,8 +83,8 @@ fun CIGroupInvitationView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
val sentColor = MaterialTheme.appColors.sentMessage
|
||||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||||
Surface(
|
Surface(
|
||||||
modifier = if (action && !inProgress.value) Modifier.clickable(onClick = {
|
modifier = if (action && !inProgress.value) Modifier.clickable(onClick = {
|
||||||
inProgress.value = true
|
inProgress.value = true
|
||||||
@@ -110,6 +110,7 @@ fun CIGroupInvitationView(
|
|||||||
.padding(bottom = 4.dp),
|
.padding(bottom = 4.dp),
|
||||||
) {
|
) {
|
||||||
groupInfoView()
|
groupInfoView()
|
||||||
|
val secondaryColor = MaterialTheme.colors.secondary
|
||||||
Column(Modifier.padding(top = 2.dp, start = 5.dp)) {
|
Column(Modifier.padding(top = 2.dp, start = 5.dp)) {
|
||||||
Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp))
|
Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp))
|
||||||
if (action) {
|
if (action) {
|
||||||
@@ -117,7 +118,7 @@ fun CIGroupInvitationView(
|
|||||||
Text(
|
Text(
|
||||||
buildAnnotatedString {
|
buildAnnotatedString {
|
||||||
append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join))
|
append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join))
|
||||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false)) }
|
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) }
|
||||||
},
|
},
|
||||||
color = if (inProgress.value)
|
color = if (inProgress.value)
|
||||||
MaterialTheme.colors.secondary
|
MaterialTheme.colors.secondary
|
||||||
@@ -128,7 +129,7 @@ fun CIGroupInvitationView(
|
|||||||
Text(
|
Text(
|
||||||
buildAnnotatedString {
|
buildAnnotatedString {
|
||||||
append(groupInvitationStr())
|
append(groupInvitationStr())
|
||||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false)) }
|
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -144,7 +145,7 @@ fun CIGroupInvitationView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false)
|
CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-21
@@ -2,8 +2,7 @@ package chat.simplex.common.views.chat.item
|
|||||||
|
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -22,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
|
||||||
@@ -52,7 +46,7 @@ fun CIImageView(
|
|||||||
icon,
|
icon,
|
||||||
stringResource(stringId),
|
stringResource(stringId),
|
||||||
Modifier.fillMaxSize(),
|
Modifier.fillMaxSize(),
|
||||||
tint = metaColor
|
tint = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,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)
|
||||||
@@ -113,21 +108,49 @@ fun CIImageView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ImageView(painter: Painter, onClick: () -> Unit) {
|
fun ImageView(painter: Painter, image: String, fileSource: CryptoFile?, onClick: () -> Unit) {
|
||||||
Image(
|
// On my Android device Compose fails to display 6000x6000 px WebP image with exception:
|
||||||
painter,
|
// IllegalStateException: Recording currently in progress - missing #endRecording() call?
|
||||||
contentDescription = stringResource(MR.strings.image_descr),
|
// but can display 5000px image. Using even lower value here just to feel safer.
|
||||||
// .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView
|
// It happens to WebP because it's not compressed while sending since it can be animated.
|
||||||
// if text is short and take all available width if text is long
|
if (painter.intrinsicSize.width <= 4320 && painter.intrinsicSize.height <= 4320) {
|
||||||
modifier = Modifier
|
Image(
|
||||||
|
painter,
|
||||||
|
contentDescription = stringResource(MR.strings.image_descr),
|
||||||
|
// .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView
|
||||||
|
// if text is short and take all available width if text is long
|
||||||
|
modifier = Modifier
|
||||||
|
.width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH)
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = { showMenu.value = true },
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
.onRightClick { showMenu.value = true },
|
||||||
|
contentScale = ContentScale.FillWidth,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(Modifier
|
||||||
.width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH)
|
.width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onLongClick = { showMenu.value = true },
|
onLongClick = { showMenu.value = true },
|
||||||
onClick = onClick
|
onClick = {}
|
||||||
)
|
)
|
||||||
.onRightClick { showMenu.value = true },
|
.onRightClick { showMenu.value = true },
|
||||||
contentScale = ContentScale.FillWidth,
|
contentAlignment = Alignment.Center
|
||||||
)
|
) {
|
||||||
|
imageView(base64ToBitmap(image), onClick = {
|
||||||
|
if (fileSource != null) {
|
||||||
|
openFile(fileSource)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Icon(
|
||||||
|
painterResource(MR.images.ic_open_in_new),
|
||||||
|
contentDescription = stringResource(MR.strings.image_descr),
|
||||||
|
modifier = Modifier.size(30.dp),
|
||||||
|
tint = MaterialTheme.colors.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fileSizeValid(): Boolean {
|
fun fileSizeValid(): Boolean {
|
||||||
@@ -172,14 +195,14 @@ fun CIImageView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val loaded = res.value
|
val loaded = res.value
|
||||||
if (loaded != null) {
|
if (loaded != null && file != null) {
|
||||||
val (imageBitmap, data, _) = loaded
|
val (imageBitmap, data, _) = loaded
|
||||||
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, onClick) })
|
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) })
|
||||||
} else {
|
} else {
|
||||||
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 {
|
||||||
|
|||||||
+41
-16
@@ -12,7 +12,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.common.ui.theme.CurrentColors
|
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.ui.theme.isInDarkTheme
|
import chat.simplex.common.ui.theme.isInDarkTheme
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
@@ -35,7 +34,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 +53,8 @@ fun CIMetaView(
|
|||||||
metaColor,
|
metaColor,
|
||||||
paleMetaColor,
|
paleMetaColor,
|
||||||
showStatus = showStatus,
|
showStatus = showStatus,
|
||||||
showEdited = showEdited
|
showEdited = showEdited,
|
||||||
|
showViaProxy = showViaProxy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +69,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 +84,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 = MaterialTheme.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 +110,15 @@ 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?,
|
||||||
|
secondaryColor: Color,
|
||||||
|
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,7 +129,10 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, showSt
|
|||||||
res += shortTimeText(ttl)
|
res += shortTimeText(ttl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showStatus && (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing)) {
|
if (showViaProxy && meta.sentViaProxy == true) {
|
||||||
|
res += iconSpace
|
||||||
|
}
|
||||||
|
if (showStatus && (meta.statusIcon(secondaryColor) != null || !meta.disappearing)) {
|
||||||
res += iconSpace
|
res += iconSpace
|
||||||
}
|
}
|
||||||
if (encrypted != null) {
|
if (encrypted != null) {
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-6
@@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.ui.theme.CurrentColors
|
import chat.simplex.common.ui.theme.CurrentColors
|
||||||
|
import chat.simplex.common.ui.theme.appColors
|
||||||
import chat.simplex.common.views.helpers.AlertManager
|
import chat.simplex.common.views.helpers.AlertManager
|
||||||
import chat.simplex.common.views.helpers.generalGetString
|
import chat.simplex.common.views.helpers.generalGetString
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
@@ -137,7 +138,7 @@ fun DecryptionErrorItemFixButton(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
syncSupported: Boolean
|
syncSupported: Boolean
|
||||||
) {
|
) {
|
||||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||||
Surface(
|
Surface(
|
||||||
Modifier.clickable(onClick = onClick),
|
Modifier.clickable(onClick = onClick),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
@@ -164,17 +165,18 @@ fun DecryptionErrorItemFixButton(
|
|||||||
tint = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
tint = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||||
)
|
)
|
||||||
Spacer(Modifier.padding(2.dp))
|
Spacer(Modifier.padding(2.dp))
|
||||||
|
val secondaryColor = MaterialTheme.colors.secondary
|
||||||
Text(
|
Text(
|
||||||
buildAnnotatedString {
|
buildAnnotatedString {
|
||||||
append(generalGetString(MR.strings.fix_connection))
|
append(generalGetString(MR.strings.fix_connection))
|
||||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) }
|
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) }
|
||||||
withStyle(reserveTimestampStyle) { append(" ") } // for icon
|
withStyle(reserveTimestampStyle) { append(" ") } // for icon
|
||||||
},
|
},
|
||||||
color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CIMetaView(ci, timedMessagesTTL = null)
|
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,7 +186,7 @@ fun DecryptionErrorItem(
|
|||||||
ci: ChatItem,
|
ci: ChatItem,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||||
Surface(
|
Surface(
|
||||||
Modifier.clickable(onClick = onClick),
|
Modifier.clickable(onClick = onClick),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
@@ -195,14 +197,15 @@ fun DecryptionErrorItem(
|
|||||||
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
|
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
|
||||||
contentAlignment = Alignment.BottomEnd,
|
contentAlignment = Alignment.BottomEnd,
|
||||||
) {
|
) {
|
||||||
|
val secondaryColor = MaterialTheme.colors.secondary
|
||||||
Text(
|
Text(
|
||||||
buildAnnotatedString {
|
buildAnnotatedString {
|
||||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
|
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
|
||||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) }
|
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) }
|
||||||
},
|
},
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -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)
|
||||||
|
|||||||
+15
-9
@@ -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,
|
||||||
@@ -151,8 +154,8 @@ private fun VoiceLayout(
|
|||||||
}
|
}
|
||||||
when {
|
when {
|
||||||
hasText -> {
|
hasText -> {
|
||||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
val sentColor = MaterialTheme.appColors.sentMessage
|
||||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||||
Spacer(Modifier.width(6.dp))
|
Spacer(Modifier.width(6.dp))
|
||||||
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)
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
@@ -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,10 +221,11 @@ 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 = MaterialTheme.appColors.sentMessage
|
||||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||||
Surface(
|
Surface(
|
||||||
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
||||||
color = if (sent) sentColor else receivedColor,
|
color = if (sent) sentColor else receivedColor,
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-66
@@ -59,6 +59,7 @@ fun ChatItemView(
|
|||||||
scrollToItem: (Long) -> Unit,
|
scrollToItem: (Long) -> Unit,
|
||||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||||
openDirectChat: (Long) -> Unit,
|
openDirectChat: (Long) -> Unit,
|
||||||
|
forwardItem: (ChatInfo, ChatItem) -> Unit,
|
||||||
updateContactStats: (Contact) -> Unit,
|
updateContactStats: (Contact) -> Unit,
|
||||||
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
||||||
syncContactConnection: (Contact) -> Unit,
|
syncContactConnection: (Contact) -> Unit,
|
||||||
@@ -68,6 +69,8 @@ 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,
|
||||||
|
preview: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val sent = cItem.chatDir.sent
|
val sent = cItem.chatDir.sent
|
||||||
@@ -83,17 +86,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 +131,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 +205,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,12 +258,11 @@ 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 = {
|
||||||
chatModel.chatId.value = null
|
forwardItem(cInfo, cItem)
|
||||||
chatModel.sharedContent.value = SharedContent.Forward(cItem, cInfo)
|
|
||||||
showMenu.value = false
|
showMenu.value = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -277,7 +273,7 @@ fun ChatItemView(
|
|||||||
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null && !cItem.localNote) {
|
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null && !cItem.localNote) {
|
||||||
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
|
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
|
||||||
}
|
}
|
||||||
if (!(live && cItem.meta.isLive)) {
|
if (!(live && cItem.meta.isLive) && !preview) {
|
||||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||||
}
|
}
|
||||||
val groupInfo = cItem.memberToModerate(cInfo)?.first
|
val groupInfo = cItem.memberToModerate(cInfo)?.first
|
||||||
@@ -338,14 +334,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 +353,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 +406,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,50 +816,44 @@ 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
|
||||||
@Composable
|
@Composable
|
||||||
fun PreviewChatItemView() {
|
fun PreviewChatItemView(
|
||||||
SimpleXTheme {
|
chatItem: ChatItem = ChatItem.getSampleData(1, CIDirection.DirectSnd(), Clock.System.now(), "hello")
|
||||||
ChatItemView(
|
) {
|
||||||
rhId = null,
|
ChatItemView(
|
||||||
ChatInfo.Direct.sampleData,
|
rhId = null,
|
||||||
ChatItem.getSampleData(
|
ChatInfo.Direct.sampleData,
|
||||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
|
chatItem,
|
||||||
),
|
useLinkPreviews = true,
|
||||||
useLinkPreviews = true,
|
linkMode = SimplexLinkMode.DESCRIPTION,
|
||||||
linkMode = SimplexLinkMode.DESCRIPTION,
|
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
revealed = remember { mutableStateOf(false) },
|
||||||
revealed = remember { mutableStateOf(false) },
|
range = 0..1,
|
||||||
range = 0..1,
|
deleteMessage = { _, _ -> },
|
||||||
deleteMessage = { _, _ -> },
|
deleteMessages = { _ -> },
|
||||||
deleteMessages = { _ -> },
|
receiveFile = { _ -> },
|
||||||
receiveFile = { _ -> },
|
cancelFile = {},
|
||||||
cancelFile = {},
|
joinGroup = { _, _ -> },
|
||||||
joinGroup = { _, _ -> },
|
acceptCall = { _ -> },
|
||||||
acceptCall = { _ -> },
|
scrollToItem = {},
|
||||||
scrollToItem = {},
|
acceptFeature = { _, _, _ -> },
|
||||||
acceptFeature = { _, _, _ -> },
|
openDirectChat = { _ -> },
|
||||||
openDirectChat = { _ -> },
|
forwardItem = { _, _ -> },
|
||||||
updateContactStats = { },
|
updateContactStats = { },
|
||||||
updateMemberStats = { _, _ -> },
|
updateMemberStats = { _, _ -> },
|
||||||
syncContactConnection = { },
|
syncContactConnection = { },
|
||||||
syncMemberConnection = { _, _ -> },
|
syncMemberConnection = { _, _ -> },
|
||||||
findModelChat = { null },
|
findModelChat = { null },
|
||||||
findModelMember = { null },
|
findModelMember = { null },
|
||||||
setReaction = { _, _, _, _ -> },
|
setReaction = { _, _, _, _ -> },
|
||||||
showItemDetails = { _, _ -> },
|
showItemDetails = { _, _ -> },
|
||||||
developerTools = false,
|
developerTools = false,
|
||||||
)
|
showViaProxy = false,
|
||||||
}
|
preview = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@@ -888,6 +878,7 @@ fun PreviewChatItemViewDeletedContent() {
|
|||||||
scrollToItem = {},
|
scrollToItem = {},
|
||||||
acceptFeature = { _, _, _ -> },
|
acceptFeature = { _, _, _ -> },
|
||||||
openDirectChat = { _ -> },
|
openDirectChat = { _ -> },
|
||||||
|
forwardItem = { _, _ -> },
|
||||||
updateContactStats = { },
|
updateContactStats = { },
|
||||||
updateMemberStats = { _, _ -> },
|
updateMemberStats = { _, _ -> },
|
||||||
syncContactConnection = { },
|
syncContactConnection = { },
|
||||||
@@ -897,6 +888,8 @@ fun PreviewChatItemViewDeletedContent() {
|
|||||||
setReaction = { _, _, _, _ -> },
|
setReaction = { _, _, _, _ -> },
|
||||||
showItemDetails = { _, _ -> },
|
showItemDetails = { _, _ -> },
|
||||||
developerTools = false,
|
developerTools = false,
|
||||||
|
showViaProxy = false,
|
||||||
|
preview = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-5
@@ -16,10 +16,10 @@ 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 = MaterialTheme.appColors.sentMessage
|
||||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
color = if (sent) sentColor else receivedColor,
|
color = if (sent) sentColor else receivedColor,
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-25
@@ -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 = {},
|
||||||
@@ -46,9 +46,6 @@ fun FramedItemView(
|
|||||||
return if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.membership else null
|
return if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.membership else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Color.toQuote(): Color = if (isInDarkTheme()) lighter(0.12f) else darker(0.12f)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) {
|
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) {
|
||||||
MarkdownText(
|
MarkdownText(
|
||||||
@@ -89,11 +86,11 @@ fun FramedItemView(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null, pad: Boolean = false) {
|
fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null, pad: Boolean = false) {
|
||||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
val sentColor = MaterialTheme.appColors.sentQuote
|
||||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val receivedColor = MaterialTheme.appColors.receivedQuote
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.background(if (sent) sentColor.toQuote() else receivedColor.toQuote())
|
.background(if (sent) sentColor else receivedColor)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (pad || (ci.quotedItem == null && ci.meta.itemForwarded == null)) 6.dp else 0.dp),
|
.padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (pad || (ci.quotedItem == null && ci.meta.itemForwarded == null)) 6.dp else 0.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
@@ -122,11 +119,11 @@ fun FramedItemView(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ciQuoteView(qi: CIQuote) {
|
fun ciQuoteView(qi: CIQuote) {
|
||||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
val sentColor = MaterialTheme.appColors.sentQuote
|
||||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val receivedColor = MaterialTheme.appColors.receivedQuote
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.background(if (sent) sentColor.toQuote() else receivedColor.toQuote())
|
.background(if (sent) sentColor else receivedColor)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onLongClick = { showMenu.value = true },
|
onLongClick = { showMenu.value = true },
|
||||||
@@ -179,19 +176,26 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVideo) &&
|
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVideo) &&
|
||||||
!ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null && ci.meta.itemForwarded == null
|
!ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null && ci.meta.itemForwarded == null
|
||||||
|
|
||||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
val sentColor = MaterialTheme.appColors.sentMessage
|
||||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||||
Box(Modifier
|
Box(Modifier
|
||||||
.clip(RoundedCornerShape(18.dp))
|
.clip(RoundedCornerShape(18.dp))
|
||||||
|
.background(
|
||||||
|
when {
|
||||||
|
transparentBackground -> Color.Transparent
|
||||||
|
sent -> MaterialTheme.colors.background
|
||||||
|
else -> MaterialTheme.colors.background
|
||||||
|
}
|
||||||
|
)
|
||||||
.background(
|
.background(
|
||||||
when {
|
when {
|
||||||
transparentBackground -> Color.Transparent
|
transparentBackground -> Color.Transparent
|
||||||
@@ -241,11 +245,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 +257,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 +297,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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-4
@@ -17,8 +17,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.common.model.ChatItem
|
import chat.simplex.common.model.ChatItem
|
||||||
import chat.simplex.common.model.MsgErrorType
|
import chat.simplex.common.model.MsgErrorType
|
||||||
import chat.simplex.common.ui.theme.CurrentColors
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.ui.theme.SimpleXTheme
|
|
||||||
import chat.simplex.common.views.helpers.AlertManager
|
import chat.simplex.common.views.helpers.AlertManager
|
||||||
import chat.simplex.common.views.helpers.generalGetString
|
import chat.simplex.common.views.helpers.generalGetString
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
@@ -51,7 +50,7 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
|
fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
|
||||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||||
Surface(
|
Surface(
|
||||||
Modifier.clickable(onClick = onClick),
|
Modifier.clickable(onClick = onClick),
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
@@ -69,7 +68,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-5
@@ -20,9 +20,9 @@ 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 = MaterialTheme.appColors.sentMessage
|
||||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(18.dp),
|
shape = RoundedCornerShape(18.dp),
|
||||||
color = if (ci.chatDir.sent) sentColor else receivedColor,
|
color = if (ci.chatDir.sent) sentColor else receivedColor,
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -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, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy)
|
||||||
} else {
|
} else {
|
||||||
" "
|
" "
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -486,6 +486,8 @@ fun deleteChatDatabaseFilesAndState() {
|
|||||||
tmpDir.deleteRecursively()
|
tmpDir.deleteRecursively()
|
||||||
getMigrationTempFilesDirectory().deleteRecursively()
|
getMigrationTempFilesDirectory().deleteRecursively()
|
||||||
tmpDir.mkdir()
|
tmpDir.mkdir()
|
||||||
|
wallpapersDir.deleteRecursively()
|
||||||
|
wallpapersDir.mkdirs()
|
||||||
DatabaseUtils.ksDatabasePassword.remove()
|
DatabaseUtils.ksDatabasePassword.remove()
|
||||||
controller.appPrefs.storeDBPassphrase.set(true)
|
controller.appPrefs.storeDBPassphrase.set(true)
|
||||||
controller.ctrl = null
|
controller.ctrl = null
|
||||||
|
|||||||
+7
-1
@@ -2,12 +2,14 @@ package chat.simplex.common.views.helpers
|
|||||||
|
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.*
|
import androidx.compose.foundation.shape.*
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
@@ -51,7 +53,8 @@ fun ProfileImage(
|
|||||||
size: Dp,
|
size: Dp,
|
||||||
image: String? = null,
|
image: String? = null,
|
||||||
icon: ImageResource = MR.images.ic_account_circle_filled,
|
icon: ImageResource = MR.images.ic_account_circle_filled,
|
||||||
color: Color = MaterialTheme.colors.secondaryVariant
|
color: Color = MaterialTheme.colors.secondaryVariant,
|
||||||
|
backgroundColor: Color? = null
|
||||||
) {
|
) {
|
||||||
Box(Modifier.size(size)) {
|
Box(Modifier.size(size)) {
|
||||||
if (image == null) {
|
if (image == null) {
|
||||||
@@ -61,6 +64,9 @@ fun ProfileImage(
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
if (iconToReplace != null) {
|
if (iconToReplace != null) {
|
||||||
|
if (backgroundColor != null) {
|
||||||
|
Box(Modifier.size(size * 0.7f).align(Alignment.Center).background(backgroundColor, CircleShape))
|
||||||
|
}
|
||||||
Icon(
|
Icon(
|
||||||
iconToReplace,
|
iconToReplace,
|
||||||
contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder),
|
contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder),
|
||||||
|
|||||||
+418
@@ -0,0 +1,418 @@
|
|||||||
|
package chat.simplex.common.views.helpers
|
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.*
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.ImageResource
|
||||||
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.math.*
|
||||||
|
|
||||||
|
enum class PresetWallpaper(
|
||||||
|
val res: ImageResource,
|
||||||
|
val filename: String,
|
||||||
|
val text: StringResource,
|
||||||
|
val scale: Float,
|
||||||
|
val background: Map<DefaultTheme, Color>,
|
||||||
|
val tint: Map<DefaultTheme, Color>,
|
||||||
|
val colors: Map<DefaultTheme, ThemeColors>,
|
||||||
|
) {
|
||||||
|
CATS(MR.images.wallpaper_cats, "cats", MR.strings.wallpaper_cats, 0.63f,
|
||||||
|
wallpaperBackgrounds(light = "#ffF8F6EA"),
|
||||||
|
tint = mapOf(
|
||||||
|
DefaultTheme.LIGHT to "#ffefdca6".colorFromReadableHex(),
|
||||||
|
DefaultTheme.DARK to "#ff4b3b0e".colorFromReadableHex(),
|
||||||
|
DefaultTheme.SIMPLEX to "#ff51400f".colorFromReadableHex(),
|
||||||
|
DefaultTheme.BLACK to "#ff4b3b0e".colorFromReadableHex()
|
||||||
|
),
|
||||||
|
mapOf(
|
||||||
|
DefaultTheme.LIGHT to ThemeColors(
|
||||||
|
sentMessage = "#fffffaed",
|
||||||
|
sentQuote = "#fffaf0d6",
|
||||||
|
receivedMessage = "#ffF8F7F4",
|
||||||
|
receivedQuote = "#ffefede9",
|
||||||
|
),
|
||||||
|
DefaultTheme.DARK to ThemeColors(
|
||||||
|
sentMessage = "#ff2f2919",
|
||||||
|
sentQuote = "#ff473a1d",
|
||||||
|
receivedMessage = "#ff272624",
|
||||||
|
receivedQuote = "#ff373633",
|
||||||
|
),
|
||||||
|
DefaultTheme.SIMPLEX to ThemeColors(
|
||||||
|
sentMessage = "#ff41371b",
|
||||||
|
sentQuote = "#ff654f1c",
|
||||||
|
receivedMessage = "#ff272624",
|
||||||
|
receivedQuote = "#ff373633",
|
||||||
|
),
|
||||||
|
DefaultTheme.BLACK to ThemeColors(
|
||||||
|
sentMessage = "#ff41371b",
|
||||||
|
sentQuote = "#ff654f1c",
|
||||||
|
receivedMessage = "#ff1f1e1b",
|
||||||
|
receivedQuote = "#ff2f2d27",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
FLOWERS(MR.images.wallpaper_flowers, "flowers", MR.strings.wallpaper_flowers, 0.53f,
|
||||||
|
wallpaperBackgrounds(light = "#ffE2FFE4"),
|
||||||
|
tint = mapOf(
|
||||||
|
DefaultTheme.LIGHT to "#ff9CEA59".colorFromReadableHex(),
|
||||||
|
DefaultTheme.DARK to "#ff31560D".colorFromReadableHex(),
|
||||||
|
DefaultTheme.SIMPLEX to "#ff36600f".colorFromReadableHex(),
|
||||||
|
DefaultTheme.BLACK to "#ff31560D".colorFromReadableHex()
|
||||||
|
),
|
||||||
|
mapOf(
|
||||||
|
DefaultTheme.LIGHT to ThemeColors(
|
||||||
|
sentMessage = "#fff1ffe5",
|
||||||
|
sentQuote = "#ffdcf9c4",
|
||||||
|
receivedMessage = "#ffF4F8F2",
|
||||||
|
receivedQuote = "#ffe7ece7",
|
||||||
|
),
|
||||||
|
DefaultTheme.DARK to ThemeColors(
|
||||||
|
sentMessage = "#ff163521",
|
||||||
|
sentQuote = "#ff1B5330",
|
||||||
|
receivedMessage = "#ff242523",
|
||||||
|
receivedQuote = "#ff353733",
|
||||||
|
),
|
||||||
|
DefaultTheme.SIMPLEX to ThemeColors(
|
||||||
|
sentMessage = "#ff184739",
|
||||||
|
sentQuote = "#ff1F6F4B",
|
||||||
|
receivedMessage = "#ff242523",
|
||||||
|
receivedQuote = "#ff353733",
|
||||||
|
),
|
||||||
|
DefaultTheme.BLACK to ThemeColors(
|
||||||
|
sentMessage = "#ff184739",
|
||||||
|
sentQuote = "#ff1F6F4B",
|
||||||
|
receivedMessage = "#ff1c1f1a",
|
||||||
|
receivedQuote = "#ff282b25",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
HEARTS(MR.images.wallpaper_hearts, "hearts", MR.strings.wallpaper_hearts, 0.59f,
|
||||||
|
wallpaperBackgrounds(light = "#ffFDECEC"),
|
||||||
|
tint = mapOf(
|
||||||
|
DefaultTheme.LIGHT to "#fffde0e0".colorFromReadableHex(),
|
||||||
|
DefaultTheme.DARK to "#ff3c0f0f".colorFromReadableHex(),
|
||||||
|
DefaultTheme.SIMPLEX to "#ff411010".colorFromReadableHex(),
|
||||||
|
DefaultTheme.BLACK to "#ff3C0F0F".colorFromReadableHex()
|
||||||
|
),
|
||||||
|
mapOf(
|
||||||
|
DefaultTheme.LIGHT to ThemeColors(
|
||||||
|
sentMessage = "#fffff4f4",
|
||||||
|
sentQuote = "#ffffdfdf",
|
||||||
|
receivedMessage = "#fff8f6f6",
|
||||||
|
receivedQuote = "#ffefebeb",
|
||||||
|
),
|
||||||
|
DefaultTheme.DARK to ThemeColors(
|
||||||
|
sentMessage = "#ff301515",
|
||||||
|
sentQuote = "#ff4C1818",
|
||||||
|
receivedMessage = "#ff242121",
|
||||||
|
receivedQuote = "#ff3b3535",
|
||||||
|
),
|
||||||
|
DefaultTheme.SIMPLEX to ThemeColors(
|
||||||
|
sentMessage = "#ff491A28",
|
||||||
|
sentQuote = "#ff761F29",
|
||||||
|
receivedMessage = "#ff242121",
|
||||||
|
receivedQuote = "#ff3b3535",
|
||||||
|
),
|
||||||
|
DefaultTheme.BLACK to ThemeColors(
|
||||||
|
sentMessage = "#ff491A28",
|
||||||
|
sentQuote = "#ff761F29",
|
||||||
|
receivedMessage = "#ff1f1b1b",
|
||||||
|
receivedQuote = "#ff2e2626",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
KIDS(MR.images.wallpaper_kids, "kids", MR.strings.wallpaper_kids, 0.53f,
|
||||||
|
wallpaperBackgrounds(light = "#ffdbfdfb"),
|
||||||
|
tint = mapOf(
|
||||||
|
DefaultTheme.LIGHT to "#ffadeffc".colorFromReadableHex(),
|
||||||
|
DefaultTheme.DARK to "#ff16404B".colorFromReadableHex(),
|
||||||
|
DefaultTheme.SIMPLEX to "#ff184753".colorFromReadableHex(),
|
||||||
|
DefaultTheme.BLACK to "#ff16404B".colorFromReadableHex()
|
||||||
|
),
|
||||||
|
mapOf(
|
||||||
|
DefaultTheme.LIGHT to ThemeColors(
|
||||||
|
sentMessage = "#ffeafeff",
|
||||||
|
sentQuote = "#ffcbf4f7",
|
||||||
|
receivedMessage = "#fff3fafa",
|
||||||
|
receivedQuote = "#ffe4efef",
|
||||||
|
),
|
||||||
|
DefaultTheme.DARK to ThemeColors(
|
||||||
|
sentMessage = "#ff16302F",
|
||||||
|
sentQuote = "#ff1a4a49",
|
||||||
|
receivedMessage = "#ff252626",
|
||||||
|
receivedQuote = "#ff373A39",
|
||||||
|
),
|
||||||
|
DefaultTheme.SIMPLEX to ThemeColors(
|
||||||
|
sentMessage = "#ff1a4745",
|
||||||
|
sentQuote = "#ff1d6b69",
|
||||||
|
receivedMessage = "#ff252626",
|
||||||
|
receivedQuote = "#ff373a39",
|
||||||
|
),
|
||||||
|
DefaultTheme.BLACK to ThemeColors(
|
||||||
|
sentMessage = "#ff1a4745",
|
||||||
|
sentQuote = "#ff1d6b69",
|
||||||
|
receivedMessage = "#ff1e1f1f",
|
||||||
|
receivedQuote = "#ff262b29",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SCHOOL(MR.images.wallpaper_school, "school", MR.strings.wallpaper_school, 0.53f,
|
||||||
|
wallpaperBackgrounds(light = "#ffE7F5FF"),
|
||||||
|
tint = mapOf(
|
||||||
|
DefaultTheme.LIGHT to "#ffCEEBFF".colorFromReadableHex(),
|
||||||
|
DefaultTheme.DARK to "#ff0F293B".colorFromReadableHex(),
|
||||||
|
DefaultTheme.SIMPLEX to "#ff112f43".colorFromReadableHex(),
|
||||||
|
DefaultTheme.BLACK to "#ff0F293B".colorFromReadableHex()
|
||||||
|
),
|
||||||
|
mapOf(
|
||||||
|
DefaultTheme.LIGHT to ThemeColors(
|
||||||
|
sentMessage = "#ffeef9ff",
|
||||||
|
sentQuote = "#ffD6EDFA",
|
||||||
|
receivedMessage = "#ffF3F5F9",
|
||||||
|
receivedQuote = "#ffe4e8ee",
|
||||||
|
),
|
||||||
|
DefaultTheme.DARK to ThemeColors(
|
||||||
|
sentMessage = "#ff172833",
|
||||||
|
sentQuote = "#ff1C3E4F",
|
||||||
|
receivedMessage = "#ff26282c",
|
||||||
|
receivedQuote = "#ff393c40",
|
||||||
|
),
|
||||||
|
DefaultTheme.SIMPLEX to ThemeColors(
|
||||||
|
sentMessage = "#ff1A3C5D",
|
||||||
|
sentQuote = "#ff235b80",
|
||||||
|
receivedMessage = "#ff26282c",
|
||||||
|
receivedQuote = "#ff393c40",
|
||||||
|
),
|
||||||
|
DefaultTheme.BLACK to ThemeColors(
|
||||||
|
sentMessage = "#ff1A3C5D",
|
||||||
|
sentQuote = "#ff235b80",
|
||||||
|
receivedMessage = "#ff1d1e22",
|
||||||
|
receivedQuote = "#ff292b2f",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
TRAVEL(MR.images.wallpaper_travel, "travel", MR.strings.wallpaper_travel, 0.68f,
|
||||||
|
wallpaperBackgrounds(light = "#fff9eeff"),
|
||||||
|
tint = mapOf(
|
||||||
|
DefaultTheme.LIGHT to "#ffeedbfe".colorFromReadableHex(),
|
||||||
|
DefaultTheme.DARK to "#ff311E48".colorFromReadableHex(),
|
||||||
|
DefaultTheme.SIMPLEX to "#ff35204e".colorFromReadableHex(),
|
||||||
|
DefaultTheme.BLACK to "#ff311E48".colorFromReadableHex()
|
||||||
|
),
|
||||||
|
mapOf(
|
||||||
|
DefaultTheme.LIGHT to ThemeColors(
|
||||||
|
sentMessage = "#fffcf6ff",
|
||||||
|
sentQuote = "#fff2e0fc",
|
||||||
|
receivedMessage = "#ffF6F4F7",
|
||||||
|
receivedQuote = "#ffede9ee",
|
||||||
|
),
|
||||||
|
DefaultTheme.DARK to ThemeColors(
|
||||||
|
sentMessage = "#ff33263B",
|
||||||
|
sentQuote = "#ff53385E",
|
||||||
|
receivedMessage = "#ff272528",
|
||||||
|
receivedQuote = "#ff3B373E",
|
||||||
|
),
|
||||||
|
DefaultTheme.SIMPLEX to ThemeColors(
|
||||||
|
sentMessage = "#ff3C255D",
|
||||||
|
sentQuote = "#ff623485",
|
||||||
|
receivedMessage = "#ff26273B",
|
||||||
|
receivedQuote = "#ff3A394F",
|
||||||
|
),
|
||||||
|
DefaultTheme.BLACK to ThemeColors(
|
||||||
|
sentMessage = "#ff3C255D",
|
||||||
|
sentQuote = "#ff623485",
|
||||||
|
receivedMessage = "#ff231f23",
|
||||||
|
receivedQuote = "#ff2c2931",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
fun toType(base: DefaultTheme, scale: Float? = null): WallpaperType =
|
||||||
|
WallpaperType.Preset(
|
||||||
|
filename,
|
||||||
|
scale ?: appPrefs.themeOverrides.get().firstOrNull { it.wallpaper != null && it.wallpaper.preset == filename && it.base == base }?.wallpaper?.scale ?: 1f
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(filename: String): PresetWallpaper? =
|
||||||
|
entries.firstOrNull { it.filename == filename }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wallpaperBackgrounds(light: String): Map<DefaultTheme, Color> =
|
||||||
|
mapOf(
|
||||||
|
DefaultTheme.LIGHT to light.colorFromReadableHex(),
|
||||||
|
DefaultTheme.DARK to "#ff121212".colorFromReadableHex(),
|
||||||
|
DefaultTheme.SIMPLEX to "#ff111528".colorFromReadableHex(),
|
||||||
|
DefaultTheme.BLACK to "#ff070707".colorFromReadableHex()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class WallpaperScaleType(val contentScale: ContentScale, val text: StringResource) {
|
||||||
|
@SerialName("fill") FILL(ContentScale.Crop, MR.strings.wallpaper_scale_fill),
|
||||||
|
@SerialName("fit") FIT(ContentScale.Fit, MR.strings.wallpaper_scale_fit),
|
||||||
|
@SerialName("repeat") REPEAT(ContentScale.Fit, MR.strings.wallpaper_scale_repeat),
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class WallpaperType {
|
||||||
|
abstract val scale: Float?
|
||||||
|
|
||||||
|
val image by lazy {
|
||||||
|
val filename = when (this) {
|
||||||
|
is Preset -> filename
|
||||||
|
is Image -> filename
|
||||||
|
else -> return@lazy null
|
||||||
|
}
|
||||||
|
if (filename == "") return@lazy null
|
||||||
|
if (cachedImages[filename] != null) {
|
||||||
|
cachedImages[filename]
|
||||||
|
} else {
|
||||||
|
val res = if (this is Preset) {
|
||||||
|
(PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).res.toComposeImageBitmap()!!
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// In case of unintentional image deletion don't crash the app
|
||||||
|
File(getWallpaperFilePath(filename)).inputStream().use { loadImageBitmap(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error while loading wallpaper file: ${e.stackTraceToString()}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res?.prepareToDraw()
|
||||||
|
cachedImages[filename] = res ?: return@lazy null
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sameType(other: WallpaperType?): Boolean =
|
||||||
|
if (this is Preset && other is Preset) this.filename == other.filename
|
||||||
|
else this.javaClass == other?.javaClass
|
||||||
|
|
||||||
|
fun samePreset(other: PresetWallpaper?): Boolean = this is Preset && filename == other?.filename
|
||||||
|
|
||||||
|
data class Preset(
|
||||||
|
val filename: String,
|
||||||
|
override val scale: Float?,
|
||||||
|
): WallpaperType() {
|
||||||
|
val predefinedImageScale = PresetWallpaper.from(filename)?.scale ?: 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Image(
|
||||||
|
val filename: String,
|
||||||
|
override val scale: Float?,
|
||||||
|
val scaleType: WallpaperScaleType?,
|
||||||
|
): WallpaperType()
|
||||||
|
|
||||||
|
object Empty: WallpaperType() {
|
||||||
|
override val scale: Float?
|
||||||
|
get() = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun defaultBackgroundColor(theme: DefaultTheme, materialBackground: Color): Color =
|
||||||
|
if (this is Preset) {
|
||||||
|
(PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).background[theme]!!
|
||||||
|
} else {
|
||||||
|
materialBackground
|
||||||
|
}
|
||||||
|
|
||||||
|
fun defaultTintColor(theme: DefaultTheme): Color =
|
||||||
|
if (this is Preset) {
|
||||||
|
(PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).tint[theme]!!
|
||||||
|
} else if (this is Image && scaleType == WallpaperScaleType.REPEAT) {
|
||||||
|
Color.Transparent
|
||||||
|
} else {
|
||||||
|
Color.Transparent
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var cachedImages: MutableMap<String, ImageBitmap> = mutableMapOf()
|
||||||
|
|
||||||
|
fun from(wallpaper: ThemeWallpaper?): WallpaperType? {
|
||||||
|
return if (wallpaper == null) {
|
||||||
|
null
|
||||||
|
} else if (wallpaper.preset != null) {
|
||||||
|
Preset(wallpaper.preset, wallpaper.scale)
|
||||||
|
} else if (wallpaper.imageFile != null) {
|
||||||
|
Image(wallpaper.imageFile, wallpaper.scale, wallpaper.scaleType)
|
||||||
|
} else {
|
||||||
|
Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, background: Color, tint: Color) = clipRect {
|
||||||
|
val quality = FilterQuality.High
|
||||||
|
fun repeat(imageScale: Float) {
|
||||||
|
val scale = imageScale * density
|
||||||
|
for (h in 0..(size.height / image.height / scale).roundToInt()) {
|
||||||
|
for (w in 0..(size.width / image.width / scale).roundToInt()) {
|
||||||
|
drawImage(
|
||||||
|
image,
|
||||||
|
dstOffset = IntOffset(x = (w * image.width * scale).roundToInt(), y = (h * image.height * scale).roundToInt()),
|
||||||
|
dstSize = IntSize((image.width * scale).roundToInt(), (image.height * scale).roundToInt()),
|
||||||
|
colorFilter = ColorFilter.tint(tint, BlendMode.SrcIn),
|
||||||
|
filterQuality = quality
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawRect(background)
|
||||||
|
when (imageType) {
|
||||||
|
is WallpaperType.Preset -> repeat((imageType.scale ?: 1f) * imageType.predefinedImageScale)
|
||||||
|
is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) {
|
||||||
|
WallpaperScaleType.REPEAT -> repeat(imageType.scale ?: 1f)
|
||||||
|
WallpaperScaleType.FILL, WallpaperScaleType.FIT -> {
|
||||||
|
val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height))
|
||||||
|
val scaledWidth = (image.width * scale.scaleX).roundToInt()
|
||||||
|
val scaledHeight = (image.height * scale.scaleY).roundToInt()
|
||||||
|
// Large image will cause freeze
|
||||||
|
if (image.width > 4320 || image.height > 4320) return@clipRect
|
||||||
|
|
||||||
|
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||||
|
if (scaleType == WallpaperScaleType.FIT) {
|
||||||
|
if (scaledWidth < size.width) {
|
||||||
|
// has black lines at left and right sides
|
||||||
|
var x = (size.width - scaledWidth) / 2
|
||||||
|
while (x > 0) {
|
||||||
|
drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||||
|
x -= scaledWidth
|
||||||
|
}
|
||||||
|
x = size.width - (size.width - scaledWidth) / 2
|
||||||
|
while (x < size.width) {
|
||||||
|
drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||||
|
x += scaledWidth
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// has black lines at top and bottom sides
|
||||||
|
var y = (size.height - scaledHeight) / 2
|
||||||
|
while (y > 0) {
|
||||||
|
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||||
|
y -= scaledHeight
|
||||||
|
}
|
||||||
|
y = size.height - (size.height - scaledHeight) / 2
|
||||||
|
while (y < size.height) {
|
||||||
|
drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality)
|
||||||
|
y += scaledHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawRect(tint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is WallpaperType.Empty -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -51,7 +51,7 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) {
|
fun AppBarTitle(title: String, hostDevice: Pair<Long?, String>? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) {
|
||||||
val theme = CurrentColors.collectAsState()
|
val theme = CurrentColors.collectAsState()
|
||||||
val titleColor = CurrentColors.collectAsState().value.appColors.title
|
val titleColor = MaterialTheme.appColors.title
|
||||||
val brush = if (theme.value.base == DefaultTheme.SIMPLEX)
|
val brush = if (theme.value.base == DefaultTheme.SIMPLEX)
|
||||||
Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
|
Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f))
|
||||||
else // color is not updated when changing themes if I pass null here
|
else // color is not updated when changing themes if I pass null here
|
||||||
|
|||||||
+1
-1
@@ -76,7 +76,7 @@ suspend fun getLinkPreview(url: String): LinkPreview? {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancelEnabled: Boolean) {
|
fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancelEnabled: Boolean) {
|
||||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
val sentColor = MaterialTheme.appColors.sentMessage
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth().padding(top = 8.dp).background(sentColor),
|
Modifier.fillMaxWidth().padding(top = 8.dp).background(sentColor),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
|||||||
+4
-1
@@ -41,9 +41,12 @@ enum class ModalPlacement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ModalData {
|
class ModalData {
|
||||||
private val state = mutableMapOf<String, MutableState<Any>>()
|
private val state = mutableMapOf<String, MutableState<Any?>>()
|
||||||
fun <T> stateGetOrPut (key: String, default: () -> T): MutableState<T> =
|
fun <T> stateGetOrPut (key: String, default: () -> T): MutableState<T> =
|
||||||
state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState<T>
|
state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState<T>
|
||||||
|
|
||||||
|
fun <T> stateGetOrPutNullable (key: String, default: () -> T?): MutableState<T?> =
|
||||||
|
state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState<T?>
|
||||||
}
|
}
|
||||||
|
|
||||||
class ModalManager(private val placement: ModalPlacement? = null) {
|
class ModalManager(private val placement: ModalPlacement? = null) {
|
||||||
|
|||||||
+520
@@ -0,0 +1,520 @@
|
|||||||
|
package chat.simplex.common.views.helpers
|
||||||
|
|
||||||
|
import SectionBottomSpacer
|
||||||
|
import SectionItemView
|
||||||
|
import SectionSpacer
|
||||||
|
import SectionView
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.MaterialTheme.colors
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
|
import chat.simplex.common.model.yaml
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.views.usersettings.*
|
||||||
|
import chat.simplex.common.views.usersettings.AppearanceScope.WallpaperPresetSelector
|
||||||
|
import chat.simplex.common.views.usersettings.AppearanceScope.editColor
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ModalData.UserWallpaperEditor(
|
||||||
|
theme: ThemeModeOverride,
|
||||||
|
applyToMode: DefaultThemeMode?,
|
||||||
|
globalThemeUsed: MutableState<Boolean>,
|
||||||
|
save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit
|
||||||
|
) {
|
||||||
|
ColumnWithScrollBar(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } }
|
||||||
|
var showMore by remember { stateGetOrPut("showMore") { false } }
|
||||||
|
val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } }
|
||||||
|
val currentTheme by CurrentColors.collectAsState()
|
||||||
|
|
||||||
|
AppBarTitle(stringResource(MR.strings.settings_section_title_user_theme))
|
||||||
|
val wallpaperImage = MaterialTheme.wallpaper.type.image
|
||||||
|
val wallpaperType = MaterialTheme.wallpaper.type
|
||||||
|
|
||||||
|
val onTypeCopyFromSameTheme = { type: WallpaperType? ->
|
||||||
|
if (type is WallpaperType.Image && chatModel.remoteHostId() != null) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
ThemeManager.copyFromSameThemeOverrides(type, null, themeModeOverride)
|
||||||
|
withBGApi { save(applyToMode.value, themeModeOverride.value) }
|
||||||
|
globalThemeUsed.value = false
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val preApplyGlobalIfNeeded = { type: WallpaperType? ->
|
||||||
|
if (globalThemeUsed.value) {
|
||||||
|
onTypeCopyFromSameTheme(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onTypeChange: (WallpaperType?) -> Unit = { type: WallpaperType? ->
|
||||||
|
if (globalThemeUsed.value) {
|
||||||
|
preApplyGlobalIfNeeded(type)
|
||||||
|
// Saves copied static image instead of original from global theme
|
||||||
|
ThemeManager.applyWallpaper(themeModeOverride.value.type, themeModeOverride)
|
||||||
|
} else {
|
||||||
|
ThemeManager.applyWallpaper(type, themeModeOverride)
|
||||||
|
}
|
||||||
|
withBGApi { save(applyToMode.value, themeModeOverride.value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||||
|
if (to != null) {
|
||||||
|
val filename = saveWallpaperFile(to)
|
||||||
|
if (filename != null) {
|
||||||
|
onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentColors = { type: WallpaperType? ->
|
||||||
|
// If applying for :
|
||||||
|
// - all themes: no overrides needed
|
||||||
|
// - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected
|
||||||
|
val perUserOverride = if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null
|
||||||
|
ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get())
|
||||||
|
}
|
||||||
|
val onChooseType: (WallpaperType?) -> Unit = { type: WallpaperType? ->
|
||||||
|
when {
|
||||||
|
// don't have image in parent or already selected wallpaper with custom image
|
||||||
|
type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing */ }
|
||||||
|
type is WallpaperType.Image && (wallpaperType is WallpaperType.Image || currentColors(type).wallpaper.type.image == null) -> withLongRunningApi { importWallpaperLauncher.launch("image/*") }
|
||||||
|
type is WallpaperType.Image -> onTypeCopyFromSameTheme(currentColors(type).wallpaper.type)
|
||||||
|
themeModeOverride.value.type != type || currentTheme.wallpaper.type != type -> onTypeCopyFromSameTheme(type)
|
||||||
|
else -> onTypeChange(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val editColor = { name: ThemeColor ->
|
||||||
|
editColor(
|
||||||
|
name,
|
||||||
|
wallpaperType,
|
||||||
|
wallpaperImage,
|
||||||
|
onColorChange = { color ->
|
||||||
|
preApplyGlobalIfNeeded(themeModeOverride.value.type)
|
||||||
|
ThemeManager.applyThemeColor(name, color, themeModeOverride)
|
||||||
|
withBGApi { save(applyToMode.value, themeModeOverride.value) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
WallpaperPresetSelector(
|
||||||
|
selectedWallpaper = wallpaperType,
|
||||||
|
baseTheme = currentTheme.base,
|
||||||
|
currentColors = { type ->
|
||||||
|
// If applying for :
|
||||||
|
// - all themes: no overrides needed
|
||||||
|
// - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected
|
||||||
|
val perUserOverride = if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null
|
||||||
|
ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get())
|
||||||
|
},
|
||||||
|
onChooseType = onChooseType
|
||||||
|
)
|
||||||
|
|
||||||
|
WallpaperSetupView(
|
||||||
|
themeModeOverride.value.type,
|
||||||
|
CurrentColors.collectAsState().value.base,
|
||||||
|
currentTheme.wallpaper,
|
||||||
|
currentTheme.appColors.sentMessage,
|
||||||
|
currentTheme.appColors.sentQuote,
|
||||||
|
currentTheme.appColors.receivedMessage,
|
||||||
|
currentTheme.appColors.receivedQuote,
|
||||||
|
editColor = { name -> editColor(name) },
|
||||||
|
onTypeChange = onTypeChange,
|
||||||
|
)
|
||||||
|
|
||||||
|
SectionSpacer()
|
||||||
|
|
||||||
|
if (!globalThemeUsed.value) {
|
||||||
|
ResetToGlobalThemeButton(true) {
|
||||||
|
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
|
globalThemeUsed.value = true
|
||||||
|
withBGApi { save(applyToMode.value, null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SetDefaultThemeButton {
|
||||||
|
globalThemeUsed.value = false
|
||||||
|
val lightBase = DefaultTheme.LIGHT
|
||||||
|
val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX
|
||||||
|
val mode = themeModeOverride.value.mode
|
||||||
|
withBGApi {
|
||||||
|
// Saving for both modes in one place by changing mode once per save
|
||||||
|
if (applyToMode.value == null) {
|
||||||
|
val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT
|
||||||
|
save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase))
|
||||||
|
}
|
||||||
|
themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase)
|
||||||
|
save(themeModeOverride.value.mode, themeModeOverride.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyChangeEffect(theme.mode) {
|
||||||
|
themeModeOverride.value = theme
|
||||||
|
if (applyToMode.value != null) {
|
||||||
|
applyToMode.value = theme.mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applies updated global theme if current one tracks global theme
|
||||||
|
KeyChangeEffect(CurrentColors.collectAsState().value) {
|
||||||
|
if (globalThemeUsed.value) {
|
||||||
|
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
|
globalThemeUsed.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionSpacer()
|
||||||
|
|
||||||
|
if (showMore) {
|
||||||
|
val values by remember { mutableStateOf(
|
||||||
|
listOf(
|
||||||
|
null to generalGetString(MR.strings.chat_theme_apply_to_all_modes),
|
||||||
|
DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode),
|
||||||
|
DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ExposedDropDownSettingRow(
|
||||||
|
generalGetString(MR.strings.chat_theme_apply_to_mode),
|
||||||
|
values,
|
||||||
|
applyToMode,
|
||||||
|
icon = null,
|
||||||
|
enabled = remember { mutableStateOf(true) },
|
||||||
|
onSelected = {
|
||||||
|
applyToMode.value = it
|
||||||
|
if (it != null && it != CurrentColors.value.base.mode) {
|
||||||
|
val lightBase = DefaultTheme.LIGHT
|
||||||
|
val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX
|
||||||
|
ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SectionSpacer()
|
||||||
|
|
||||||
|
AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor)
|
||||||
|
|
||||||
|
SectionSpacer()
|
||||||
|
ImportExportThemeSection(null, remember { chatModel.currentUser }.value?.uiThemes) {
|
||||||
|
withBGApi {
|
||||||
|
themeModeOverride.value = it
|
||||||
|
save(applyToMode.value, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdvancedSettingsButton { showMore = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionBottomSpacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ModalData.ChatWallpaperEditor(
|
||||||
|
theme: ThemeModeOverride,
|
||||||
|
applyToMode: DefaultThemeMode?,
|
||||||
|
globalThemeUsed: MutableState<Boolean>,
|
||||||
|
save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit
|
||||||
|
) {
|
||||||
|
ColumnWithScrollBar(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } }
|
||||||
|
var showMore by remember { stateGetOrPut("showMore") { false } }
|
||||||
|
val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } }
|
||||||
|
val currentTheme by remember(themeModeOverride.value, CurrentColors.collectAsState().value) {
|
||||||
|
mutableStateOf(
|
||||||
|
ThemeManager.currentColors(null, if (globalThemeUsed.value) null else themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AppBarTitle(stringResource(MR.strings.settings_section_title_chat_theme))
|
||||||
|
|
||||||
|
val onTypeCopyFromSameTheme: (WallpaperType?) -> Boolean = { type ->
|
||||||
|
if (type is WallpaperType.Image && chatModel.remoteHostId() != null) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
val success = ThemeManager.copyFromSameThemeOverrides(type, chatModel.currentUser.value?.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight), themeModeOverride)
|
||||||
|
if (success) {
|
||||||
|
withBGApi { save(applyToMode.value, themeModeOverride.value) }
|
||||||
|
globalThemeUsed.value = false
|
||||||
|
}
|
||||||
|
success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val preApplyGlobalIfNeeded = { type: WallpaperType? ->
|
||||||
|
if (globalThemeUsed.value) {
|
||||||
|
onTypeCopyFromSameTheme(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val onTypeChange: (WallpaperType?) -> Unit = { type ->
|
||||||
|
if (globalThemeUsed.value) {
|
||||||
|
preApplyGlobalIfNeeded(type)
|
||||||
|
// Saves copied static image instead of original from global theme
|
||||||
|
ThemeManager.applyWallpaper(themeModeOverride.value.type, themeModeOverride)
|
||||||
|
} else {
|
||||||
|
ThemeManager.applyWallpaper(type, themeModeOverride)
|
||||||
|
}
|
||||||
|
withBGApi { save(applyToMode.value, themeModeOverride.value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val editColor: (ThemeColor) -> Unit = { name: ThemeColor ->
|
||||||
|
ModalManager.end.showModal {
|
||||||
|
val currentTheme by remember(themeModeOverride.value, CurrentColors.collectAsState().value) {
|
||||||
|
mutableStateOf(
|
||||||
|
ThemeManager.currentColors(null, themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val initialColor: Color = when (name) {
|
||||||
|
ThemeColor.WALLPAPER_BACKGROUND -> currentTheme.wallpaper.background ?: Color.Transparent
|
||||||
|
ThemeColor.WALLPAPER_TINT -> currentTheme.wallpaper.tint ?: Color.Transparent
|
||||||
|
ThemeColor.PRIMARY -> currentTheme.colors.primary
|
||||||
|
ThemeColor.PRIMARY_VARIANT -> currentTheme.colors.primaryVariant
|
||||||
|
ThemeColor.SECONDARY -> currentTheme.colors.secondary
|
||||||
|
ThemeColor.SECONDARY_VARIANT -> currentTheme.colors.secondaryVariant
|
||||||
|
ThemeColor.BACKGROUND -> currentTheme.colors.background
|
||||||
|
ThemeColor.SURFACE -> currentTheme.colors.surface
|
||||||
|
ThemeColor.TITLE -> currentTheme.appColors.title
|
||||||
|
ThemeColor.PRIMARY_VARIANT2 -> currentTheme.appColors.primaryVariant2
|
||||||
|
ThemeColor.SENT_MESSAGE -> currentTheme.appColors.sentMessage
|
||||||
|
ThemeColor.SENT_QUOTE -> currentTheme.appColors.sentQuote
|
||||||
|
ThemeColor.RECEIVED_MESSAGE -> currentTheme.appColors.receivedMessage
|
||||||
|
ThemeColor.RECEIVED_QUOTE -> currentTheme.appColors.receivedQuote
|
||||||
|
}
|
||||||
|
AppearanceScope.ColorEditor(
|
||||||
|
name,
|
||||||
|
initialColor,
|
||||||
|
CurrentColors.collectAsState().value.base,
|
||||||
|
themeModeOverride.value.type,
|
||||||
|
themeModeOverride.value.type?.image,
|
||||||
|
currentTheme.wallpaper.background,
|
||||||
|
currentTheme.wallpaper.tint,
|
||||||
|
currentColors = {
|
||||||
|
ThemeManager.currentColors(null, themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())
|
||||||
|
},
|
||||||
|
onColorChange = { color ->
|
||||||
|
preApplyGlobalIfNeeded(themeModeOverride.value.type)
|
||||||
|
ThemeManager.applyThemeColor(name, color, themeModeOverride)
|
||||||
|
withBGApi { save(applyToMode.value, themeModeOverride.value) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||||
|
if (to != null) {
|
||||||
|
val filename = saveWallpaperFile(to)
|
||||||
|
if (filename != null) {
|
||||||
|
// Delete only non-user image
|
||||||
|
if (!globalThemeUsed.value) {
|
||||||
|
removeWallpaperFile((themeModeOverride.value.type as? WallpaperType.Image)?.filename)
|
||||||
|
}
|
||||||
|
globalThemeUsed.value = false
|
||||||
|
onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentColors = { type: WallpaperType? ->
|
||||||
|
ThemeManager.currentColors(type, if (type?.sameType(themeModeOverride.value.type) == true) themeModeOverride.value else null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
WallpaperPresetSelector(
|
||||||
|
selectedWallpaper = currentTheme.wallpaper.type,
|
||||||
|
activeBackgroundColor = currentTheme.wallpaper.background,
|
||||||
|
activeTintColor = currentTheme.wallpaper.tint,
|
||||||
|
baseTheme = CurrentColors.collectAsState().value.base,
|
||||||
|
currentColors = { type -> currentColors(type) },
|
||||||
|
onChooseType = { type ->
|
||||||
|
when {
|
||||||
|
type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing */ }
|
||||||
|
type is WallpaperType.Image && ((themeModeOverride.value.type is WallpaperType.Image && !globalThemeUsed.value) || currentColors(type).wallpaper.type.image == null) -> {
|
||||||
|
withLongRunningApi { importWallpaperLauncher.launch("image/*") }
|
||||||
|
}
|
||||||
|
type is WallpaperType.Image -> {
|
||||||
|
if (!onTypeCopyFromSameTheme(currentColors(type).wallpaper.type)) {
|
||||||
|
withLongRunningApi { importWallpaperLauncher.launch("image/*") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalThemeUsed.value || themeModeOverride.value.type != type -> {
|
||||||
|
onTypeCopyFromSameTheme(type)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
onTypeChange(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
WallpaperSetupView(
|
||||||
|
themeModeOverride.value.type,
|
||||||
|
CurrentColors.collectAsState().value.base,
|
||||||
|
currentTheme.wallpaper,
|
||||||
|
currentTheme.appColors.sentMessage,
|
||||||
|
currentTheme.appColors.sentQuote,
|
||||||
|
currentTheme.appColors.receivedMessage,
|
||||||
|
currentTheme.appColors.receivedQuote,
|
||||||
|
editColor = editColor,
|
||||||
|
onTypeChange = onTypeChange,
|
||||||
|
)
|
||||||
|
|
||||||
|
SectionSpacer()
|
||||||
|
|
||||||
|
if (!globalThemeUsed.value) {
|
||||||
|
ResetToGlobalThemeButton(remember { chatModel.currentUser }.value?.uiThemes?.preferredMode(isInDarkTheme()) == null) {
|
||||||
|
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
|
globalThemeUsed.value = true
|
||||||
|
withBGApi { save(applyToMode.value, null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SetDefaultThemeButton {
|
||||||
|
globalThemeUsed.value = false
|
||||||
|
val lightBase = DefaultTheme.LIGHT
|
||||||
|
val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX
|
||||||
|
val mode = themeModeOverride.value.mode
|
||||||
|
withBGApi {
|
||||||
|
// Saving for both modes in one place by changing mode once per save
|
||||||
|
if (applyToMode.value == null) {
|
||||||
|
val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT
|
||||||
|
save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase))
|
||||||
|
}
|
||||||
|
themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase)
|
||||||
|
save(themeModeOverride.value.mode, themeModeOverride.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyChangeEffect(theme.mode) {
|
||||||
|
themeModeOverride.value = theme
|
||||||
|
if (applyToMode.value != null) {
|
||||||
|
applyToMode.value = theme.mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applies updated global theme if current one tracks global theme
|
||||||
|
KeyChangeEffect(CurrentColors.collectAsState()) {
|
||||||
|
if (globalThemeUsed.value) {
|
||||||
|
themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
|
||||||
|
globalThemeUsed.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionSpacer()
|
||||||
|
|
||||||
|
if (showMore) {
|
||||||
|
val values by remember { mutableStateOf(
|
||||||
|
listOf(
|
||||||
|
null to generalGetString(MR.strings.chat_theme_apply_to_all_modes),
|
||||||
|
DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode),
|
||||||
|
DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ExposedDropDownSettingRow(
|
||||||
|
generalGetString(MR.strings.chat_theme_apply_to_mode),
|
||||||
|
values,
|
||||||
|
applyToMode,
|
||||||
|
icon = null,
|
||||||
|
enabled = remember { mutableStateOf(true) },
|
||||||
|
onSelected = {
|
||||||
|
applyToMode.value = it
|
||||||
|
if (it != null && it != CurrentColors.value.base.mode) {
|
||||||
|
val lightBase = DefaultTheme.LIGHT
|
||||||
|
val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX
|
||||||
|
ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SectionSpacer()
|
||||||
|
|
||||||
|
AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor)
|
||||||
|
|
||||||
|
SectionSpacer()
|
||||||
|
ImportExportThemeSection(themeModeOverride.value, remember { chatModel.currentUser }.value?.uiThemes) {
|
||||||
|
withBGApi {
|
||||||
|
themeModeOverride.value = it
|
||||||
|
save(applyToMode.value, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AdvancedSettingsButton { showMore = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionBottomSpacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ImportExportThemeSection(perChat: ThemeModeOverride?, perUser: ThemeModeOverrides?, save: (ThemeModeOverride) -> Unit) {
|
||||||
|
SectionView {
|
||||||
|
val theme = remember { mutableStateOf(null as String?) }
|
||||||
|
val exportThemeLauncher = rememberFileChooserLauncher(false) { to: URI? ->
|
||||||
|
val themeValue = theme.value
|
||||||
|
if (themeValue != null && to != null) {
|
||||||
|
copyBytesToFile(themeValue.byteInputStream(), to) {
|
||||||
|
theme.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SectionItemView({
|
||||||
|
val overrides = ThemeManager.currentThemeOverridesForExport(perChat, perUser)
|
||||||
|
val lines = yaml.encodeToString<ThemeOverrides>(overrides).lines()
|
||||||
|
// Removing theme id without using custom serializer or data class
|
||||||
|
theme.value = lines.subList(1, lines.size).joinToString("\n")
|
||||||
|
withLongRunningApi { exportThemeLauncher.launch("simplex.theme") }
|
||||||
|
}) {
|
||||||
|
Text(generalGetString(MR.strings.export_theme), color = colors.primary)
|
||||||
|
}
|
||||||
|
val importThemeLauncher = rememberFileChooserLauncher(true) { to: URI? ->
|
||||||
|
if (to != null) {
|
||||||
|
val theme = getThemeFromUri(to)
|
||||||
|
if (theme != null) {
|
||||||
|
val res = ThemeModeOverride(mode = theme.base.mode, colors = theme.colors, wallpaper = theme.wallpaper?.importFromString()).removeSameColors(theme.base)
|
||||||
|
save(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Can not limit to YAML mime type since it's unsupported by Android
|
||||||
|
SectionItemView({ withLongRunningApi { importThemeLauncher.launch("*/*") } }) {
|
||||||
|
Text(generalGetString(MR.strings.import_theme), color = colors.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ResetToGlobalThemeButton(app: Boolean, onClick: () -> Unit) {
|
||||||
|
SectionItemView(onClick) {
|
||||||
|
Text(stringResource(if (app) MR.strings.chat_theme_reset_to_app_theme else MR.strings.chat_theme_reset_to_user_theme), color = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SetDefaultThemeButton(onClick: () -> Unit) {
|
||||||
|
SectionItemView(onClick) {
|
||||||
|
Text(stringResource(MR.strings.chat_theme_set_default_theme), color = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AdvancedSettingsButton(onClick: () -> Unit) {
|
||||||
|
SettingsActionItem(
|
||||||
|
painterResource(MR.images.ic_arrow_downward),
|
||||||
|
stringResource(MR.strings.wallpaper_advanced_settings),
|
||||||
|
click = onClick
|
||||||
|
)
|
||||||
|
}
|
||||||
+58
-1
@@ -19,6 +19,7 @@ import kotlinx.serialization.encodeToString
|
|||||||
import java.io.*
|
import java.io.*
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.StandardCopyOption
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@@ -145,6 +146,7 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri
|
|||||||
runCatching {
|
runCatching {
|
||||||
return yaml.decodeFromStream<ThemeOverrides>(it!!)
|
return yaml.decodeFromStream<ThemeOverrides>(it!!)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
|
Log.e(TAG, "Error while decoding theme: ${it.stackTraceToString()}")
|
||||||
if (withAlertOnException) {
|
if (withAlertOnException) {
|
||||||
AlertManager.shared.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
title = generalGetString(MR.strings.import_theme_error),
|
title = generalGetString(MR.strings.import_theme_error),
|
||||||
@@ -280,6 +282,38 @@ fun saveFileFromUri(uri: URI, withAlertOnException: Boolean = true): CryptoFile?
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveWallpaperFile(uri: URI): String? {
|
||||||
|
val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath("")))
|
||||||
|
val destFile = File(getWallpaperFilePath(destFileName))
|
||||||
|
try {
|
||||||
|
val inputStream = uri.inputStream()
|
||||||
|
Files.copy(inputStream!!, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error saving wallpaper file: ${e.stackTraceToString()}")
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString())
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return destFile.name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveWallpaperFile(image: ImageBitmap): String {
|
||||||
|
val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath("")))
|
||||||
|
val destFile = File(getWallpaperFilePath(destFileName))
|
||||||
|
val dataResized = resizeImageToDataSize(image, false, maxDataSize = 5_000_000)
|
||||||
|
val output = FileOutputStream(destFile)
|
||||||
|
dataResized.use {
|
||||||
|
it.writeTo(output)
|
||||||
|
}
|
||||||
|
return destFile.name
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeWallpaperFile(fileName: String? = null) {
|
||||||
|
File(getWallpaperFilePath("_")).parentFile.listFiles()?.forEach {
|
||||||
|
if (it.name == fileName) it.delete()
|
||||||
|
}
|
||||||
|
WallpaperType.cachedImages.remove(fileName)
|
||||||
|
}
|
||||||
|
|
||||||
fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T {
|
fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T {
|
||||||
val tmpFile = File(tmpDir, UUID.randomUUID().toString())
|
val tmpFile = File(tmpDir, UUID.randomUUID().toString())
|
||||||
tmpFile.deleteOnExit()
|
tmpFile.deleteOnExit()
|
||||||
@@ -550,10 +584,33 @@ fun KeyChangeEffect(
|
|||||||
val initialKey = remember { key1 }
|
val initialKey = remember { key1 }
|
||||||
val initialKey2 = remember { key2 }
|
val initialKey2 = remember { key2 }
|
||||||
var anyChange by remember { mutableStateOf(false) }
|
var anyChange by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(key1) {
|
LaunchedEffect(key1, key2) {
|
||||||
if (anyChange || key1 != initialKey || key2 != initialKey2) {
|
if (anyChange || key1 != initialKey || key2 != initialKey2) {
|
||||||
block()
|
block()
|
||||||
anyChange = true
|
anyChange = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the [block] only after initial value of the [key1], or [key2], or [key3] changes, not after initial launch
|
||||||
|
* */
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
fun KeyChangeEffect(
|
||||||
|
key1: Any?,
|
||||||
|
key2: Any?,
|
||||||
|
key3: Any?,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val initialKey = remember { key1 }
|
||||||
|
val initialKey2 = remember { key2 }
|
||||||
|
val initialKey3 = remember { key3 }
|
||||||
|
var anyChange by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(key1, key2, key3) {
|
||||||
|
if (anyChange || key1 != initialKey || key2 != initialKey2 || key3 != initialKey3) {
|
||||||
|
block()
|
||||||
|
anyChange = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
|
|||||||
} else {
|
} else {
|
||||||
val r: LAResult = if (passcode.value == authRequest.password) {
|
val r: LAResult = if (passcode.value == authRequest.password) {
|
||||||
if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) {
|
if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) {
|
||||||
initChatControllerAndRunMigrations()
|
initChatControllerOnStart()
|
||||||
}
|
}
|
||||||
LAResult.Success
|
LAResult.Success
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+1
-1
@@ -669,7 +669,7 @@ private suspend fun MutableState<MigrationToState?>.cleanUpOnBack(chatReceiver:
|
|||||||
if (state is MigrationToState.ArchiveImportFailed) {
|
if (state is MigrationToState.ArchiveImportFailed) {
|
||||||
// Original database is not exist, nothing is set up correctly for showing to a user yet. Return to clean state
|
// Original database is not exist, nothing is set up correctly for showing to a user yet. Return to clean state
|
||||||
deleteChatDatabaseFilesAndState()
|
deleteChatDatabaseFilesAndState()
|
||||||
initChatControllerAndRunMigrations()
|
initChatControllerOnStart()
|
||||||
} else if (state is MigrationToState.DownloadProgress && state.ctrl != null) {
|
} else if (state is MigrationToState.DownloadProgress && state.ctrl != null) {
|
||||||
stopArchiveDownloading(state.fileId, state.ctrl)
|
stopArchiveDownloading(state.fileId, state.ctrl)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-7
@@ -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,
|
||||||
|
|||||||
+880
-119
File diff suppressed because it is too large
Load Diff
+152
-1
@@ -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 = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-5
@@ -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 ->
|
||||||
|
|||||||
+1
-1
@@ -144,7 +144,7 @@ fun SettingsLayout(
|
|||||||
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped, extraPadding = true)
|
||||||
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true)
|
||||||
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true)
|
||||||
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true)
|
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) }, extraPadding = true)
|
||||||
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
|
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
|
||||||
}
|
}
|
||||||
SectionDividerSpaced()
|
SectionDividerSpaced()
|
||||||
|
|||||||
@@ -847,7 +847,6 @@
|
|||||||
<string name="auth_open_chat_profiles">افتح ملفات تعريف الدردشة</string>
|
<string name="auth_open_chat_profiles">افتح ملفات تعريف الدردشة</string>
|
||||||
<string name="revoke_file__confirm">اسحب الوصول</string>
|
<string name="revoke_file__confirm">اسحب الوصول</string>
|
||||||
<string name="save_archive">حفظ الأرشيف</string>
|
<string name="save_archive">حفظ الأرشيف</string>
|
||||||
<string name="save_color">حفظ اللون</string>
|
|
||||||
<string name="reveal_verb">كشف</string>
|
<string name="reveal_verb">كشف</string>
|
||||||
<string name="stop_rcv_file__message">سيتم إيقاف استلام الملف.</string>
|
<string name="stop_rcv_file__message">سيتم إيقاف استلام الملف.</string>
|
||||||
<string name="reject_contact_button">رفض</string>
|
<string name="reject_contact_button">رفض</string>
|
||||||
|
|||||||
@@ -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,9 +724,29 @@
|
|||||||
<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">INTERFACE COLORS</string>
|
||||||
<string name="app_version_title">App version</string>
|
<string name="app_version_title">App version</string>
|
||||||
<string name="app_version_name">App version: v%s</string>
|
<string name="app_version_name">App version: v%s</string>
|
||||||
<string name="app_version_code">App build: %s</string>
|
<string name="app_version_code">App build: %s</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>
|
||||||
@@ -1034,7 +1072,11 @@
|
|||||||
<string name="settings_section_title_icon">APP ICON</string>
|
<string name="settings_section_title_icon">APP ICON</string>
|
||||||
<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_chat_theme">Chat theme</string>
|
||||||
|
<string name="settings_section_title_user_theme">Profile theme</string>
|
||||||
|
<string name="settings_section_title_chat_colors">Chat colors</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>
|
||||||
@@ -1162,6 +1204,7 @@
|
|||||||
<string name="incompatible_database_version">Incompatible database version</string>
|
<string name="incompatible_database_version">Incompatible database version</string>
|
||||||
<string name="confirm_database_upgrades">Confirm database upgrades</string>
|
<string name="confirm_database_upgrades">Confirm database upgrades</string>
|
||||||
<string name="terminal_always_visible">Show console in new window</string>
|
<string name="terminal_always_visible">Show console in new window</string>
|
||||||
|
<string name="chat_list_always_visible">Show chat list in new window</string>
|
||||||
<string name="invalid_migration_confirmation">Invalid migration confirmation</string>
|
<string name="invalid_migration_confirmation">Invalid migration confirmation</string>
|
||||||
<string name="upgrade_and_open_chat">Upgrade and open chat</string>
|
<string name="upgrade_and_open_chat">Upgrade and open chat</string>
|
||||||
<string name="downgrade_and_open_chat">Downgrade and open chat</string>
|
<string name="downgrade_and_open_chat">Downgrade and open chat</string>
|
||||||
@@ -1504,23 +1547,30 @@
|
|||||||
<string name="incognito_info_share">When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</string>
|
<string name="incognito_info_share">When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</string>
|
||||||
|
|
||||||
<!-- Default themes -->
|
<!-- Default themes -->
|
||||||
|
<string name="color_mode_system">System</string>
|
||||||
|
<string name="color_mode_light">Light</string>
|
||||||
|
<string name="color_mode_dark">Dark</string>
|
||||||
<string name="theme_system">System</string>
|
<string name="theme_system">System</string>
|
||||||
<string name="theme_light">Light</string>
|
<string name="theme_light">Light</string>
|
||||||
<string name="theme_dark">Dark</string>
|
<string name="theme_dark">Dark</string>
|
||||||
<string name="theme_simplex">SimpleX</string>
|
<string name="theme_simplex">SimpleX</string>
|
||||||
|
<string name="theme_black">Black</string>
|
||||||
|
|
||||||
<!-- Languages -->
|
<!-- Languages -->
|
||||||
<string name="language_system">System</string>
|
<string name="language_system">System</string>
|
||||||
|
|
||||||
<!-- Appearance.kt -->
|
<!-- Appearance.kt -->
|
||||||
<string name="theme">Theme</string>
|
<string name="theme">Theme</string>
|
||||||
|
<string name="color_mode">Color mode</string>
|
||||||
<string name="dark_theme">Dark theme</string>
|
<string name="dark_theme">Dark theme</string>
|
||||||
<string name="save_color">Save color</string>
|
<string name="dark_mode_colors">Dark mode colors</string>
|
||||||
<string name="import_theme">Import theme</string>
|
<string name="import_theme">Import theme</string>
|
||||||
<string name="import_theme_error">Import theme error</string>
|
<string name="import_theme_error">Import theme error</string>
|
||||||
<string name="import_theme_error_desc">Make sure the file has correct YAML syntax. Export theme to have an example of the theme file structure.</string>
|
<string name="import_theme_error_desc">Make sure the file has correct YAML syntax. Export theme to have an example of the theme file structure.</string>
|
||||||
<string name="export_theme">Export theme</string>
|
<string name="export_theme">Export theme</string>
|
||||||
<string name="reset_color">Reset colors</string>
|
<string name="reset_color">Reset colors</string>
|
||||||
|
<string name="reset_single_color">Reset color</string>
|
||||||
|
<string name="theme_destination_app_theme">App theme</string>
|
||||||
<string name="color_primary">Accent</string>
|
<string name="color_primary">Accent</string>
|
||||||
<string name="color_primary_variant">Additional accent</string>
|
<string name="color_primary_variant">Additional accent</string>
|
||||||
<string name="color_secondary">Secondary</string>
|
<string name="color_secondary">Secondary</string>
|
||||||
@@ -1528,8 +1578,37 @@
|
|||||||
<string name="color_background">Background</string>
|
<string name="color_background">Background</string>
|
||||||
<string name="color_surface">Menus & alerts</string>
|
<string name="color_surface">Menus & alerts</string>
|
||||||
<string name="color_title">Title</string>
|
<string name="color_title">Title</string>
|
||||||
|
<string name="color_primary_variant2">Additional accent 2</string>
|
||||||
<string name="color_sent_message">Sent message</string>
|
<string name="color_sent_message">Sent message</string>
|
||||||
|
<string name="color_sent_quote">Sent reply</string>
|
||||||
<string name="color_received_message">Received message</string>
|
<string name="color_received_message">Received message</string>
|
||||||
|
<string name="color_received_quote">Received reply</string>
|
||||||
|
<string name="color_wallpaper_background">Wallpaper background</string>
|
||||||
|
<string name="color_wallpaper_tint">Wallpaper accent</string>
|
||||||
|
<string name="theme_remove_image">Remove image</string>
|
||||||
|
|
||||||
|
<!-- Backgrounds -->
|
||||||
|
<string name="wallpaper_cats">Cats</string>
|
||||||
|
<string name="wallpaper_flowers">Flowers</string>
|
||||||
|
<string name="wallpaper_hearts">Hearts</string>
|
||||||
|
<string name="wallpaper_kids">Kids</string>
|
||||||
|
<string name="wallpaper_school">School</string>
|
||||||
|
<string name="wallpaper_travel">Travel</string>
|
||||||
|
<string name="wallpaper_preview_hello_alice">Good afternoon!</string>
|
||||||
|
<string name="wallpaper_preview_hello_bob">Good morning!</string>
|
||||||
|
<string name="wallpaper_scale">Scale</string>
|
||||||
|
<string name="wallpaper_scale_repeat">Repeat</string>
|
||||||
|
<string name="wallpaper_scale_fill">Fill</string>
|
||||||
|
<string name="wallpaper_scale_fit">Fit</string>
|
||||||
|
<string name="wallpaper_advanced_settings">Advanced settings</string>
|
||||||
|
<string name="chat_theme_reset_to_app_theme">Reset to app theme</string>
|
||||||
|
<string name="chat_theme_reset_to_user_theme">Reset to user theme</string>
|
||||||
|
<string name="chat_theme_set_default_theme">Set default theme</string>
|
||||||
|
<string name="chat_theme_apply_to_mode">Apply to</string>
|
||||||
|
<string name="chat_theme_apply_to_all_modes">All color modes</string>
|
||||||
|
<string name="chat_theme_apply_to_light_mode">Light mode</string>
|
||||||
|
<string name="chat_theme_apply_to_dark_mode">Dark mode</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- Preferences.kt -->
|
<!-- Preferences.kt -->
|
||||||
<string name="chat_preferences_you_allow">You allow</string>
|
<string name="chat_preferences_you_allow">You allow</string>
|
||||||
|
|||||||
@@ -1150,7 +1150,6 @@
|
|||||||
<string name="network_options_revert">Отмени промените</string>
|
<string name="network_options_revert">Отмени промените</string>
|
||||||
<string name="network_options_save">Запази</string>
|
<string name="network_options_save">Запази</string>
|
||||||
<string name="reset_color">Нулирай цветовете</string>
|
<string name="reset_color">Нулирай цветовете</string>
|
||||||
<string name="save_color">Запази цвета</string>
|
|
||||||
<string name="color_secondary">Вторичен</string>
|
<string name="color_secondary">Вторичен</string>
|
||||||
<string name="custom_time_picker_select">Избери</string>
|
<string name="custom_time_picker_select">Избери</string>
|
||||||
<string name="to_start_a_new_chat_help_header">За да започнете нов чат</string>
|
<string name="to_start_a_new_chat_help_header">За да започнете нов чат</string>
|
||||||
|
|||||||
@@ -55,7 +55,6 @@
|
|||||||
<string name="update_network_settings_question">Aktualizovat nastavení sítě\?</string>
|
<string name="update_network_settings_question">Aktualizovat nastavení sítě\?</string>
|
||||||
<string name="incognito">Inkognito</string>
|
<string name="incognito">Inkognito</string>
|
||||||
<string name="incognito_random_profile">Váš náhodný profil</string>
|
<string name="incognito_random_profile">Váš náhodný profil</string>
|
||||||
<string name="save_color">Uložit barvu</string>
|
|
||||||
<string name="reset_color">Obnovit barvu</string>
|
<string name="reset_color">Obnovit barvu</string>
|
||||||
<string name="color_primary">Zbarvení</string>
|
<string name="color_primary">Zbarvení</string>
|
||||||
<string name="chat_preferences_you_allow">Povolujete</string>
|
<string name="chat_preferences_you_allow">Povolujete</string>
|
||||||
|
|||||||
@@ -842,7 +842,6 @@
|
|||||||
<string name="theme_dark">Dunkel</string>
|
<string name="theme_dark">Dunkel</string>
|
||||||
<!-- Appearance.kt -->
|
<!-- Appearance.kt -->
|
||||||
<string name="theme">Design</string>
|
<string name="theme">Design</string>
|
||||||
<string name="save_color">Farbe speichern</string>
|
|
||||||
<string name="reset_color">Farben zurücksetzen</string>
|
<string name="reset_color">Farben zurücksetzen</string>
|
||||||
<string name="color_primary">Akzent</string>
|
<string name="color_primary">Akzent</string>
|
||||||
<!-- Preferences.kt -->
|
<!-- Preferences.kt -->
|
||||||
|
|||||||
@@ -607,7 +607,6 @@
|
|||||||
<string name="secret_text">secreto</string>
|
<string name="secret_text">secreto</string>
|
||||||
<string name="open_simplex_chat_to_accept_call">Abrir SimpleX Chat para aceptar llamada</string>
|
<string name="open_simplex_chat_to_accept_call">Abrir SimpleX Chat para aceptar llamada</string>
|
||||||
<string name="network_options_reset_to_defaults">Restablecer valores por defecto</string>
|
<string name="network_options_reset_to_defaults">Restablecer valores por defecto</string>
|
||||||
<string name="save_color">Guardar color</string>
|
|
||||||
<string name="icon_descr_server_status_pending">Pendiente</string>
|
<string name="icon_descr_server_status_pending">Pendiente</string>
|
||||||
<string name="periodic_notifications">Notificaciones periódicas</string>
|
<string name="periodic_notifications">Notificaciones periódicas</string>
|
||||||
<string name="store_passphrase_securely">Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes.</string>
|
<string name="store_passphrase_securely">Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes.</string>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user