mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85dc467f74 | |||
| 89e65c2fc1 | |||
| 0fa8d77214 | |||
| 6ba82c8c39 | |||
| e19c14b249 | |||
| d993381d31 | |||
| 0198c1f110 | |||
| 6c4c9b172c | |||
| d9a61af401 | |||
| a58522ef15 | |||
| bcd510c913 | |||
| c4083e00c5 | |||
| 7cd35b9068 | |||
| d3274b28ba | |||
| 40bd5dc2da | |||
| 1573b7af13 | |||
| a5bf3cbca8 | |||
| d5529031ee | |||
| d244338b13 | |||
| ce55c84b96 | |||
| c52f9baefa | |||
| ea1c5da65d | |||
| 5695bb9d46 | |||
| 13751d5705 | |||
| 02803b1491 | |||
| 222af69a71 | |||
| 5e96e1ea2b | |||
| 490e8cead8 | |||
| f578ee843b | |||
| cf2eb76cb7 | |||
| 48f9dcab9a | |||
| ac2ef54838 | |||
| 7e269c6b8c | |||
| 3e739924bb | |||
| 83706b19da | |||
| 6f19a7a20f | |||
| a8740cee4b | |||
| 4000fe383d | |||
| 75445e604e | |||
| 0596278e50 | |||
| 7a77775a42 |
@@ -234,6 +234,8 @@ You can use SimpleX with your own servers and still communicate with people usin
|
|||||||
|
|
||||||
Recent and important updates:
|
Recent and important updates:
|
||||||
|
|
||||||
|
[Jun 4, 2024. SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md)
|
||||||
|
|
||||||
[Apr 26, 2024. SimpleX network: legally binding transparency, v5.7 released with better calls and messages.](./blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md)
|
[Apr 26, 2024. SimpleX network: legally binding transparency, v5.7 released with better calls and messages.](./blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md)
|
||||||
|
|
||||||
[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md)
|
[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md)
|
||||||
@@ -382,10 +384,10 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
|
|||||||
- ✅ Private notes.
|
- ✅ Private notes.
|
||||||
- ✅ Improve sending videos (including encryption of locally stored videos).
|
- ✅ Improve sending videos (including encryption of locally stored videos).
|
||||||
- ✅ Post-quantum resistant key exchange in double ratchet protocol.
|
- ✅ Post-quantum resistant key exchange in double ratchet protocol.
|
||||||
|
- ✅ Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||||
- 🏗 Improve stability and reduce battery usage.
|
- 🏗 Improve stability and reduce battery usage.
|
||||||
- 🏗 Improve experience for the new users.
|
- 🏗 Improve experience for the new users.
|
||||||
- 🏗 Large groups, communities and public channels.
|
- 🏗 Large groups, communities and public channels.
|
||||||
- 🏗 Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
|
||||||
- Privacy & security slider - a simple way to set all settings at once.
|
- Privacy & security slider - a simple way to set all settings at once.
|
||||||
- SMP queue redundancy and rotation (manual is supported).
|
- SMP queue redundancy and rotation (manual is supported).
|
||||||
- Include optional message into connection request sent via contact address.
|
- Include optional message into connection request sent via contact address.
|
||||||
|
|||||||
@@ -1826,11 +1826,15 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
if let aChatItem = aChatItem {
|
if let aChatItem = aChatItem {
|
||||||
await chatItemSimpleUpdate(user, aChatItem)
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
}
|
}
|
||||||
case let .rcvFileError(user, aChatItem, _):
|
case let .rcvFileError(user, aChatItem, _, _):
|
||||||
if let aChatItem = aChatItem {
|
if let aChatItem = aChatItem {
|
||||||
await chatItemSimpleUpdate(user, aChatItem)
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
Task { cleanupFile(aChatItem) }
|
Task { cleanupFile(aChatItem) }
|
||||||
}
|
}
|
||||||
|
case let .rcvFileWarning(user, aChatItem, _, _):
|
||||||
|
if let aChatItem = aChatItem {
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
}
|
||||||
case let .sndFileStart(user, aChatItem, _):
|
case let .sndFileStart(user, aChatItem, _):
|
||||||
await chatItemSimpleUpdate(user, aChatItem)
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
case let .sndFileComplete(user, aChatItem, _):
|
case let .sndFileComplete(user, aChatItem, _):
|
||||||
@@ -1852,6 +1856,10 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
await chatItemSimpleUpdate(user, aChatItem)
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
Task { cleanupFile(aChatItem) }
|
Task { cleanupFile(aChatItem) }
|
||||||
}
|
}
|
||||||
|
case let .sndFileWarning(user, aChatItem, _, _):
|
||||||
|
if let aChatItem = aChatItem {
|
||||||
|
await chatItemSimpleUpdate(user, aChatItem)
|
||||||
|
}
|
||||||
case let .callInvitation(invitation):
|
case let .callInvitation(invitation):
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
m.callInvitations[invitation.contact.id] = invitation
|
m.callInvitations[invitation.contact.id] = invitation
|
||||||
@@ -1949,12 +1957,28 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
let state = UIRemoteCtrlSessionState.connected(remoteCtrl: remoteCtrl, sessionCode: m.remoteCtrlSession?.sessionCode ?? "")
|
let state = UIRemoteCtrlSessionState.connected(remoteCtrl: remoteCtrl, sessionCode: m.remoteCtrlSession?.sessionCode ?? "")
|
||||||
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state)
|
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state)
|
||||||
}
|
}
|
||||||
case .remoteCtrlStopped:
|
case let .remoteCtrlStopped(_, rcStopReason):
|
||||||
// This delay is needed to cancel the session that fails on network failure,
|
// This delay is needed to cancel the session that fails on network failure,
|
||||||
// e.g. when user did not grant permission to access local network yet.
|
// e.g. when user did not grant permission to access local network yet.
|
||||||
if let sess = m.remoteCtrlSession {
|
if let sess = m.remoteCtrlSession {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
m.remoteCtrlSession = nil
|
m.remoteCtrlSession = nil
|
||||||
|
dismissAllSheets() {
|
||||||
|
switch rcStopReason {
|
||||||
|
case .connectionFailed(.errorAgent(.RCP(.identity))):
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title: "Connection with desktop stopped",
|
||||||
|
message: "This link was used with another mobile device, please create a new link on the desktop."
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("Connection with desktop stopped"),
|
||||||
|
message: Text("Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers."),
|
||||||
|
primaryButton: .default(Text("Ok")),
|
||||||
|
secondaryButton: .default(Text("Copy error")) { UIPasteboard.general.string = String(describing: rcStopReason) }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if case .connected = sess.sessionState {
|
if case .connected = sess.sessionState {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
|
|||||||
@@ -56,14 +56,16 @@ struct CIFileView: View {
|
|||||||
case .sndTransfer: return false
|
case .sndTransfer: return false
|
||||||
case .sndComplete: return true
|
case .sndComplete: return true
|
||||||
case .sndCancelled: return false
|
case .sndCancelled: return false
|
||||||
case .sndError: return false
|
case .sndError: return true
|
||||||
|
case .sndWarning: return true
|
||||||
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 .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 true
|
||||||
|
case .rcvWarning: return true
|
||||||
case .invalid: return false
|
case .invalid: return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,6 +110,18 @@ struct CIFileView: View {
|
|||||||
if let fileSource = getLoadedFileSource(file) {
|
if let fileSource = getLoadedFileSource(file) {
|
||||||
saveCryptoFile(fileSource)
|
saveCryptoFile(fileSource)
|
||||||
}
|
}
|
||||||
|
case let .rcvError(rcvFileError):
|
||||||
|
logger.debug("CIFileView fileAction - in .rcvError")
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("File error"),
|
||||||
|
message: Text(rcvFileError.errorInfo)
|
||||||
|
))
|
||||||
|
case let .rcvWarning(rcvFileError):
|
||||||
|
logger.debug("CIFileView fileAction - in .rcvWarning")
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("Temporary file error"),
|
||||||
|
message: Text(rcvFileError.errorInfo)
|
||||||
|
))
|
||||||
case .sndStored:
|
case .sndStored:
|
||||||
logger.debug("CIFileView fileAction - in .sndStored")
|
logger.debug("CIFileView fileAction - in .sndStored")
|
||||||
if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) {
|
if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) {
|
||||||
@@ -118,6 +132,18 @@ struct CIFileView: View {
|
|||||||
if let fileSource = getLoadedFileSource(file) {
|
if let fileSource = getLoadedFileSource(file) {
|
||||||
saveCryptoFile(fileSource)
|
saveCryptoFile(fileSource)
|
||||||
}
|
}
|
||||||
|
case let .sndError(sndFileError):
|
||||||
|
logger.debug("CIFileView fileAction - in .sndError")
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("File error"),
|
||||||
|
message: Text(sndFileError.errorInfo)
|
||||||
|
))
|
||||||
|
case let .sndWarning(sndFileError):
|
||||||
|
logger.debug("CIFileView fileAction - in .sndWarning")
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("Temporary file error"),
|
||||||
|
message: Text(sndFileError.errorInfo)
|
||||||
|
))
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,6 +167,7 @@ struct CIFileView: View {
|
|||||||
case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10)
|
case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10)
|
||||||
case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||||
case .sndError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
case .sndError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||||
|
case .sndWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10)
|
||||||
case .rcvInvitation:
|
case .rcvInvitation:
|
||||||
if fileSizeValid(file) {
|
if fileSizeValid(file) {
|
||||||
fileIcon("arrow.down.doc.fill", color: .accentColor)
|
fileIcon("arrow.down.doc.fill", color: .accentColor)
|
||||||
@@ -159,6 +186,7 @@ struct CIFileView: View {
|
|||||||
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)
|
||||||
|
case .rcvWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10)
|
||||||
case .invalid: fileIcon("doc.fill", innerIcon: "questionmark", innerIconSize: 10)
|
case .invalid: fileIcon("doc.fill", innerIcon: "questionmark", innerIconSize: 10)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -60,6 +60,26 @@ struct CIImageView: View {
|
|||||||
case .rcvTransfer: () // ?
|
case .rcvTransfer: () // ?
|
||||||
case .rcvComplete: () // ?
|
case .rcvComplete: () // ?
|
||||||
case .rcvCancelled: () // TODO
|
case .rcvCancelled: () // TODO
|
||||||
|
case let .rcvError(rcvFileError):
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("File error"),
|
||||||
|
message: Text(rcvFileError.errorInfo)
|
||||||
|
))
|
||||||
|
case let .rcvWarning(rcvFileError):
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("Temporary file error"),
|
||||||
|
message: Text(rcvFileError.errorInfo)
|
||||||
|
))
|
||||||
|
case let .sndError(sndFileError):
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("File error"),
|
||||||
|
message: Text(sndFileError.errorInfo)
|
||||||
|
))
|
||||||
|
case let .sndWarning(sndFileError):
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("Temporary file error"),
|
||||||
|
message: Text(sndFileError.errorInfo)
|
||||||
|
))
|
||||||
default: ()
|
default: ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,14 +119,16 @@ struct CIImageView: View {
|
|||||||
case .sndComplete: fileIcon("checkmark", 10, 13)
|
case .sndComplete: fileIcon("checkmark", 10, 13)
|
||||||
case .sndCancelled: fileIcon("xmark", 10, 13)
|
case .sndCancelled: fileIcon("xmark", 10, 13)
|
||||||
case .sndError: fileIcon("xmark", 10, 13)
|
case .sndError: fileIcon("xmark", 10, 13)
|
||||||
|
case .sndWarning: fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||||
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 .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
|
||||||
|
case .rcvComplete: EmptyView()
|
||||||
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 .rcvWarning: fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||||
case .invalid: fileIcon("questionmark", 10, 13)
|
case .invalid: fileIcon("questionmark", 10, 13)
|
||||||
default: EmptyView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ struct CIVideoView: View {
|
|||||||
.disabled(!canBePlayed)
|
.disabled(!canBePlayed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadingIndicator()
|
fileStatusIcon()
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
addObserver(player, url)
|
addObserver(player, url)
|
||||||
@@ -258,11 +258,11 @@ struct CIVideoView: View {
|
|||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.frame(width: w)
|
.frame(width: w)
|
||||||
loadingIndicator()
|
fileStatusIcon()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func loadingIndicator() -> some View {
|
@ViewBuilder private func fileStatusIcon() -> some View {
|
||||||
if let file = chatItem.file {
|
if let file = chatItem.file {
|
||||||
switch file.fileStatus {
|
switch file.fileStatus {
|
||||||
case .sndStored:
|
case .sndStored:
|
||||||
@@ -279,7 +279,22 @@ struct CIVideoView: View {
|
|||||||
}
|
}
|
||||||
case .sndComplete: fileIcon("checkmark", 10, 13)
|
case .sndComplete: fileIcon("checkmark", 10, 13)
|
||||||
case .sndCancelled: fileIcon("xmark", 10, 13)
|
case .sndCancelled: fileIcon("xmark", 10, 13)
|
||||||
case .sndError: fileIcon("xmark", 10, 13)
|
case let .sndError(sndFileError):
|
||||||
|
fileIcon("xmark", 10, 13)
|
||||||
|
.onTapGesture {
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("File error"),
|
||||||
|
message: Text(sndFileError.errorInfo)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
case let .sndWarning(sndFileError):
|
||||||
|
fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||||
|
.onTapGesture {
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("Temporary file error"),
|
||||||
|
message: Text(sndFileError.errorInfo)
|
||||||
|
))
|
||||||
|
}
|
||||||
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 let .rcvTransfer(rcvProgress, rcvTotal):
|
case let .rcvTransfer(rcvProgress, rcvTotal):
|
||||||
@@ -289,10 +304,25 @@ struct CIVideoView: View {
|
|||||||
progressView()
|
progressView()
|
||||||
}
|
}
|
||||||
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
|
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
|
||||||
|
case .rcvComplete: EmptyView()
|
||||||
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
||||||
case .rcvError: fileIcon("xmark", 10, 13)
|
case let .rcvError(rcvFileError):
|
||||||
|
fileIcon("xmark", 10, 13)
|
||||||
|
.onTapGesture {
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("File error"),
|
||||||
|
message: Text(rcvFileError.errorInfo)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
case let .rcvWarning(rcvFileError):
|
||||||
|
fileIcon("exclamationmark.triangle.fill", 10, 13)
|
||||||
|
.onTapGesture {
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("Temporary file error"),
|
||||||
|
message: Text(rcvFileError.errorInfo)
|
||||||
|
))
|
||||||
|
}
|
||||||
case .invalid: fileIcon("questionmark", 10, 13)
|
case .invalid: fileIcon("questionmark", 10, 13)
|
||||||
default: EmptyView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,18 +134,53 @@ struct VoiceMessagePlayer: View {
|
|||||||
ZStack {
|
ZStack {
|
||||||
if let recordingFile = recordingFile {
|
if let recordingFile = recordingFile {
|
||||||
switch recordingFile.fileStatus {
|
switch recordingFile.fileStatus {
|
||||||
case .sndStored: playbackButton()
|
case .sndStored:
|
||||||
case .sndTransfer: playbackButton()
|
if recordingFile.fileProtocol == .local {
|
||||||
|
playbackButton()
|
||||||
|
} else {
|
||||||
|
loadingIcon()
|
||||||
|
}
|
||||||
|
case .sndTransfer: loadingIcon()
|
||||||
case .sndComplete: playbackButton()
|
case .sndComplete: playbackButton()
|
||||||
case .sndCancelled: playbackButton()
|
case .sndCancelled: playbackButton()
|
||||||
case .sndError: playbackButton()
|
case let .sndError(sndFileError):
|
||||||
|
fileStatusIcon("multiply", 14)
|
||||||
|
.onTapGesture {
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("File error"),
|
||||||
|
message: Text(sndFileError.errorInfo)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
case let .sndWarning(sndFileError):
|
||||||
|
fileStatusIcon("exclamationmark.triangle.fill", 16)
|
||||||
|
.onTapGesture {
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("Temporary file error"),
|
||||||
|
message: Text(sndFileError.errorInfo)
|
||||||
|
))
|
||||||
|
}
|
||||||
case .rcvInvitation: downloadButton(recordingFile, "play.fill")
|
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 .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 let .rcvError(rcvFileError):
|
||||||
|
fileStatusIcon("multiply", 14)
|
||||||
|
.onTapGesture {
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("File error"),
|
||||||
|
message: Text(rcvFileError.errorInfo)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
case let .rcvWarning(rcvFileError):
|
||||||
|
fileStatusIcon("exclamationmark.triangle.fill", 16)
|
||||||
|
.onTapGesture {
|
||||||
|
AlertManager.shared.showAlert(Alert(
|
||||||
|
title: Text("Temporary file error"),
|
||||||
|
message: Text(rcvFileError.errorInfo)
|
||||||
|
))
|
||||||
|
}
|
||||||
case .invalid: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
case .invalid: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -246,6 +281,17 @@ struct VoiceMessagePlayer: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func fileStatusIcon(_ image: String, _ size: CGFloat) -> some View {
|
||||||
|
Image(systemName: image)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||||
|
.frame(width: 56, height: 56)
|
||||||
|
.background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear)
|
||||||
|
.clipShape(Circle())
|
||||||
|
}
|
||||||
|
|
||||||
private func loadingIcon() -> some View {
|
private func loadingIcon() -> some View {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.frame(width: 30, height: 30)
|
.frame(width: 30, height: 30)
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ struct ChatItemForwardingView: View {
|
|||||||
|
|
||||||
@State private var searchText: String = ""
|
@State private var searchText: String = ""
|
||||||
@FocusState private var searchFocused
|
@FocusState private var searchFocused
|
||||||
|
@State private var alert: SomeAlert?
|
||||||
|
@State private var hasSimplexLink_: Bool?
|
||||||
|
private let chatsToForwardTo = filterChatsToForwardTo()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
@@ -35,47 +38,29 @@ struct ChatItemForwardingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.alert(item: $alert) { $0.alert }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func forwardListView() -> some View {
|
@ViewBuilder private func forwardListView() -> some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
let chatsToForwardTo = filterChatsToForwardTo()
|
|
||||||
if !chatsToForwardTo.isEmpty {
|
if !chatsToForwardTo.isEmpty {
|
||||||
ScrollView {
|
List {
|
||||||
LazyVStack(alignment: .leading, spacing: 8) {
|
searchFieldView(text: $searchText, focussed: $searchFocused)
|
||||||
searchFieldView(text: $searchText, focussed: $searchFocused)
|
.padding(.leading, 2)
|
||||||
.padding(.leading, 2)
|
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||||
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { foundChat($0, s) }
|
||||||
let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { filterChatSearched($0, s) }
|
ForEach(chats) { chat in
|
||||||
ForEach(chats) { chat in
|
forwardListChatView(chat)
|
||||||
Divider()
|
.disabled(chatModel.deletedChats.contains(chat.chatInfo.id))
|
||||||
forwardListNavLinkView(chat)
|
|
||||||
.disabled(chatModel.deletedChats.contains(chat.chatInfo.id))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.background(Color(uiColor: .systemBackground))
|
|
||||||
.cornerRadius(12)
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func filterChatsToForwardTo() -> [Chat] {
|
private func foundChat(_ chat: Chat, _ searchStr: String) -> Bool {
|
||||||
var filteredChats = chatModel.chats.filter({ canForwardToChat($0) })
|
|
||||||
if let index = filteredChats.firstIndex(where: { $0.chatInfo.chatType == .local }) {
|
|
||||||
let privateNotes = filteredChats.remove(at: index)
|
|
||||||
filteredChats.insert(privateNotes, at: 0)
|
|
||||||
}
|
|
||||||
return filteredChats
|
|
||||||
}
|
|
||||||
|
|
||||||
private func filterChatSearched(_ chat: Chat, _ searchStr: String) -> Bool {
|
|
||||||
let cInfo = chat.chatInfo
|
let cInfo = chat.chatInfo
|
||||||
return switch cInfo {
|
return switch cInfo {
|
||||||
case let .direct(contact):
|
case let .direct(contact):
|
||||||
@@ -91,42 +76,70 @@ struct ChatItemForwardingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func canForwardToChat(_ chat: Chat) -> Bool {
|
private func prohibitedByPref(_ chat: Chat) -> Bool {
|
||||||
switch chat.chatInfo {
|
// preference checks should match checks in compose view
|
||||||
case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv
|
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
|
||||||
case let .group(groupInfo): groupInfo.sendMsgEnabled
|
let fileProhibited = (ci.content.msgContent?.isMediaOrFileAttachment ?? false) && !chat.groupFeatureEnabled(.files)
|
||||||
case let .local(noteFolder): noteFolder.sendMsgEnabled
|
let voiceProhibited = (ci.content.msgContent?.isVoice ?? false) && !chat.chatInfo.featureEnabled(.voice)
|
||||||
|
return switch chat.chatInfo {
|
||||||
|
case .direct: voiceProhibited
|
||||||
|
case .group: simplexLinkProhibited || fileProhibited || voiceProhibited
|
||||||
|
case .local: false
|
||||||
case .contactRequest: false
|
case .contactRequest: false
|
||||||
case .contactConnection: false
|
case .contactConnection: false
|
||||||
case .invalidJSON: false
|
case .invalidJSON: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var hasSimplexLink: Bool {
|
||||||
|
if let hasSimplexLink_ { return hasSimplexLink_ }
|
||||||
|
let r =
|
||||||
|
if let mcText = ci.content.msgContent?.text,
|
||||||
|
let parsedMsg = parseSimpleXMarkdown(mcText) {
|
||||||
|
parsedMsgHasSimplexLink(parsedMsg)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
hasSimplexLink_ = r
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
private func emptyList() -> some View {
|
private func emptyList() -> some View {
|
||||||
Text("No filtered chats")
|
Text("No filtered chats")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func forwardListNavLinkView(_ chat: Chat) -> some View {
|
@ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View {
|
||||||
|
let prohibited = prohibitedByPref(chat)
|
||||||
Button {
|
Button {
|
||||||
dismiss()
|
if prohibited {
|
||||||
if chat.id == fromChatInfo.id {
|
alert = SomeAlert(
|
||||||
composeState = ComposeState(
|
alert: mkAlert(
|
||||||
message: composeState.message,
|
title: "Cannot forward message",
|
||||||
preview: composeState.linkPreview != nil ? composeState.preview : .noPreview,
|
message: "Selected chat preferences prohibit this message."
|
||||||
contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo)
|
),
|
||||||
)
|
id: "forward prohibited by preferences"
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo)
|
dismiss()
|
||||||
chatModel.chatId = chat.id
|
if chat.id == fromChatInfo.id {
|
||||||
|
composeState = ComposeState(
|
||||||
|
message: composeState.message,
|
||||||
|
preview: composeState.linkPreview != nil ? composeState.preview : .noPreview,
|
||||||
|
contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo)
|
||||||
|
chatModel.chatId = chat.id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
ChatInfoImage(chat: chat, size: 30)
|
ChatInfoImage(chat: chat, size: 30)
|
||||||
.padding(.trailing, 2)
|
.padding(.trailing, 2)
|
||||||
Text(chat.chatInfo.chatViewName)
|
Text(chat.chatInfo.chatViewName)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(prohibited ? .secondary : .primary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
if chat.chatInfo.incognito {
|
if chat.chatInfo.incognito {
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -142,6 +155,27 @@ struct ChatItemForwardingView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func filterChatsToForwardTo() -> [Chat] {
|
||||||
|
var filteredChats = ChatModel.shared.chats.filter { c in
|
||||||
|
c.chatInfo.chatType != .local && canForwardToChat(c)
|
||||||
|
}
|
||||||
|
if let privateNotes = ChatModel.shared.chats.first(where: { $0.chatInfo.chatType == .local }) {
|
||||||
|
filteredChats.insert(privateNotes, at: 0)
|
||||||
|
}
|
||||||
|
return filteredChats
|
||||||
|
}
|
||||||
|
|
||||||
|
private func canForwardToChat(_ chat: Chat) -> Bool {
|
||||||
|
switch chat.chatInfo {
|
||||||
|
case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv
|
||||||
|
case let .group(groupInfo): groupInfo.sendMsgEnabled
|
||||||
|
case let .local(noteFolder): noteFolder.sendMsgEnabled
|
||||||
|
case .contactRequest: false
|
||||||
|
case .contactConnection: false
|
||||||
|
case .invalidJSON: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
ChatItemForwardingView(
|
ChatItemForwardingView(
|
||||||
ci: ChatItem.getSample(1, .directSnd, .now, "hello"),
|
ci: ChatItem.getSample(1, .directSnd, .now, "hello"),
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ struct ChatItemInfoView: View {
|
|||||||
@Binding var chatItemInfo: ChatItemInfo?
|
@Binding var chatItemInfo: ChatItemInfo?
|
||||||
@State private var selection: CIInfoTab = .history
|
@State private var selection: CIInfoTab = .history
|
||||||
@State private var alert: CIInfoViewAlert? = nil
|
@State private var alert: CIInfoViewAlert? = nil
|
||||||
|
@State private var messageStatusLimited: Bool = true
|
||||||
|
@State private var fileStatusLimited: Bool = true
|
||||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||||
|
|
||||||
enum CIInfoTab {
|
enum CIInfoTab {
|
||||||
@@ -157,6 +159,35 @@ struct ChatItemInfoView: View {
|
|||||||
if developerTools {
|
if developerTools {
|
||||||
infoRow("Database ID", "\(meta.itemId)")
|
infoRow("Database ID", "\(meta.itemId)")
|
||||||
infoRow("Record updated at", localTimestamp(meta.updatedAt))
|
infoRow("Record updated at", localTimestamp(meta.updatedAt))
|
||||||
|
let msv = infoRow("Message status", ci.meta.itemStatus.id)
|
||||||
|
Group {
|
||||||
|
if messageStatusLimited {
|
||||||
|
msv.lineLimit(1)
|
||||||
|
} else {
|
||||||
|
msv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
withAnimation {
|
||||||
|
messageStatusLimited.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let file = ci.file {
|
||||||
|
let fsv = infoRow("File status", file.fileStatus.id)
|
||||||
|
Group {
|
||||||
|
if fileStatusLimited {
|
||||||
|
fsv.lineLimit(1)
|
||||||
|
} else {
|
||||||
|
fsv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
withAnimation {
|
||||||
|
fileStatusLimited.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -473,8 +504,12 @@ struct ChatItemInfoView: View {
|
|||||||
if developerTools {
|
if developerTools {
|
||||||
shareText += [
|
shareText += [
|
||||||
String.localizedStringWithFormat(NSLocalizedString("Database ID: %d", comment: "copied message info"), meta.itemId),
|
String.localizedStringWithFormat(NSLocalizedString("Database ID: %d", comment: "copied message info"), meta.itemId),
|
||||||
String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt))
|
String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt)),
|
||||||
|
String.localizedStringWithFormat(NSLocalizedString("Message status: %@", comment: "copied message info"), meta.itemStatus.id)
|
||||||
]
|
]
|
||||||
|
if let file = ci.file {
|
||||||
|
shareText += [String.localizedStringWithFormat(NSLocalizedString("File status: %@", comment: "copied message info"), file.fileStatus.id)]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let qi = ci.quotedItem {
|
if let qi = ci.quotedItem {
|
||||||
shareText += ["", NSLocalizedString("## In reply to", comment: "copied message info")]
|
shareText += ["", NSLocalizedString("## In reply to", comment: "copied message info")]
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ struct ComposeView: View {
|
|||||||
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
||||||
ContextInvitingContactMemberView()
|
ContextInvitingContactMemberView()
|
||||||
}
|
}
|
||||||
|
// preference checks should match checks in forwarding list
|
||||||
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
|
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
|
||||||
let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files)
|
let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files)
|
||||||
let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice)
|
let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice)
|
||||||
@@ -1065,7 +1066,7 @@ struct ComposeView: View {
|
|||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
let simplexLink = parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
|
let simplexLink = parsedMsgHasSimplexLink(parsedMsg)
|
||||||
return (url, simplexLink)
|
return (url, simplexLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1105,6 +1106,10 @@ struct ComposeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parsedMsgHasSimplexLink(_ parsedMsg: [FormattedText]) -> Bool {
|
||||||
|
parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
|
||||||
|
}
|
||||||
|
|
||||||
struct ComposeView_Previews: PreviewProvider {
|
struct ComposeView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
||||||
|
|||||||
@@ -11,14 +11,9 @@ import SimpleXChat
|
|||||||
import CodeScanner
|
import CodeScanner
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
enum SomeAlert: Identifiable {
|
struct SomeAlert: Identifiable {
|
||||||
case someAlert(alert: Alert, id: String)
|
var alert: Alert
|
||||||
|
var id: String
|
||||||
var id: String {
|
|
||||||
switch self {
|
|
||||||
case let .someAlert(_, id): return id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum NewChatViewAlert: Identifiable {
|
private enum NewChatViewAlert: Identifiable {
|
||||||
@@ -142,8 +137,8 @@ struct NewChatView: View {
|
|||||||
switch(a) {
|
switch(a) {
|
||||||
case let .planAndConnectAlert(alert):
|
case let .planAndConnectAlert(alert):
|
||||||
return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" })
|
return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" })
|
||||||
case let .newChatSomeAlert(.someAlert(alert, _)):
|
case let .newChatSomeAlert(a):
|
||||||
return alert
|
return a.alert
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,7 +176,7 @@ struct NewChatView: View {
|
|||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
creatingConnReq = false
|
creatingConnReq = false
|
||||||
if let apiAlert = apiAlert {
|
if let apiAlert = apiAlert {
|
||||||
alert = .newChatSomeAlert(alert: .someAlert(alert: apiAlert, id: "createInvitation error"))
|
alert = .newChatSomeAlert(alert: SomeAlert(alert: apiAlert, id: "createInvitation error"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,7 +310,7 @@ private struct ConnectView: View {
|
|||||||
// showQRCodeScanner = false
|
// showQRCodeScanner = false
|
||||||
connect(pastedLink)
|
connect(pastedLink)
|
||||||
} else {
|
} else {
|
||||||
alert = .newChatSomeAlert(alert: .someAlert(
|
alert = .newChatSomeAlert(alert: SomeAlert(
|
||||||
alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."),
|
alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."),
|
||||||
id: "pasteLinkView: code is not a SimpleX link"
|
id: "pasteLinkView: code is not a SimpleX link"
|
||||||
))
|
))
|
||||||
@@ -338,14 +333,14 @@ private struct ConnectView: View {
|
|||||||
if strIsSimplexLink(r.string) {
|
if strIsSimplexLink(r.string) {
|
||||||
connect(link)
|
connect(link)
|
||||||
} else {
|
} else {
|
||||||
alert = .newChatSomeAlert(alert: .someAlert(
|
alert = .newChatSomeAlert(alert: SomeAlert(
|
||||||
alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."),
|
alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."),
|
||||||
id: "processQRCode: code is not a SimpleX link"
|
id: "processQRCode: code is not a SimpleX link"
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
case let .failure(e):
|
case let .failure(e):
|
||||||
logger.error("processQRCode QR code error: \(e.localizedDescription)")
|
logger.error("processQRCode QR code error: \(e.localizedDescription)")
|
||||||
alert = .newChatSomeAlert(alert: .someAlert(
|
alert = .newChatSomeAlert(alert: SomeAlert(
|
||||||
alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"),
|
alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"),
|
||||||
id: "processQRCode: failure"
|
id: "processQRCode: failure"
|
||||||
))
|
))
|
||||||
@@ -367,11 +362,12 @@ struct ScannerInView: View {
|
|||||||
@Binding var showQRCodeScanner: Bool
|
@Binding var showQRCodeScanner: Bool
|
||||||
let processQRCode: (_ resp: Result<ScanResult, ScanError>) -> Void
|
let processQRCode: (_ resp: Result<ScanResult, ScanError>) -> Void
|
||||||
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
|
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
|
||||||
|
var scanMode: ScanMode = .continuous
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if showQRCodeScanner, case .authorized = cameraAuthorizationStatus {
|
if showQRCodeScanner, case .authorized = cameraAuthorizationStatus {
|
||||||
CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode)
|
CodeScannerView(codeTypes: [.qr], scanMode: scanMode, completion: processQRCode)
|
||||||
.aspectRatio(1, contentMode: .fit)
|
.aspectRatio(1, contentMode: .fit)
|
||||||
.cornerRadius(12)
|
.cornerRadius(12)
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
@@ -436,6 +432,7 @@ struct ScannerInView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func linkTextView(_ link: String) -> some View {
|
private func linkTextView(_ link: String) -> some View {
|
||||||
Text(link)
|
Text(link)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|||||||
@@ -181,23 +181,27 @@ struct ConnectDesktopView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View {
|
private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View {
|
||||||
List {
|
ZStack {
|
||||||
Section("Connecting to desktop") {
|
List {
|
||||||
ctrlDeviceNameText(session, rc)
|
Section("Connecting to desktop") {
|
||||||
ctrlDeviceVersionText(session)
|
ctrlDeviceNameText(session, rc)
|
||||||
}
|
ctrlDeviceVersionText(session)
|
||||||
|
}
|
||||||
|
|
||||||
if let sessCode = session.sessionCode {
|
if let sessCode = session.sessionCode {
|
||||||
Section("Session code") {
|
Section("Session code") {
|
||||||
sessionCodeText(sessCode)
|
sessionCodeText(sessCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
disconnectButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.navigationTitle("Connecting to desktop")
|
||||||
|
|
||||||
Section {
|
ProgressView().scaleEffect(2)
|
||||||
disconnectButton()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.navigationTitle("Connecting to desktop")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func searchingDesktopView() -> some View {
|
private func searchingDesktopView() -> some View {
|
||||||
@@ -332,13 +336,7 @@ struct ConnectDesktopView: View {
|
|||||||
|
|
||||||
private func scanDesctopAddressView() -> some View {
|
private func scanDesctopAddressView() -> some View {
|
||||||
Section("Scan QR code from desktop") {
|
Section("Scan QR code from desktop") {
|
||||||
CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processDesktopQRCode)
|
ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processDesktopQRCode, scanMode: .oncePerCode)
|
||||||
.aspectRatio(1, contentMode: .fit)
|
|
||||||
.cornerRadius(12)
|
|
||||||
.listRowBackground(Color.clear)
|
|
||||||
.listRowSeparator(.hidden)
|
|
||||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,6 @@
|
|||||||
5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; };
|
5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; };
|
||||||
5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; };
|
5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; };
|
||||||
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; };
|
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; };
|
||||||
5C0EA13B2C0B176B00AD2E5E /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1362C0B176B00AD2E5E /* libgmp.a */; };
|
|
||||||
5C0EA13C2C0B176B00AD2E5E /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1372C0B176B00AD2E5E /* libgmpxx.a */; };
|
|
||||||
5C0EA13D2C0B176B00AD2E5E /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1382C0B176B00AD2E5E /* libffi.a */; };
|
|
||||||
5C0EA13E2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1392C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a */; };
|
|
||||||
5C0EA13F2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA13A2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc.a */; };
|
|
||||||
5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */; };
|
5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */; };
|
||||||
5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */; };
|
5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */; };
|
||||||
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
|
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
|
||||||
@@ -197,6 +192,11 @@
|
|||||||
D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; };
|
D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; };
|
||||||
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
|
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
|
||||||
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
|
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
|
||||||
|
E5D68D3F2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */; };
|
||||||
|
E5D68D402C22D78C00CBA347 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3B2C22D78C00CBA347 /* libffi.a */; };
|
||||||
|
E5D68D412C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */; };
|
||||||
|
E5D68D422C22D78C00CBA347 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3D2C22D78C00CBA347 /* libgmp.a */; };
|
||||||
|
E5D68D432C22D78C00CBA347 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -273,11 +273,6 @@
|
|||||||
5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = "<group>"; };
|
5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = "<group>"; };
|
||||||
5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = "<group>"; };
|
5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = "<group>"; };
|
||||||
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = "<group>"; };
|
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = "<group>"; };
|
||||||
5C0EA1362C0B176B00AD2E5E /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
|
||||||
5C0EA1372C0B176B00AD2E5E /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
|
||||||
5C0EA1382C0B176B00AD2E5E /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
|
||||||
5C0EA1392C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a"; sourceTree = "<group>"; };
|
|
||||||
5C0EA13A2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc.a"; sourceTree = "<group>"; };
|
|
||||||
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = "<group>"; };
|
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = "<group>"; };
|
||||||
5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMediaView.swift; sourceTree = "<group>"; };
|
5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMediaView.swift; sourceTree = "<group>"; };
|
||||||
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = "<group>"; };
|
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = "<group>"; };
|
||||||
@@ -492,6 +487,11 @@
|
|||||||
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
|
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||||
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||||
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||||
|
E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a"; sourceTree = "<group>"; };
|
||||||
|
E5D68D3B2C22D78C00CBA347 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||||
|
E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||||
|
E5D68D3D2C22D78C00CBA347 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||||
|
E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -529,13 +529,13 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
5C0EA13C2C0B176B00AD2E5E /* libgmpxx.a in Frameworks */,
|
E5D68D412C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */,
|
||||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||||
5C0EA13B2C0B176B00AD2E5E /* libgmp.a in Frameworks */,
|
|
||||||
5C0EA13D2C0B176B00AD2E5E /* libffi.a in Frameworks */,
|
|
||||||
5C0EA13F2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc.a in Frameworks */,
|
|
||||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||||
5C0EA13E2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a in Frameworks */,
|
E5D68D3F2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */,
|
||||||
|
E5D68D422C22D78C00CBA347 /* libgmp.a in Frameworks */,
|
||||||
|
E5D68D402C22D78C00CBA347 /* libffi.a in Frameworks */,
|
||||||
|
E5D68D432C22D78C00CBA347 /* libgmpxx.a in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -601,11 +601,11 @@
|
|||||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5C0EA1382C0B176B00AD2E5E /* libffi.a */,
|
E5D68D3B2C22D78C00CBA347 /* libffi.a */,
|
||||||
5C0EA1362C0B176B00AD2E5E /* libgmp.a */,
|
E5D68D3D2C22D78C00CBA347 /* libgmp.a */,
|
||||||
5C0EA1372C0B176B00AD2E5E /* libgmpxx.a */,
|
E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */,
|
||||||
5C0EA1392C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a */,
|
E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */,
|
||||||
5C0EA13A2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc.a */,
|
E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.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 = 223;
|
CURRENT_PROJECT_VERSION = 225;
|
||||||
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.8;
|
MARKETING_VERSION = 5.8.1;
|
||||||
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 = 223;
|
CURRENT_PROJECT_VERSION = 225;
|
||||||
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.8;
|
MARKETING_VERSION = 5.8.1;
|
||||||
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 = 223;
|
CURRENT_PROJECT_VERSION = 225;
|
||||||
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.8;
|
MARKETING_VERSION = 5.8.1;
|
||||||
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 = 223;
|
CURRENT_PROJECT_VERSION = 225;
|
||||||
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.8;
|
MARKETING_VERSION = 5.8.1;
|
||||||
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 = 223;
|
CURRENT_PROJECT_VERSION = 225;
|
||||||
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.8;
|
MARKETING_VERSION = 5.8.1;
|
||||||
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 = 223;
|
CURRENT_PROJECT_VERSION = 225;
|
||||||
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.8;
|
MARKETING_VERSION = 5.8.1;
|
||||||
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;
|
||||||
|
|||||||
@@ -622,7 +622,8 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case rcvStandaloneFileComplete(user: UserRef, targetPath: String, rcvFileTransfer: RcvFileTransfer)
|
case rcvStandaloneFileComplete(user: UserRef, targetPath: String, rcvFileTransfer: RcvFileTransfer)
|
||||||
case rcvFileCancelled(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer)
|
case rcvFileCancelled(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer)
|
||||||
case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
|
case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
|
||||||
case rcvFileError(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer)
|
case rcvFileError(user: UserRef, chatItem_: AChatItem?, agentError: AgentErrorType, rcvFileTransfer: RcvFileTransfer)
|
||||||
|
case rcvFileWarning(user: UserRef, chatItem_: AChatItem?, agentError: AgentErrorType, rcvFileTransfer: RcvFileTransfer)
|
||||||
// sending file events
|
// sending file events
|
||||||
case sndFileStart(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
case sndFileStart(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||||
case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||||
@@ -636,6 +637,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case sndStandaloneFileComplete(user: UserRef, fileTransferMeta: FileTransferMeta, rcvURIs: [String])
|
case sndStandaloneFileComplete(user: UserRef, fileTransferMeta: FileTransferMeta, rcvURIs: [String])
|
||||||
case sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta)
|
case sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta)
|
||||||
case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String)
|
case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String)
|
||||||
|
case sndFileWarning(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String)
|
||||||
// call events
|
// call events
|
||||||
case callInvitation(callInvitation: RcvCallInvitation)
|
case callInvitation(callInvitation: RcvCallInvitation)
|
||||||
case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
|
case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
|
||||||
@@ -784,6 +786,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case .rcvFileCancelled: return "rcvFileCancelled"
|
case .rcvFileCancelled: return "rcvFileCancelled"
|
||||||
case .rcvFileSndCancelled: return "rcvFileSndCancelled"
|
case .rcvFileSndCancelled: return "rcvFileSndCancelled"
|
||||||
case .rcvFileError: return "rcvFileError"
|
case .rcvFileError: return "rcvFileError"
|
||||||
|
case .rcvFileWarning: return "rcvFileWarning"
|
||||||
case .sndFileStart: return "sndFileStart"
|
case .sndFileStart: return "sndFileStart"
|
||||||
case .sndFileComplete: return "sndFileComplete"
|
case .sndFileComplete: return "sndFileComplete"
|
||||||
case .sndFileCancelled: return "sndFileCancelled"
|
case .sndFileCancelled: return "sndFileCancelled"
|
||||||
@@ -796,6 +799,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case .sndStandaloneFileComplete: return "sndStandaloneFileComplete"
|
case .sndStandaloneFileComplete: return "sndStandaloneFileComplete"
|
||||||
case .sndFileCancelledXFTP: return "sndFileCancelledXFTP"
|
case .sndFileCancelledXFTP: return "sndFileCancelledXFTP"
|
||||||
case .sndFileError: return "sndFileError"
|
case .sndFileError: return "sndFileError"
|
||||||
|
case .sndFileWarning: return "sndFileWarning"
|
||||||
case .callInvitation: return "callInvitation"
|
case .callInvitation: return "callInvitation"
|
||||||
case .callOffer: return "callOffer"
|
case .callOffer: return "callOffer"
|
||||||
case .callAnswer: return "callAnswer"
|
case .callAnswer: return "callAnswer"
|
||||||
@@ -944,7 +948,8 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem))
|
case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||||
case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .rcvFileError(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
case let .rcvFileError(u, chatItem, agentError, _): return withUser(u, "agentError: \(String(describing: agentError))\nchatItem: \(String(describing: chatItem))")
|
||||||
|
case let .rcvFileWarning(u, chatItem, agentError, _): return withUser(u, "agentError: \(String(describing: agentError))\nchatItem: \(String(describing: chatItem))")
|
||||||
case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
|
case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
|
||||||
@@ -957,6 +962,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .sndStandaloneFileComplete(u, _, rcvURIs): return withUser(u, String(rcvURIs.count))
|
case let .sndStandaloneFileComplete(u, _, rcvURIs): return withUser(u, String(rcvURIs.count))
|
||||||
case let .sndFileCancelledXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
case let .sndFileCancelledXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||||
case let .sndFileError(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))")
|
case let .sndFileError(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))")
|
||||||
|
case let .sndFileWarning(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))")
|
||||||
case let .callInvitation(inv): return String(describing: inv)
|
case let .callInvitation(inv): return String(describing: inv)
|
||||||
case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))")
|
case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))")
|
||||||
case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))")
|
case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))")
|
||||||
@@ -974,7 +980,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
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)"
|
||||||
case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)"
|
case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)"
|
||||||
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
|
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
|
||||||
case .remoteCtrlStopped: return noDetails
|
case let .remoteCtrlStopped(rcsState, rcStopReason): return "rcsState: \(String(describing: rcsState))\nrcStopReason: \(String(describing: rcStopReason))"
|
||||||
case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)")
|
case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)")
|
||||||
case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))"
|
case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))"
|
||||||
case .cmdOk: return noDetails
|
case .cmdOk: return noDetails
|
||||||
@@ -1771,8 +1777,6 @@ public enum ChatErrorType: Decodable {
|
|||||||
case fileImageSize(filePath: String)
|
case fileImageSize(filePath: String)
|
||||||
case fileNotReceived(fileId: Int64)
|
case fileNotReceived(fileId: Int64)
|
||||||
case fileNotApproved(fileId: Int64, unknownServers: [String])
|
case fileNotApproved(fileId: Int64, unknownServers: [String])
|
||||||
// case xFTPRcvFile
|
|
||||||
// case xFTPSndFile
|
|
||||||
case fallbackToSMPProhibited(fileId: Int64)
|
case fallbackToSMPProhibited(fileId: Int64)
|
||||||
case inlineFileProhibited(fileId: Int64)
|
case inlineFileProhibited(fileId: Int64)
|
||||||
case invalidQuote
|
case invalidQuote
|
||||||
|
|||||||
@@ -2728,7 +2728,7 @@ public enum CIStatus: Decodable {
|
|||||||
case rcvRead
|
case rcvRead
|
||||||
case invalid(text: String)
|
case invalid(text: String)
|
||||||
|
|
||||||
var id: String {
|
public var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .sndNew: return "sndNew"
|
case .sndNew: return "sndNew"
|
||||||
case .sndSent: return "sndSent"
|
case .sndSent: return "sndSent"
|
||||||
@@ -2809,11 +2809,19 @@ public enum SndError: Decodable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SrvError: Decodable {
|
public enum SrvError: Decodable, Equatable {
|
||||||
case host
|
case host
|
||||||
case version
|
case version
|
||||||
case other(srvError: String)
|
case other(srvError: String)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .host: return "host"
|
||||||
|
case .version: return "version"
|
||||||
|
case let .other(srvError): return "other \(srvError)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public var errorInfo: String {
|
public var errorInfo: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .host: NSLocalizedString("Server address is incompatible with network settings.", comment: "srv error text.")
|
case .host: NSLocalizedString("Server address is incompatible with network settings.", comment: "srv error text.")
|
||||||
@@ -3171,6 +3179,7 @@ public struct CIFile: Decodable {
|
|||||||
case .sndComplete: return true
|
case .sndComplete: return true
|
||||||
case .sndCancelled: return true
|
case .sndCancelled: return true
|
||||||
case .sndError: return true
|
case .sndError: return true
|
||||||
|
case .sndWarning: return true
|
||||||
case .rcvInvitation: return false
|
case .rcvInvitation: return false
|
||||||
case .rcvAccepted: return false
|
case .rcvAccepted: return false
|
||||||
case .rcvTransfer: return false
|
case .rcvTransfer: return false
|
||||||
@@ -3178,6 +3187,7 @@ public struct CIFile: Decodable {
|
|||||||
case .rcvCancelled: return false
|
case .rcvCancelled: return false
|
||||||
case .rcvComplete: return true
|
case .rcvComplete: return true
|
||||||
case .rcvError: return false
|
case .rcvError: return false
|
||||||
|
case .rcvWarning: return false
|
||||||
case .invalid: return false
|
case .invalid: return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3196,12 +3206,14 @@ public struct CIFile: Decodable {
|
|||||||
}
|
}
|
||||||
case .sndCancelled: return nil
|
case .sndCancelled: return nil
|
||||||
case .sndError: return nil
|
case .sndError: return nil
|
||||||
|
case .sndWarning: return sndCancelAction
|
||||||
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 .rcvAborted: return nil
|
||||||
case .rcvCancelled: return nil
|
case .rcvCancelled: return nil
|
||||||
case .rcvComplete: return nil
|
case .rcvComplete: return nil
|
||||||
|
case .rcvWarning: return rcvCancelAction
|
||||||
case .rcvError: return nil
|
case .rcvError: return nil
|
||||||
case .invalid: return nil
|
case .invalid: return nil
|
||||||
}
|
}
|
||||||
@@ -3310,35 +3322,64 @@ public enum CIFileStatus: Decodable, Equatable {
|
|||||||
case sndTransfer(sndProgress: Int64, sndTotal: Int64)
|
case sndTransfer(sndProgress: Int64, sndTotal: Int64)
|
||||||
case sndComplete
|
case sndComplete
|
||||||
case sndCancelled
|
case sndCancelled
|
||||||
case sndError
|
case sndError(sndFileError: FileError)
|
||||||
|
case sndWarning(sndFileError: FileError)
|
||||||
case rcvInvitation
|
case rcvInvitation
|
||||||
case rcvAccepted
|
case rcvAccepted
|
||||||
case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64)
|
case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64)
|
||||||
case rcvAborted
|
case rcvAborted
|
||||||
case rcvComplete
|
case rcvComplete
|
||||||
case rcvCancelled
|
case rcvCancelled
|
||||||
case rcvError
|
case rcvError(rcvFileError: FileError)
|
||||||
|
case rcvWarning(rcvFileError: FileError)
|
||||||
case invalid(text: String)
|
case invalid(text: String)
|
||||||
|
|
||||||
var id: String {
|
public var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .sndStored: return "sndStored"
|
case .sndStored: return "sndStored"
|
||||||
case let .sndTransfer(sndProgress, sndTotal): return "sndTransfer \(sndProgress) \(sndTotal)"
|
case let .sndTransfer(sndProgress, sndTotal): return "sndTransfer \(sndProgress) \(sndTotal)"
|
||||||
case .sndComplete: return "sndComplete"
|
case .sndComplete: return "sndComplete"
|
||||||
case .sndCancelled: return "sndCancelled"
|
case .sndCancelled: return "sndCancelled"
|
||||||
case .sndError: return "sndError"
|
case let .sndError(sndFileError): return "sndError \(sndFileError)"
|
||||||
|
case let .sndWarning(sndFileError): return "sndWarning \(sndFileError)"
|
||||||
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 .rcvAborted: return "rcvAborted"
|
||||||
case .rcvComplete: return "rcvComplete"
|
case .rcvComplete: return "rcvComplete"
|
||||||
case .rcvCancelled: return "rcvCancelled"
|
case .rcvCancelled: return "rcvCancelled"
|
||||||
case .rcvError: return "rcvError"
|
case let .rcvError(rcvFileError): return "rcvError \(rcvFileError)"
|
||||||
|
case let .rcvWarning(rcvFileError): return "rcvWarning \(rcvFileError)"
|
||||||
case .invalid: return "invalid"
|
case .invalid: return "invalid"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum FileError: Decodable, Equatable {
|
||||||
|
case auth
|
||||||
|
case noFile
|
||||||
|
case relay(srvError: SrvError)
|
||||||
|
case other(fileError: String)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .auth: return "auth"
|
||||||
|
case .noFile: return "noFile"
|
||||||
|
case let .relay(srvError): return "relay \(srvError)"
|
||||||
|
case let .other(fileError): return "other \(fileError)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var errorInfo: String {
|
||||||
|
switch self {
|
||||||
|
case .auth: NSLocalizedString("Wrong key or unknown file chunk address - most likely file is deleted.", comment: "file error text")
|
||||||
|
case .noFile: NSLocalizedString("File not found - most likely file was deleted or cancelled.", comment: "file error text")
|
||||||
|
case let .relay(srvError): String.localizedStringWithFormat(NSLocalizedString("File server error: %@", comment: "file error text"), srvError.errorInfo)
|
||||||
|
case let .other(fileError): String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "file error text"), fileError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum MsgContent: Equatable {
|
public enum MsgContent: Equatable {
|
||||||
case text(String)
|
case text(String)
|
||||||
case link(text: String, preview: LinkPreview)
|
case link(text: String, preview: LinkPreview)
|
||||||
@@ -3397,6 +3438,15 @@ public enum MsgContent: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var isMediaOrFileAttachment: Bool {
|
||||||
|
switch self {
|
||||||
|
case .image: true
|
||||||
|
case .video: true
|
||||||
|
case .file: true
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var cmdString: String {
|
var cmdString: String {
|
||||||
"json \(encodeJSON(self))"
|
"json \(encodeJSON(self))"
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-2
@@ -2660,6 +2660,7 @@ data class CIFile(
|
|||||||
is CIFileStatus.SndComplete -> true
|
is CIFileStatus.SndComplete -> true
|
||||||
is CIFileStatus.SndCancelled -> true
|
is CIFileStatus.SndCancelled -> true
|
||||||
is CIFileStatus.SndError -> true
|
is CIFileStatus.SndError -> true
|
||||||
|
is CIFileStatus.SndWarning -> true
|
||||||
is CIFileStatus.RcvInvitation -> false
|
is CIFileStatus.RcvInvitation -> false
|
||||||
is CIFileStatus.RcvAccepted -> false
|
is CIFileStatus.RcvAccepted -> false
|
||||||
is CIFileStatus.RcvTransfer -> false
|
is CIFileStatus.RcvTransfer -> false
|
||||||
@@ -2667,6 +2668,7 @@ data class CIFile(
|
|||||||
is CIFileStatus.RcvCancelled -> false
|
is CIFileStatus.RcvCancelled -> false
|
||||||
is CIFileStatus.RcvComplete -> true
|
is CIFileStatus.RcvComplete -> true
|
||||||
is CIFileStatus.RcvError -> false
|
is CIFileStatus.RcvError -> false
|
||||||
|
is CIFileStatus.RcvWarning -> false
|
||||||
is CIFileStatus.Invalid -> false
|
is CIFileStatus.Invalid -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2682,6 +2684,7 @@ data class CIFile(
|
|||||||
}
|
}
|
||||||
is CIFileStatus.SndCancelled -> null
|
is CIFileStatus.SndCancelled -> null
|
||||||
is CIFileStatus.SndError -> null
|
is CIFileStatus.SndError -> null
|
||||||
|
is CIFileStatus.SndWarning -> sndCancelAction
|
||||||
is CIFileStatus.RcvInvitation -> null
|
is CIFileStatus.RcvInvitation -> null
|
||||||
is CIFileStatus.RcvAccepted -> rcvCancelAction
|
is CIFileStatus.RcvAccepted -> rcvCancelAction
|
||||||
is CIFileStatus.RcvTransfer -> rcvCancelAction
|
is CIFileStatus.RcvTransfer -> rcvCancelAction
|
||||||
@@ -2689,6 +2692,7 @@ data class CIFile(
|
|||||||
is CIFileStatus.RcvCancelled -> null
|
is CIFileStatus.RcvCancelled -> null
|
||||||
is CIFileStatus.RcvComplete -> null
|
is CIFileStatus.RcvComplete -> null
|
||||||
is CIFileStatus.RcvError -> null
|
is CIFileStatus.RcvError -> null
|
||||||
|
is CIFileStatus.RcvWarning -> rcvCancelAction
|
||||||
is CIFileStatus.Invalid -> null
|
is CIFileStatus.Invalid -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2862,14 +2866,16 @@ sealed class CIFileStatus {
|
|||||||
@Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus()
|
@Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus()
|
||||||
@Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus()
|
@Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus()
|
||||||
@Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus()
|
@Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus()
|
||||||
@Serializable @SerialName("sndError") object SndError: CIFileStatus()
|
@Serializable @SerialName("sndError") class SndError(val sndFileError: FileError): CIFileStatus()
|
||||||
|
@Serializable @SerialName("sndWarning") class SndWarning(val sndFileError: FileError): 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("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") class RcvError(val rcvFileError: FileError): CIFileStatus()
|
||||||
|
@Serializable @SerialName("rcvWarning") class RcvWarning(val rcvFileError: FileError): CIFileStatus()
|
||||||
@Serializable @SerialName("invalid") class Invalid(val text: String): CIFileStatus()
|
@Serializable @SerialName("invalid") class Invalid(val text: String): CIFileStatus()
|
||||||
|
|
||||||
val sent: Boolean get() = when (this) {
|
val sent: Boolean get() = when (this) {
|
||||||
@@ -2878,6 +2884,7 @@ sealed class CIFileStatus {
|
|||||||
is SndComplete -> true
|
is SndComplete -> true
|
||||||
is SndCancelled -> true
|
is SndCancelled -> true
|
||||||
is SndError -> true
|
is SndError -> true
|
||||||
|
is SndWarning -> true
|
||||||
is RcvInvitation -> false
|
is RcvInvitation -> false
|
||||||
is RcvAccepted -> false
|
is RcvAccepted -> false
|
||||||
is RcvTransfer -> false
|
is RcvTransfer -> false
|
||||||
@@ -2885,10 +2892,26 @@ sealed class CIFileStatus {
|
|||||||
is RcvComplete -> false
|
is RcvComplete -> false
|
||||||
is RcvCancelled -> false
|
is RcvCancelled -> false
|
||||||
is RcvError -> false
|
is RcvError -> false
|
||||||
|
is RcvWarning -> false
|
||||||
is Invalid -> false
|
is Invalid -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class FileError {
|
||||||
|
@Serializable @SerialName("auth") class Auth: FileError()
|
||||||
|
@Serializable @SerialName("noFile") class NoFile: FileError()
|
||||||
|
@Serializable @SerialName("relay") class Relay(val srvError: SrvError): FileError()
|
||||||
|
@Serializable @SerialName("other") class Other(val fileError: String): FileError()
|
||||||
|
|
||||||
|
val errorInfo: String get() = when (this) {
|
||||||
|
is FileError.Auth -> generalGetString(MR.strings.file_error_auth)
|
||||||
|
is FileError.NoFile -> generalGetString(MR.strings.file_error_no_file)
|
||||||
|
is FileError.Relay -> generalGetString(MR.strings.file_error_relay).format(srvError.errorInfo)
|
||||||
|
is FileError.Other -> generalGetString(MR.strings.ci_status_other_error).format(fileError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
|
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
|
||||||
@Serializable(with = MsgContentSerializer::class)
|
@Serializable(with = MsgContentSerializer::class)
|
||||||
sealed class MsgContent {
|
sealed class MsgContent {
|
||||||
@@ -2902,6 +2925,20 @@ sealed class MsgContent {
|
|||||||
@Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent()
|
@Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent()
|
||||||
@Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent()
|
@Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent()
|
||||||
|
|
||||||
|
val isVoice: Boolean get() =
|
||||||
|
when (this) {
|
||||||
|
is MCVoice -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
val isMediaOrFileAttachment: Boolean get() =
|
||||||
|
when (this) {
|
||||||
|
is MCImage -> true
|
||||||
|
is MCVideo -> true
|
||||||
|
is MCFile -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
val cmdString: String get() =
|
val cmdString: String get() =
|
||||||
if (this is MCUnknown) "json $json" else "json ${json.encodeToString(this)}"
|
if (this is MCUnknown) "json $json" else "json ${json.encodeToString(this)}"
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-16
@@ -1,9 +1,18 @@
|
|||||||
package chat.simplex.common.model
|
package chat.simplex.common.model
|
||||||
|
|
||||||
|
import SectionItemView
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import chat.simplex.common.model.ChatController.getNetCfg
|
import chat.simplex.common.model.ChatController.getNetCfg
|
||||||
import chat.simplex.common.model.ChatController.setNetCfg
|
import chat.simplex.common.model.ChatController.setNetCfg
|
||||||
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||||
@@ -12,7 +21,6 @@ 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.*
|
||||||
@@ -20,6 +28,7 @@ import com.charleskorn.kaml.Yaml
|
|||||||
import com.charleskorn.kaml.YamlConfiguration
|
import com.charleskorn.kaml.YamlConfiguration
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
@@ -2026,6 +2035,11 @@ object ChatController {
|
|||||||
cleanupFile(r.chatItem_)
|
cleanupFile(r.chatItem_)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is CR.RcvFileWarning -> {
|
||||||
|
if (r.chatItem_ != null) {
|
||||||
|
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||||
|
}
|
||||||
|
}
|
||||||
is CR.SndFileStart ->
|
is CR.SndFileStart ->
|
||||||
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
|
||||||
is CR.SndFileComplete -> {
|
is CR.SndFileComplete -> {
|
||||||
@@ -2052,6 +2066,11 @@ object ChatController {
|
|||||||
cleanupFile(r.chatItem_)
|
cleanupFile(r.chatItem_)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is CR.SndFileWarning -> {
|
||||||
|
if (r.chatItem_ != null) {
|
||||||
|
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
|
||||||
|
}
|
||||||
|
}
|
||||||
is CR.CallInvitation -> {
|
is CR.CallInvitation -> {
|
||||||
chatModel.callManager.reportNewIncomingCall(r.callInvitation.copy(remoteHostId = rhId))
|
chatModel.callManager.reportNewIncomingCall(r.callInvitation.copy(remoteHostId = rhId))
|
||||||
}
|
}
|
||||||
@@ -2184,15 +2203,43 @@ object ChatController {
|
|||||||
val sess = chatModel.remoteCtrlSession.value
|
val sess = chatModel.remoteCtrlSession.value
|
||||||
if (sess != null) {
|
if (sess != null) {
|
||||||
chatModel.remoteCtrlSession.value = null
|
chatModel.remoteCtrlSession.value = null
|
||||||
|
ModalManager.fullscreen.closeModals()
|
||||||
fun showAlert(chatError: ChatError) {
|
fun showAlert(chatError: ChatError) {
|
||||||
AlertManager.shared.showAlertMsg(
|
when {
|
||||||
generalGetString(MR.strings.remote_ctrl_was_disconnected_title),
|
r.rcStopReason is RemoteCtrlStopReason.ConnectionFailed
|
||||||
if (chatError is ChatError.ChatErrorRemoteCtrl) {
|
&& r.rcStopReason.chatError is ChatError.ChatErrorAgent
|
||||||
chatError.remoteCtrlError.localizedString
|
&& r.rcStopReason.chatError.agentError is AgentErrorType.RCP
|
||||||
} else {
|
&& r.rcStopReason.chatError.agentError.rcpErr is RCErrorType.IDENTITY ->
|
||||||
generalGetString(MR.strings.remote_ctrl_disconnected_with_reason).format(chatError.string)
|
AlertManager.shared.showAlertMsg(
|
||||||
}
|
title = generalGetString(MR.strings.remote_ctrl_was_disconnected_title),
|
||||||
)
|
text = generalGetString(MR.strings.remote_ctrl_connection_stopped_identity_desc)
|
||||||
|
)
|
||||||
|
else ->
|
||||||
|
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||||
|
title = generalGetString(MR.strings.remote_ctrl_was_disconnected_title),
|
||||||
|
text = if (chatError is ChatError.ChatErrorRemoteCtrl) {
|
||||||
|
chatError.remoteCtrlError.localizedString
|
||||||
|
} else {
|
||||||
|
generalGetString(MR.strings.remote_ctrl_connection_stopped_desc)
|
||||||
|
},
|
||||||
|
buttons = {
|
||||||
|
Column {
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(MR.strings.ok), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
val clipboard = LocalClipboardManager.current
|
||||||
|
SectionItemView({
|
||||||
|
clipboard.setText(AnnotatedString(json.encodeToString(r.rcStopReason)))
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(MR.strings.copy_error), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
when (r.rcStopReason) {
|
when (r.rcStopReason) {
|
||||||
is RemoteCtrlStopReason.DiscoveryFailed -> showAlert(r.rcStopReason.chatError)
|
is RemoteCtrlStopReason.DiscoveryFailed -> showAlert(r.rcStopReason.chatError)
|
||||||
@@ -4322,6 +4369,7 @@ sealed class 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 agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR()
|
@Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem_: AChatItem?, val agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||||
|
@Serializable @SerialName("rcvFileWarning") class RcvFileWarning(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()
|
||||||
@@ -4334,7 +4382,8 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR()
|
@Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR()
|
||||||
@Serializable @SerialName("sndStandaloneFileComplete") class SndStandaloneFileComplete(val user: UserRef, val fileTransferMeta: FileTransferMeta, val rcvURIs: List<String>): CR()
|
@Serializable @SerialName("sndStandaloneFileComplete") class SndStandaloneFileComplete(val user: UserRef, val fileTransferMeta: FileTransferMeta, val rcvURIs: List<String>): CR()
|
||||||
@Serializable @SerialName("sndFileCancelledXFTP") class SndFileCancelledXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR()
|
@Serializable @SerialName("sndFileCancelledXFTP") class SndFileCancelledXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR()
|
||||||
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR()
|
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR()
|
||||||
|
@Serializable @SerialName("sndFileWarning") class SndFileWarning(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR()
|
||||||
// call events
|
// call events
|
||||||
@Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR()
|
@Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR()
|
||||||
@Serializable @SerialName("callInvitations") class CallInvitations(val callInvitations: List<RcvCallInvitation>): CR()
|
@Serializable @SerialName("callInvitations") class CallInvitations(val callInvitations: List<RcvCallInvitation>): CR()
|
||||||
@@ -4493,6 +4542,7 @@ sealed class CR {
|
|||||||
is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
|
is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
|
||||||
is SndFileRedirectStartXFTP -> "sndFileRedirectStartXFTP"
|
is SndFileRedirectStartXFTP -> "sndFileRedirectStartXFTP"
|
||||||
is RcvFileError -> "rcvFileError"
|
is RcvFileError -> "rcvFileError"
|
||||||
|
is RcvFileWarning -> "rcvFileWarning"
|
||||||
is SndFileStart -> "sndFileStart"
|
is SndFileStart -> "sndFileStart"
|
||||||
is SndFileComplete -> "sndFileComplete"
|
is SndFileComplete -> "sndFileComplete"
|
||||||
is SndFileRcvCancelled -> "sndFileRcvCancelled"
|
is SndFileRcvCancelled -> "sndFileRcvCancelled"
|
||||||
@@ -4502,6 +4552,7 @@ sealed class CR {
|
|||||||
is SndStandaloneFileComplete -> "sndStandaloneFileComplete"
|
is SndStandaloneFileComplete -> "sndStandaloneFileComplete"
|
||||||
is SndFileCancelledXFTP -> "sndFileCancelledXFTP"
|
is SndFileCancelledXFTP -> "sndFileCancelledXFTP"
|
||||||
is SndFileError -> "sndFileError"
|
is SndFileError -> "sndFileError"
|
||||||
|
is SndFileWarning -> "sndFileWarning"
|
||||||
is CallInvitations -> "callInvitations"
|
is CallInvitations -> "callInvitations"
|
||||||
is CallInvitation -> "callInvitation"
|
is CallInvitation -> "callInvitation"
|
||||||
is CallOffer -> "callOffer"
|
is CallOffer -> "callOffer"
|
||||||
@@ -4652,6 +4703,7 @@ sealed class CR {
|
|||||||
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, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer")
|
is RcvFileError -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer")
|
||||||
|
is RcvFileWarning -> 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))
|
||||||
@@ -4663,7 +4715,8 @@ sealed class CR {
|
|||||||
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
|
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
|
||||||
is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString())
|
is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString())
|
||||||
is SndFileCancelledXFTP -> withUser(user, json.encodeToString(chatItem_))
|
is SndFileCancelledXFTP -> withUser(user, json.encodeToString(chatItem_))
|
||||||
is SndFileError -> withUser(user, json.encodeToString(chatItem_))
|
is SndFileError -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}")
|
||||||
|
is SndFileWarning -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}")
|
||||||
is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}"
|
is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}"
|
||||||
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}"
|
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}"
|
||||||
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
|
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
|
||||||
@@ -4700,7 +4753,7 @@ sealed class CR {
|
|||||||
(if (remoteCtrl_ == null) "null" else json.encodeToString(remoteCtrl_)) +
|
(if (remoteCtrl_ == null) "null" else json.encodeToString(remoteCtrl_)) +
|
||||||
"\nsessionCode: $sessionCode"
|
"\nsessionCode: $sessionCode"
|
||||||
is RemoteCtrlConnected -> json.encodeToString(remoteCtrl)
|
is RemoteCtrlConnected -> json.encodeToString(remoteCtrl)
|
||||||
is RemoteCtrlStopped -> noDetails()
|
is RemoteCtrlStopped -> "rcsState: $rcsState\nrcsStopReason: $rcStopReason"
|
||||||
is ContactPQAllowed -> withUser(user, "contact: ${contact.id}\npqEncryption: $pqEncryption")
|
is ContactPQAllowed -> withUser(user, "contact: ${contact.id}\npqEncryption: $pqEncryption")
|
||||||
is ContactPQEnabled -> withUser(user, "contact: ${contact.id}\npqEnabled: $pqEnabled")
|
is ContactPQEnabled -> withUser(user, "contact: ${contact.id}\npqEnabled: $pqEnabled")
|
||||||
is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" +
|
is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" +
|
||||||
@@ -4982,8 +5035,6 @@ sealed class ChatErrorType {
|
|||||||
is FileImageSize -> "fileImageSize"
|
is FileImageSize -> "fileImageSize"
|
||||||
is FileNotReceived -> "fileNotReceived"
|
is FileNotReceived -> "fileNotReceived"
|
||||||
is FileNotApproved -> "fileNotApproved"
|
is FileNotApproved -> "fileNotApproved"
|
||||||
// is XFTPRcvFile -> "xftpRcvFile"
|
|
||||||
// is XFTPSndFile -> "xftpSndFile"
|
|
||||||
is FallbackToSMPProhibited -> "fallbackToSMPProhibited"
|
is FallbackToSMPProhibited -> "fallbackToSMPProhibited"
|
||||||
is InlineFileProhibited -> "inlineFileProhibited"
|
is InlineFileProhibited -> "inlineFileProhibited"
|
||||||
is InvalidQuote -> "invalidQuote"
|
is InvalidQuote -> "invalidQuote"
|
||||||
@@ -5061,8 +5112,6 @@ sealed class 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("fileNotApproved") class FileNotApproved(val fileId: Long, val unknownServers: List<String>): ChatErrorType()
|
||||||
// @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: 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()
|
||||||
@Serializable @SerialName("inlineFileProhibited") class InlineFileProhibited(val fileId: Long): ChatErrorType()
|
@Serializable @SerialName("inlineFileProhibited") class InlineFileProhibited(val fileId: Long): ChatErrorType()
|
||||||
@Serializable @SerialName("invalidQuote") object InvalidQuote: ChatErrorType()
|
@Serializable @SerialName("invalidQuote") object InvalidQuote: ChatErrorType()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package chat.simplex.common.platform
|
package chat.simplex.common.platform
|
||||||
|
|
||||||
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.model.ChatModel.currentUser
|
import chat.simplex.common.model.ChatModel.currentUser
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
@@ -57,6 +58,9 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
|
|||||||
try {
|
try {
|
||||||
if (chatModel.ctrlInitInProgress.value) return
|
if (chatModel.ctrlInitInProgress.value) return
|
||||||
chatModel.ctrlInitInProgress.value = true
|
chatModel.ctrlInitInProgress.value = true
|
||||||
|
if (!appPrefs.storeDBPassphrase.get() && !appPrefs.initialRandomDBPassphrase.get()) {
|
||||||
|
ksDatabasePassword.remove()
|
||||||
|
}
|
||||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||||
val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
||||||
var migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value)
|
var migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value)
|
||||||
|
|||||||
+2
@@ -224,6 +224,8 @@ object ThemeManager {
|
|||||||
s.length == 1 -> "#ff$s$s$s$s$s$s"
|
s.length == 1 -> "#ff$s$s$s$s$s$s"
|
||||||
s.length == 2 -> "#ff$s$s$s"
|
s.length == 2 -> "#ff$s$s$s"
|
||||||
s.length == 3 -> "#ff$s$s"
|
s.length == 3 -> "#ff$s$s"
|
||||||
|
s.length == 4 && this.alpha == 0f -> "#0000$s" // 000088ff treated as 88ff
|
||||||
|
s.length == 4 -> "#ff00$s"
|
||||||
s.length == 6 && this.alpha == 0f -> "#00$s"
|
s.length == 6 && this.alpha == 0f -> "#00$s"
|
||||||
s.length == 6 -> "#ff$s"
|
s.length == 6 -> "#ff$s"
|
||||||
else -> "#$s"
|
else -> "#$s"
|
||||||
|
|||||||
+30
@@ -31,6 +31,7 @@ import chat.simplex.common.ui.theme.*
|
|||||||
import chat.simplex.common.views.chatlist.*
|
import chat.simplex.common.views.chatlist.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.ImageResource
|
import dev.icerock.moko.resources.ImageResource
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
|
||||||
sealed class CIInfoTab {
|
sealed class CIInfoTab {
|
||||||
class Delivery(val memberDeliveryStatuses: List<MemberDeliveryStatus>): CIInfoTab()
|
class Delivery(val memberDeliveryStatuses: List<MemberDeliveryStatus>): CIInfoTab()
|
||||||
@@ -216,6 +217,27 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ExpandableInfoRow(title: String, value: String) {
|
||||||
|
val expanded = remember { mutableStateOf(false) }
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.sizeIn(minHeight = 46.dp)
|
||||||
|
.padding(PaddingValues(horizontal = DEFAULT_PADDING))
|
||||||
|
.clickable { expanded.value = !expanded.value },
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(title, color = MaterialTheme.colors.onBackground)
|
||||||
|
if (expanded.value) {
|
||||||
|
Text(value, color = MaterialTheme.colors.secondary)
|
||||||
|
} else {
|
||||||
|
Text(value, color = MaterialTheme.colors.secondary, maxLines = 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Details() {
|
fun Details() {
|
||||||
AppBarTitle(stringResource(if (ci.localNote) MR.strings.saved_message_title else if (sent) MR.strings.sent_message else MR.strings.received_message))
|
AppBarTitle(stringResource(if (ci.localNote) MR.strings.saved_message_title else if (sent) MR.strings.sent_message else MR.strings.received_message))
|
||||||
@@ -244,6 +266,10 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
|
|||||||
if (devTools) {
|
if (devTools) {
|
||||||
InfoRow(stringResource(MR.strings.info_row_database_id), ci.meta.itemId.toString())
|
InfoRow(stringResource(MR.strings.info_row_database_id), ci.meta.itemId.toString())
|
||||||
InfoRow(stringResource(MR.strings.info_row_updated_at), localTimestamp(ci.meta.updatedAt))
|
InfoRow(stringResource(MR.strings.info_row_updated_at), localTimestamp(ci.meta.updatedAt))
|
||||||
|
ExpandableInfoRow(stringResource(MR.strings.info_row_message_status), jsonShort.encodeToString(ci.meta.itemStatus))
|
||||||
|
if (ci.file != null) {
|
||||||
|
ExpandableInfoRow(stringResource(MR.strings.info_row_file_status), jsonShort.encodeToString(ci.file.fileStatus))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,6 +557,10 @@ fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItem
|
|||||||
if (devTools) {
|
if (devTools) {
|
||||||
shareText.add(String.format(generalGetString(MR.strings.share_text_database_id), meta.itemId))
|
shareText.add(String.format(generalGetString(MR.strings.share_text_database_id), meta.itemId))
|
||||||
shareText.add(String.format(generalGetString(MR.strings.share_text_updated_at), meta.updatedAt))
|
shareText.add(String.format(generalGetString(MR.strings.share_text_updated_at), meta.updatedAt))
|
||||||
|
shareText.add(String.format(generalGetString(MR.strings.share_text_message_status), jsonShort.encodeToString(ci.meta.itemStatus)))
|
||||||
|
if (ci.file != null) {
|
||||||
|
shareText.add(String.format(generalGetString(MR.strings.share_text_file_status), jsonShort.encodeToString(ci.file.fileStatus)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val qi = ci.quotedItem
|
val qi = ci.quotedItem
|
||||||
if (qi != null) {
|
if (qi != null) {
|
||||||
|
|||||||
+8
-11
@@ -962,17 +962,8 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
||||||
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
|
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
|
||||||
) {
|
) {
|
||||||
val dismissState = rememberDismissState(initialValue = DismissValue.Default) { false }
|
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
|
||||||
val directions = setOf(DismissDirection.EndToStart)
|
if (it == DismissValue.DismissedToStart) {
|
||||||
val swipeableModifier = SwipeToDismissModifier(
|
|
||||||
state = dismissState,
|
|
||||||
directions = directions,
|
|
||||||
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
|
|
||||||
)
|
|
||||||
val swipedToEnd = (dismissState.overflow.value > 0f && directions.contains(DismissDirection.StartToEnd))
|
|
||||||
val swipedToStart = (dismissState.overflow.value < 0f && directions.contains(DismissDirection.EndToStart))
|
|
||||||
if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) {
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chat.chatInfo !is ChatInfo.Local) {
|
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chat.chatInfo !is ChatInfo.Local) {
|
||||||
if (composeState.value.editing) {
|
if (composeState.value.editing) {
|
||||||
@@ -983,7 +974,13 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
val swipeableModifier = SwipeToDismissModifier(
|
||||||
|
state = dismissState,
|
||||||
|
directions = setOf(DismissDirection.EndToStart),
|
||||||
|
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
|
||||||
|
)
|
||||||
val provider = {
|
val provider = {
|
||||||
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|||||||
+1
-1
@@ -203,7 +203,7 @@ fun GroupChatInfoLayout(
|
|||||||
scope.launch { listState.scrollToItem(0) }
|
scope.launch { listState.scrollToItem(0) }
|
||||||
}
|
}
|
||||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
|
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
|
||||||
val filteredMembers = remember(members) { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } }
|
val filteredMembers = remember(members) { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim().lowercase()) } } }
|
||||||
// LALAL strange scrolling
|
// LALAL strange scrolling
|
||||||
LazyColumnWithScrollBar(
|
LazyColumnWithScrollBar(
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
+22
@@ -87,6 +87,26 @@ fun CIFileView(
|
|||||||
)
|
)
|
||||||
FileProtocol.LOCAL -> {}
|
FileProtocol.LOCAL -> {}
|
||||||
}
|
}
|
||||||
|
file.fileStatus is CIFileStatus.RcvError ->
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.file_error),
|
||||||
|
file.fileStatus.rcvFileError.errorInfo
|
||||||
|
)
|
||||||
|
file.fileStatus is CIFileStatus.RcvWarning ->
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.temporary_file_error),
|
||||||
|
file.fileStatus.rcvFileError.errorInfo
|
||||||
|
)
|
||||||
|
file.fileStatus is CIFileStatus.SndError ->
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.file_error),
|
||||||
|
file.fileStatus.sndFileError.errorInfo
|
||||||
|
)
|
||||||
|
file.fileStatus is CIFileStatus.SndWarning ->
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.temporary_file_error),
|
||||||
|
file.fileStatus.sndFileError.errorInfo
|
||||||
|
)
|
||||||
file.forwardingAllowed() -> {
|
file.forwardingAllowed() -> {
|
||||||
withLongRunningApi(slow = 600_000) {
|
withLongRunningApi(slow = 600_000) {
|
||||||
var filePath = getLoadedFilePath(file)
|
var filePath = getLoadedFilePath(file)
|
||||||
@@ -157,6 +177,7 @@ fun CIFileView(
|
|||||||
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled))
|
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled))
|
||||||
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
is CIFileStatus.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.SndWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled))
|
||||||
is CIFileStatus.RcvInvitation ->
|
is CIFileStatus.RcvInvitation ->
|
||||||
if (fileSizeValid(file))
|
if (fileSizeValid(file))
|
||||||
fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary)
|
fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary)
|
||||||
@@ -174,6 +195,7 @@ fun CIFileView(
|
|||||||
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))
|
||||||
|
is CIFileStatus.RcvWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled))
|
||||||
is CIFileStatus.Invalid -> fileIcon(innerIcon = painterResource(MR.images.ic_question_mark))
|
is CIFileStatus.Invalid -> fileIcon(innerIcon = painterResource(MR.images.ic_question_mark))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+29
-7
@@ -70,14 +70,16 @@ fun CIImageView(
|
|||||||
is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_image_snd_complete)
|
is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_image_snd_complete)
|
||||||
is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||||
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||||
|
is CIFileStatus.SndWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file)
|
||||||
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.RcvComplete -> {}
|
||||||
is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file)
|
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.RcvWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), 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)
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,8 +203,8 @@ fun CIImageView(
|
|||||||
} else {
|
} else {
|
||||||
imageView(base64ToBitmap(image), onClick = {
|
imageView(base64ToBitmap(image), onClick = {
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
when (file.fileStatus) {
|
when {
|
||||||
CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
|
file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted ->
|
||||||
if (fileSizeValid()) {
|
if (fileSizeValid()) {
|
||||||
receiveFile(file.fileId)
|
receiveFile(file.fileId)
|
||||||
} else {
|
} else {
|
||||||
@@ -211,7 +213,7 @@ fun CIImageView(
|
|||||||
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
|
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CIFileStatus.RcvAccepted ->
|
file.fileStatus is CIFileStatus.RcvAccepted ->
|
||||||
when (file.fileProtocol) {
|
when (file.fileProtocol) {
|
||||||
FileProtocol.XFTP ->
|
FileProtocol.XFTP ->
|
||||||
AlertManager.shared.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
@@ -225,9 +227,29 @@ fun CIImageView(
|
|||||||
)
|
)
|
||||||
FileProtocol.LOCAL -> {}
|
FileProtocol.LOCAL -> {}
|
||||||
}
|
}
|
||||||
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
|
file.fileStatus is CIFileStatus.RcvError ->
|
||||||
CIFileStatus.RcvComplete -> {} // ?
|
AlertManager.shared.showAlertMsg(
|
||||||
CIFileStatus.RcvCancelled -> {} // TODO
|
generalGetString(MR.strings.file_error),
|
||||||
|
file.fileStatus.rcvFileError.errorInfo
|
||||||
|
)
|
||||||
|
file.fileStatus is CIFileStatus.RcvWarning ->
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.temporary_file_error),
|
||||||
|
file.fileStatus.rcvFileError.errorInfo
|
||||||
|
)
|
||||||
|
file.fileStatus is CIFileStatus.SndError ->
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.file_error),
|
||||||
|
file.fileStatus.sndFileError.errorInfo
|
||||||
|
)
|
||||||
|
file.fileStatus is CIFileStatus.SndWarning ->
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.temporary_file_error),
|
||||||
|
file.fileStatus.sndFileError.errorInfo
|
||||||
|
)
|
||||||
|
file.fileStatus is CIFileStatus.RcvTransfer -> {} // ?
|
||||||
|
file.fileStatus is CIFileStatus.RcvComplete -> {} // ?
|
||||||
|
file.fileStatus is CIFileStatus.RcvCancelled -> {} // TODO
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-7
@@ -107,7 +107,7 @@ fun CIVideoView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadingIndicator(file)
|
fileStatusIcon(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,11 +339,13 @@ private fun progressIndicator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun fileIcon(icon: Painter, stringId: StringResource) {
|
private fun fileIcon(icon: Painter, stringId: StringResource, onClick: (() -> Unit)? = null) {
|
||||||
|
var modifier = Modifier.fillMaxSize()
|
||||||
|
modifier = if (onClick != null) { modifier.clickable { onClick() } } else { modifier }
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
stringResource(stringId),
|
stringResource(stringId),
|
||||||
Modifier.fillMaxSize(),
|
modifier,
|
||||||
tint = Color.White
|
tint = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -364,7 +366,7 @@ private fun progressCircle(progress: Long, total: Long) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun loadingIndicator(file: CIFile?) {
|
private fun fileStatusIcon(file: CIFile?) {
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
@@ -387,7 +389,28 @@ private fun loadingIndicator(file: CIFile?) {
|
|||||||
}
|
}
|
||||||
is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_video_snd_complete)
|
is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_video_snd_complete)
|
||||||
is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||||
is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
is CIFileStatus.SndError ->
|
||||||
|
fileIcon(
|
||||||
|
painterResource(MR.images.ic_close),
|
||||||
|
MR.strings.icon_descr_file,
|
||||||
|
onClick = {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.file_error),
|
||||||
|
file.fileStatus.sndFileError.errorInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is CIFileStatus.SndWarning ->
|
||||||
|
fileIcon(
|
||||||
|
painterResource(MR.images.ic_warning_filled),
|
||||||
|
MR.strings.icon_descr_file,
|
||||||
|
onClick = {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.temporary_file_error),
|
||||||
|
file.fileStatus.sndFileError.errorInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive)
|
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive)
|
||||||
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_video)
|
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_video)
|
||||||
is CIFileStatus.RcvTransfer ->
|
is CIFileStatus.RcvTransfer ->
|
||||||
@@ -397,10 +420,31 @@ private fun loadingIndicator(file: CIFile?) {
|
|||||||
progressIndicator()
|
progressIndicator()
|
||||||
}
|
}
|
||||||
is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file)
|
is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file)
|
||||||
|
is CIFileStatus.RcvComplete -> {}
|
||||||
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,
|
||||||
|
onClick = {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.file_error),
|
||||||
|
file.fileStatus.rcvFileError.errorInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is CIFileStatus.RcvWarning ->
|
||||||
|
fileIcon(
|
||||||
|
painterResource(MR.images.ic_warning_filled),
|
||||||
|
MR.strings.icon_descr_file,
|
||||||
|
onClick = {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.temporary_file_error),
|
||||||
|
file.fileStatus.rcvFileError.errorInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
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)
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+138
-29
@@ -252,6 +252,81 @@ private fun PlayPauseButton(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PlayablePlayPauseButton(
|
||||||
|
audioPlaying: Boolean,
|
||||||
|
sent: Boolean,
|
||||||
|
hasText: Boolean,
|
||||||
|
progress: State<Int>,
|
||||||
|
duration: State<Int>,
|
||||||
|
strokeWidth: Float,
|
||||||
|
strokeColor: Color,
|
||||||
|
error: Boolean,
|
||||||
|
play: () -> Unit,
|
||||||
|
pause: () -> Unit,
|
||||||
|
longClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val angle = 360f * (progress.value.toDouble() / duration.value).toFloat()
|
||||||
|
if (hasText) {
|
||||||
|
IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) {
|
||||||
|
Icon(
|
||||||
|
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled),
|
||||||
|
contentDescription = null,
|
||||||
|
Modifier.size(36.dp),
|
||||||
|
tint = MaterialTheme.colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VoiceMsgLoadingProgressIndicator() {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.size(56.dp)
|
||||||
|
.clip(RoundedCornerShape(4.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
ProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FileStatusIcon(
|
||||||
|
sent: Boolean,
|
||||||
|
icon: ImageResource,
|
||||||
|
longClick: () -> Unit,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val sentColor = MaterialTheme.appColors.sentMessage
|
||||||
|
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||||
|
Surface(
|
||||||
|
color = if (sent) sentColor else receivedColor,
|
||||||
|
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
|
||||||
|
contentColor = LocalContentColor.current
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.defaultMinSize(minWidth = 56.dp, minHeight = 56.dp)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = longClick
|
||||||
|
)
|
||||||
|
.onRightClick { longClick() },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(icon),
|
||||||
|
contentDescription = null,
|
||||||
|
Modifier.size(36.dp),
|
||||||
|
tint = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun VoiceMsgIndicator(
|
private fun VoiceMsgIndicator(
|
||||||
file: CIFile?,
|
file: CIFile?,
|
||||||
@@ -268,39 +343,73 @@ private fun VoiceMsgIndicator(
|
|||||||
) {
|
) {
|
||||||
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
|
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
|
||||||
val strokeColor = MaterialTheme.colors.primary
|
val strokeColor = MaterialTheme.colors.primary
|
||||||
if (file != null && file.loaded && progress != null && duration != null) {
|
when {
|
||||||
val angle = 360f * (progress.value.toDouble() / duration.value).toFloat()
|
file?.fileStatus is CIFileStatus.SndStored ->
|
||||||
if (hasText) {
|
if (file.fileProtocol == FileProtocol.LOCAL && progress != null && duration != null) {
|
||||||
IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) {
|
PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, play, pause, longClick = longClick)
|
||||||
Icon(
|
} else {
|
||||||
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled),
|
VoiceMsgLoadingProgressIndicator()
|
||||||
contentDescription = null,
|
|
||||||
Modifier.size(36.dp),
|
|
||||||
tint = MaterialTheme.colors.primary
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
file?.fileStatus is CIFileStatus.SndTransfer ->
|
||||||
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick)
|
VoiceMsgLoadingProgressIndicator()
|
||||||
}
|
file != null && file.fileStatus is CIFileStatus.SndError ->
|
||||||
} else {
|
FileStatusIcon(
|
||||||
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
|
sent,
|
||||||
|
MR.images.ic_close,
|
||||||
|
longClick,
|
||||||
|
onClick = {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.file_error),
|
||||||
|
file.fileStatus.sndFileError.errorInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
file != null && file.fileStatus is CIFileStatus.SndWarning ->
|
||||||
|
FileStatusIcon(
|
||||||
|
sent,
|
||||||
|
MR.images.ic_warning_filled,
|
||||||
|
longClick,
|
||||||
|
onClick = {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.temporary_file_error),
|
||||||
|
file.fileStatus.sndFileError.errorInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
file?.fileStatus is CIFileStatus.RcvInvitation ->
|
||||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick)
|
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick)
|
||||||
} else if (file?.fileStatus is CIFileStatus.RcvTransfer
|
file?.fileStatus is CIFileStatus.RcvTransfer || file?.fileStatus is CIFileStatus.RcvAccepted ->
|
||||||
|| file?.fileStatus is CIFileStatus.RcvAccepted
|
VoiceMsgLoadingProgressIndicator()
|
||||||
) {
|
file?.fileStatus is CIFileStatus.RcvAborted ->
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.size(56.dp)
|
|
||||||
.clip(RoundedCornerShape(4.dp)),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
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)
|
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem)
|
||||||
} else {
|
file != null && file.fileStatus is CIFileStatus.RcvError ->
|
||||||
|
FileStatusIcon(
|
||||||
|
sent,
|
||||||
|
MR.images.ic_close,
|
||||||
|
longClick,
|
||||||
|
onClick = {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.file_error),
|
||||||
|
file.fileStatus.rcvFileError.errorInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
file != null && file.fileStatus is CIFileStatus.RcvWarning ->
|
||||||
|
FileStatusIcon(
|
||||||
|
sent,
|
||||||
|
MR.images.ic_warning_filled,
|
||||||
|
longClick,
|
||||||
|
onClick = {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
generalGetString(MR.strings.temporary_file_error),
|
||||||
|
file.fileStatus.rcvFileError.errorInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
file != null && file.loaded && progress != null && duration != null ->
|
||||||
|
PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, play, pause, longClick = longClick)
|
||||||
|
else ->
|
||||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick)
|
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-7
@@ -189,13 +189,6 @@ fun FramedItemView(
|
|||||||
val receivedColor = MaterialTheme.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
|
||||||
|
|||||||
+43
-11
@@ -9,30 +9,55 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.views.helpers.ProfileImage
|
|
||||||
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.res.MR
|
import chat.simplex.res.MR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
fun ShareListNavLinkView(
|
||||||
|
chat: Chat,
|
||||||
|
chatModel: ChatModel,
|
||||||
|
isMediaOrFileAttachment: Boolean,
|
||||||
|
isVoice: Boolean,
|
||||||
|
hasSimplexLink: Boolean
|
||||||
|
) {
|
||||||
val stopped = chatModel.chatRunning.value == false
|
val stopped = chatModel.chatRunning.value == false
|
||||||
when (chat.chatInfo) {
|
when (chat.chatInfo) {
|
||||||
is ChatInfo.Direct ->
|
is ChatInfo.Direct -> {
|
||||||
|
val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
|
||||||
ShareListNavLinkLayout(
|
ShareListNavLinkLayout(
|
||||||
chatLinkPreview = { SharePreviewView(chat) },
|
chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited) },
|
||||||
click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) },
|
click = {
|
||||||
|
if (voiceProhibited) {
|
||||||
|
showForwardProhibitedByPrefAlert()
|
||||||
|
} else {
|
||||||
|
directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel)
|
||||||
|
}
|
||||||
|
},
|
||||||
stopped
|
stopped
|
||||||
)
|
)
|
||||||
is ChatInfo.Group ->
|
}
|
||||||
|
is ChatInfo.Group -> {
|
||||||
|
val simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)
|
||||||
|
val fileProhibited = isMediaOrFileAttachment && !chat.groupFeatureEnabled(GroupFeature.Files)
|
||||||
|
val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
|
||||||
|
val prohibitedByPref = simplexLinkProhibited || fileProhibited || voiceProhibited
|
||||||
ShareListNavLinkLayout(
|
ShareListNavLinkLayout(
|
||||||
chatLinkPreview = { SharePreviewView(chat) },
|
chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref) },
|
||||||
click = { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) },
|
click = {
|
||||||
|
if (prohibitedByPref) {
|
||||||
|
showForwardProhibitedByPrefAlert()
|
||||||
|
} else {
|
||||||
|
groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel)
|
||||||
|
}
|
||||||
|
},
|
||||||
stopped
|
stopped
|
||||||
)
|
)
|
||||||
|
}
|
||||||
is ChatInfo.Local ->
|
is ChatInfo.Local ->
|
||||||
ShareListNavLinkLayout(
|
ShareListNavLinkLayout(
|
||||||
chatLinkPreview = { SharePreviewView(chat) },
|
chatLinkPreview = { SharePreviewView(chat, disabled = false) },
|
||||||
click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) },
|
click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) },
|
||||||
stopped
|
stopped
|
||||||
)
|
)
|
||||||
@@ -40,6 +65,13 @@ fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showForwardProhibitedByPrefAlert() {
|
||||||
|
AlertManager.shared.showAlertMsg(
|
||||||
|
title = generalGetString(MR.strings.cannot_share_message_alert_title),
|
||||||
|
text = generalGetString(MR.strings.cannot_share_message_alert_text),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ShareListNavLinkLayout(
|
private fun ShareListNavLinkLayout(
|
||||||
chatLinkPreview: @Composable () -> Unit,
|
chatLinkPreview: @Composable () -> Unit,
|
||||||
@@ -53,7 +85,7 @@ private fun ShareListNavLinkLayout(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SharePreviewView(chat: Chat) {
|
private fun SharePreviewView(chat: Chat, disabled: Boolean) {
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxSize(),
|
Modifier.fillMaxSize(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
@@ -70,7 +102,7 @@ private fun SharePreviewView(chat: Chat) {
|
|||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||||
color = if (chat.chatInfo.incognito) Indigo else Color.Unspecified
|
color = if (disabled) MaterialTheme.colors.secondary else if (chat.chatInfo.incognito) Indigo else Color.Unspecified
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-3
@@ -31,13 +31,44 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
|||||||
scaffoldState = scaffoldState,
|
scaffoldState = scaffoldState,
|
||||||
topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
|
topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||||
) {
|
) {
|
||||||
|
val sharedContent = chatModel.sharedContent.value
|
||||||
|
var isMediaOrFileAttachment = false
|
||||||
|
var isVoice = false
|
||||||
|
var hasSimplexLink = false
|
||||||
|
when (sharedContent) {
|
||||||
|
is SharedContent.Text ->
|
||||||
|
hasSimplexLink = hasSimplexLink(sharedContent.text)
|
||||||
|
is SharedContent.Media -> {
|
||||||
|
isMediaOrFileAttachment = true
|
||||||
|
hasSimplexLink = hasSimplexLink(sharedContent.text)
|
||||||
|
}
|
||||||
|
is SharedContent.File -> {
|
||||||
|
isMediaOrFileAttachment = true
|
||||||
|
hasSimplexLink = hasSimplexLink(sharedContent.text)
|
||||||
|
}
|
||||||
|
is SharedContent.Forward -> {
|
||||||
|
val mc = sharedContent.chatItem.content.msgContent
|
||||||
|
if (mc != null) {
|
||||||
|
isMediaOrFileAttachment = mc.isMediaOrFileAttachment
|
||||||
|
isVoice = mc.isVoice
|
||||||
|
hasSimplexLink = hasSimplexLink(mc.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
Box(Modifier.padding(it)) {
|
Box(Modifier.padding(it)) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
if (chatModel.chats.isNotEmpty()) {
|
if (chatModel.chats.isNotEmpty()) {
|
||||||
ShareList(chatModel, search = searchInList)
|
ShareList(
|
||||||
|
chatModel,
|
||||||
|
search = searchInList,
|
||||||
|
isMediaOrFileAttachment = isMediaOrFileAttachment,
|
||||||
|
isVoice = isVoice,
|
||||||
|
hasSimplexLink = hasSimplexLink
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
EmptyList()
|
EmptyList()
|
||||||
}
|
}
|
||||||
@@ -54,6 +85,11 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hasSimplexLink(msg: String): Boolean {
|
||||||
|
val parsedMsg = parseToMarkdown(msg) ?: return false
|
||||||
|
return parsedMsg.any { ft -> ft.format is Format.SimplexLink }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EmptyList() {
|
private fun EmptyList() {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
@@ -141,7 +177,13 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ShareList(chatModel: ChatModel, search: String) {
|
private fun ShareList(
|
||||||
|
chatModel: ChatModel,
|
||||||
|
search: String,
|
||||||
|
isMediaOrFileAttachment: Boolean,
|
||||||
|
isVoice: Boolean,
|
||||||
|
hasSimplexLink: Boolean
|
||||||
|
) {
|
||||||
val chats by remember(search) {
|
val chats by remember(search) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val sorted = chatModel.chats.toList().sortedByDescending { it.chatInfo is ChatInfo.Local }
|
val sorted = chatModel.chats.toList().sortedByDescending { it.chatInfo is ChatInfo.Local }
|
||||||
@@ -156,7 +198,13 @@ private fun ShareList(chatModel: ChatModel, search: String) {
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
items(chats) { chat ->
|
items(chats) { chat ->
|
||||||
ShareListNavLinkView(chat, chatModel)
|
ShareListNavLinkView(
|
||||||
|
chat,
|
||||||
|
chatModel,
|
||||||
|
isMediaOrFileAttachment = isMediaOrFileAttachment,
|
||||||
|
isVoice = isVoice,
|
||||||
|
hasSimplexLink = hasSimplexLink
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-19
@@ -27,6 +27,7 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
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.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.*
|
||||||
@@ -40,9 +41,8 @@ import kotlin.math.log2
|
|||||||
@Composable
|
@Composable
|
||||||
fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
|
fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
|
||||||
val progressIndicator = remember { mutableStateOf(false) }
|
val progressIndicator = remember { mutableStateOf(false) }
|
||||||
val prefs = m.controller.appPrefs
|
val useKeychain = remember { mutableStateOf(appPrefs.storeDBPassphrase.get()) }
|
||||||
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
val initialRandomDBPassphrase = remember { mutableStateOf(appPrefs.initialRandomDBPassphrase.get()) }
|
||||||
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
|
|
||||||
val storedKey = remember { val key = DatabaseUtils.ksDatabasePassword.get(); mutableStateOf(key != null && key != "") }
|
val storedKey = remember { val key = DatabaseUtils.ksDatabasePassword.get(); mutableStateOf(key != null && key != "") }
|
||||||
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
|
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
|
||||||
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
|
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
|
||||||
@@ -54,7 +54,6 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
|
|||||||
) {
|
) {
|
||||||
DatabaseEncryptionLayout(
|
DatabaseEncryptionLayout(
|
||||||
useKeychain,
|
useKeychain,
|
||||||
prefs,
|
|
||||||
m.chatDbEncrypted.value,
|
m.chatDbEncrypted.value,
|
||||||
currentKey,
|
currentKey,
|
||||||
newKey,
|
newKey,
|
||||||
@@ -65,7 +64,16 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
|
|||||||
migration,
|
migration,
|
||||||
onConfirmEncrypt = {
|
onConfirmEncrypt = {
|
||||||
withLongRunningApi {
|
withLongRunningApi {
|
||||||
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator, migration)
|
encryptDatabase(
|
||||||
|
currentKey = currentKey,
|
||||||
|
newKey = newKey,
|
||||||
|
confirmNewKey = confirmNewKey,
|
||||||
|
initialRandomDBPassphrase = initialRandomDBPassphrase,
|
||||||
|
useKeychain = useKeychain,
|
||||||
|
storedKey = storedKey,
|
||||||
|
progressIndicator = progressIndicator,
|
||||||
|
migration = migration
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -89,7 +97,6 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun DatabaseEncryptionLayout(
|
fun DatabaseEncryptionLayout(
|
||||||
useKeychain: MutableState<Boolean>,
|
useKeychain: MutableState<Boolean>,
|
||||||
prefs: AppPreferences,
|
|
||||||
chatDbEncrypted: Boolean?,
|
chatDbEncrypted: Boolean?,
|
||||||
currentKey: MutableState<String>,
|
currentKey: MutableState<String>,
|
||||||
newKey: MutableState<String>,
|
newKey: MutableState<String>,
|
||||||
@@ -119,14 +126,14 @@ fun DatabaseEncryptionLayout(
|
|||||||
enabled = (!initialRandomDBPassphrase.value && !progressIndicator.value) || migration
|
enabled = (!initialRandomDBPassphrase.value && !progressIndicator.value) || migration
|
||||||
) { checked ->
|
) { checked ->
|
||||||
if (checked) {
|
if (checked) {
|
||||||
setUseKeychain(true, useKeychain, prefs, migration)
|
setUseKeychain(true, useKeychain, migration)
|
||||||
} else if (storedKey.value && !migration) {
|
} else if (storedKey.value && !migration) {
|
||||||
// Don't show in migration process since it will remove the key after successful encryption
|
// Don't show in migration process since it will remove the key after successful encryption
|
||||||
removePassphraseAlert {
|
removePassphraseAlert {
|
||||||
removePassphraseFromKeyChain(useKeychain, prefs, storedKey, false)
|
removePassphraseFromKeyChain(useKeychain, storedKey, false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setUseKeychain(false, useKeychain, prefs, migration)
|
setUseKeychain(false, useKeychain, migration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,17 +279,17 @@ fun resetFormAfterEncryption(
|
|||||||
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
|
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences, migration: Boolean) {
|
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, migration: Boolean) {
|
||||||
useKeychain.value = value
|
useKeychain.value = value
|
||||||
// Postpone it when migrating to the end of encryption process
|
// Postpone it when migrating to the end of encryption process
|
||||||
if (!migration) {
|
if (!migration) {
|
||||||
prefs.storeDBPassphrase.set(value)
|
appPrefs.storeDBPassphrase.set(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removePassphraseFromKeyChain(useKeychain: MutableState<Boolean>, prefs: AppPreferences, storedKey: MutableState<Boolean>, migration: Boolean) {
|
private fun removePassphraseFromKeyChain(useKeychain: MutableState<Boolean>, storedKey: MutableState<Boolean>, migration: Boolean) {
|
||||||
DatabaseUtils.ksDatabasePassword.remove()
|
DatabaseUtils.ksDatabasePassword.remove()
|
||||||
setUseKeychain(false, useKeychain, prefs, migration)
|
setUseKeychain(false, useKeychain, migration)
|
||||||
storedKey.value = false
|
storedKey.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,15 +422,14 @@ suspend fun encryptDatabase(
|
|||||||
migration: Boolean,
|
migration: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val m = ChatModel
|
val m = ChatModel
|
||||||
val prefs = ChatController.appPrefs
|
|
||||||
progressIndicator.value = true
|
progressIndicator.value = true
|
||||||
return try {
|
return try {
|
||||||
prefs.encryptionStartedAt.set(Clock.System.now())
|
appPrefs.encryptionStartedAt.set(Clock.System.now())
|
||||||
if (!m.chatDbChanged.value) {
|
if (!m.chatDbChanged.value) {
|
||||||
m.controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
|
m.controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
|
||||||
}
|
}
|
||||||
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
|
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
|
||||||
prefs.encryptionStartedAt.set(null)
|
appPrefs.encryptionStartedAt.set(null)
|
||||||
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
|
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
|
||||||
when {
|
when {
|
||||||
sqliteError is SQLiteError.ErrorNotADatabase -> {
|
sqliteError is SQLiteError.ErrorNotADatabase -> {
|
||||||
@@ -451,8 +457,8 @@ suspend fun encryptDatabase(
|
|||||||
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
||||||
if (useKeychain.value) {
|
if (useKeychain.value) {
|
||||||
DatabaseUtils.ksDatabasePassword.set(new)
|
DatabaseUtils.ksDatabasePassword.set(new)
|
||||||
} else if (migration) {
|
} else {
|
||||||
removePassphraseFromKeyChain(useKeychain, prefs, storedKey, true)
|
removePassphraseFromKeyChain(useKeychain, storedKey, migration)
|
||||||
}
|
}
|
||||||
operationEnded(m, progressIndicator) {
|
operationEnded(m, progressIndicator) {
|
||||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
|
||||||
@@ -523,7 +529,6 @@ fun PreviewDatabaseEncryptionLayout() {
|
|||||||
SimpleXTheme {
|
SimpleXTheme {
|
||||||
DatabaseEncryptionLayout(
|
DatabaseEncryptionLayout(
|
||||||
useKeychain = remember { mutableStateOf(true) },
|
useKeychain = remember { mutableStateOf(true) },
|
||||||
prefs = AppPreferences(),
|
|
||||||
chatDbEncrypted = true,
|
chatDbEncrypted = true,
|
||||||
currentKey = remember { mutableStateOf("") },
|
currentKey = remember { mutableStateOf("") },
|
||||||
newKey = remember { mutableStateOf("") },
|
newKey = remember { mutableStateOf("") },
|
||||||
|
|||||||
+3
@@ -448,6 +448,9 @@ private fun stopChat(m: ChatModel, progressIndicator: MutableState<Boolean>? = n
|
|||||||
progressIndicator?.value = true
|
progressIndicator?.value = true
|
||||||
stopChatAsync(m)
|
stopChatAsync(m)
|
||||||
platform.androidChatStopped()
|
platform.androidChatStopped()
|
||||||
|
// close chat view for desktop
|
||||||
|
chatModel.chatId.value = null
|
||||||
|
ModalManager.end.closeModals()
|
||||||
onStop?.invoke()
|
onStop?.invoke()
|
||||||
} catch (e: Error) {
|
} catch (e: Error) {
|
||||||
m.chatRunning.value = true
|
m.chatRunning.value = true
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ fun SwipeToDismissModifier(
|
|||||||
return Modifier.swipeable(
|
return Modifier.swipeable(
|
||||||
state = state,
|
state = state,
|
||||||
anchors = anchors,
|
anchors = anchors,
|
||||||
thresholds = { _, _ -> FractionalThreshold(0.5f) },
|
thresholds = { _, _ -> FractionalThreshold(0.99f) },
|
||||||
orientation = Orientation.Horizontal,
|
orientation = Orientation.Horizontal,
|
||||||
reverseDirection = isRtl,
|
reverseDirection = isRtl,
|
||||||
).offset { IntOffset(state.offset.value.roundToInt(), 0) }
|
).offset { IntOffset(state.offset.value.roundToInt(), 0) }
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
|
|||||||
withBGApi {
|
withBGApi {
|
||||||
val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile)
|
val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile)
|
||||||
if (groupInfo != null) {
|
if (groupInfo != null) {
|
||||||
chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf()))
|
chatModel.updateGroup(rhId = rhId, groupInfo)
|
||||||
chatModel.chatItems.clear()
|
chatModel.chatItems.clear()
|
||||||
chatModel.chatItemStatuses.clear()
|
chatModel.chatItemStatuses.clear()
|
||||||
chatModel.chatId.value = groupInfo.id
|
chatModel.chatId.value = groupInfo.id
|
||||||
|
|||||||
+10
-2
@@ -31,7 +31,6 @@ import kotlinx.coroutines.delay
|
|||||||
fun SetupDatabasePassphrase(m: ChatModel) {
|
fun SetupDatabasePassphrase(m: ChatModel) {
|
||||||
val progressIndicator = remember { mutableStateOf(false) }
|
val progressIndicator = remember { mutableStateOf(false) }
|
||||||
val prefs = m.controller.appPrefs
|
val prefs = m.controller.appPrefs
|
||||||
val saveInPreferences = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
|
||||||
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
|
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
|
||||||
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
|
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
|
||||||
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
|
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
|
||||||
@@ -58,7 +57,16 @@ fun SetupDatabasePassphrase(m: ChatModel) {
|
|||||||
prefs.storeDBPassphrase.set(false)
|
prefs.storeDBPassphrase.set(false)
|
||||||
|
|
||||||
val newKeyValue = newKey.value
|
val newKeyValue = newKey.value
|
||||||
val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator, false)
|
val success = encryptDatabase(
|
||||||
|
currentKey = currentKey,
|
||||||
|
newKey = newKey,
|
||||||
|
confirmNewKey = confirmNewKey,
|
||||||
|
initialRandomDBPassphrase = mutableStateOf(true),
|
||||||
|
useKeychain = mutableStateOf(false),
|
||||||
|
storedKey = mutableStateOf(true),
|
||||||
|
progressIndicator = progressIndicator,
|
||||||
|
migration = false
|
||||||
|
)
|
||||||
if (success) {
|
if (success) {
|
||||||
startChat(newKeyValue)
|
startChat(newKeyValue)
|
||||||
nextStep()
|
nextStep()
|
||||||
|
|||||||
+19
@@ -15,6 +15,7 @@ import androidx.compose.material.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
@@ -166,6 +167,24 @@ private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
|
|||||||
SectionView {
|
SectionView {
|
||||||
DisconnectButton(onClick = ::disconnectDesktop)
|
DisconnectButton(onClick = ::disconnectDesktop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProgressIndicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProgressIndicator() {
|
||||||
|
Box(
|
||||||
|
Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = 2.dp)
|
||||||
|
.size(30.dp),
|
||||||
|
color = MaterialTheme.colors.secondary,
|
||||||
|
strokeWidth = 3.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+29
-2
@@ -28,6 +28,7 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
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.ChatModel.controller
|
||||||
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.chat.item.ClickableText
|
import chat.simplex.common.views.chat.item.ClickableText
|
||||||
@@ -58,6 +59,8 @@ fun NetworkAndServersView() {
|
|||||||
smpProxyFallback = smpProxyFallback,
|
smpProxyFallback = smpProxyFallback,
|
||||||
proxyPort = proxyPort,
|
proxyPort = proxyPort,
|
||||||
toggleSocksProxy = { enable ->
|
toggleSocksProxy = { enable ->
|
||||||
|
val def = NetCfg.defaults
|
||||||
|
val proxyDef = NetCfg.proxyDefaults
|
||||||
if (enable) {
|
if (enable) {
|
||||||
AlertManager.shared.showAlertDialog(
|
AlertManager.shared.showAlertDialog(
|
||||||
title = generalGetString(MR.strings.network_enable_socks),
|
title = generalGetString(MR.strings.network_enable_socks),
|
||||||
@@ -65,7 +68,19 @@ fun NetworkAndServersView() {
|
|||||||
confirmText = generalGetString(MR.strings.confirm_verb),
|
confirmText = generalGetString(MR.strings.confirm_verb),
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
val conf = NetCfg.proxyDefaults.withHostPort(chatModel.controller.appPrefs.networkProxyHostPort.get())
|
var conf = controller.getNetCfg().withHostPort(controller.appPrefs.networkProxyHostPort.get())
|
||||||
|
if (conf.tcpConnectTimeout == def.tcpConnectTimeout) {
|
||||||
|
conf = conf.copy(tcpConnectTimeout = proxyDef.tcpConnectTimeout)
|
||||||
|
}
|
||||||
|
if (conf.tcpTimeout == def.tcpTimeout) {
|
||||||
|
conf = conf.copy(tcpTimeout = proxyDef.tcpTimeout)
|
||||||
|
}
|
||||||
|
if (conf.tcpTimeoutPerKb == def.tcpTimeoutPerKb) {
|
||||||
|
conf = conf.copy(tcpTimeoutPerKb = proxyDef.tcpTimeoutPerKb)
|
||||||
|
}
|
||||||
|
if (conf.rcvConcurrency == def.rcvConcurrency) {
|
||||||
|
conf = conf.copy(rcvConcurrency = proxyDef.rcvConcurrency)
|
||||||
|
}
|
||||||
chatModel.controller.apiSetNetworkConfig(conf)
|
chatModel.controller.apiSetNetworkConfig(conf)
|
||||||
chatModel.controller.setNetCfg(conf)
|
chatModel.controller.setNetCfg(conf)
|
||||||
networkUseSocksProxy.value = true
|
networkUseSocksProxy.value = true
|
||||||
@@ -80,7 +95,19 @@ fun NetworkAndServersView() {
|
|||||||
confirmText = generalGetString(MR.strings.confirm_verb),
|
confirmText = generalGetString(MR.strings.confirm_verb),
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
withBGApi {
|
withBGApi {
|
||||||
val conf = NetCfg.defaults
|
var conf = controller.getNetCfg().copy(socksProxy = null)
|
||||||
|
if (conf.tcpConnectTimeout == proxyDef.tcpConnectTimeout) {
|
||||||
|
conf = conf.copy(tcpConnectTimeout = def.tcpConnectTimeout)
|
||||||
|
}
|
||||||
|
if (conf.tcpTimeout == proxyDef.tcpTimeout) {
|
||||||
|
conf = conf.copy(tcpTimeout = def.tcpTimeout)
|
||||||
|
}
|
||||||
|
if (conf.tcpTimeoutPerKb == proxyDef.tcpTimeoutPerKb) {
|
||||||
|
conf = conf.copy(tcpTimeoutPerKb = def.tcpTimeoutPerKb)
|
||||||
|
}
|
||||||
|
if (conf.rcvConcurrency == proxyDef.rcvConcurrency) {
|
||||||
|
conf = conf.copy(rcvConcurrency = def.rcvConcurrency)
|
||||||
|
}
|
||||||
chatModel.controller.apiSetNetworkConfig(conf)
|
chatModel.controller.apiSetNetworkConfig(conf)
|
||||||
chatModel.controller.setNetCfg(conf)
|
chatModel.controller.setNetCfg(conf)
|
||||||
networkUseSocksProxy.value = false
|
networkUseSocksProxy.value = false
|
||||||
|
|||||||
@@ -271,6 +271,11 @@
|
|||||||
<string name="srv_error_host">Server address is incompatible with network settings.</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>
|
<string name="srv_error_version">Server version is incompatible with network settings.</string>
|
||||||
|
|
||||||
|
<!-- CIFileStatus errors -->
|
||||||
|
<string name="file_error_auth">Wrong key or unknown file chunk address - most likely file is deleted.</string>
|
||||||
|
<string name="file_error_no_file">File not found - most likely file was deleted or cancelled.</string>
|
||||||
|
<string name="file_error_relay">File server error: %1$s</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>
|
||||||
@@ -353,6 +358,8 @@
|
|||||||
<string name="share_image">Share media…</string>
|
<string name="share_image">Share media…</string>
|
||||||
<string name="share_file">Share file…</string>
|
<string name="share_file">Share file…</string>
|
||||||
<string name="forward_message">Forward message…</string>
|
<string name="forward_message">Forward message…</string>
|
||||||
|
<string name="cannot_share_message_alert_title">Cannot send message</string>
|
||||||
|
<string name="cannot_share_message_alert_text">Selected chat preferences prohibit this message.</string>
|
||||||
|
|
||||||
<!-- ComposeView.kt, helpers -->
|
<!-- ComposeView.kt, helpers -->
|
||||||
<string name="attach">Attach</string>
|
<string name="attach">Attach</string>
|
||||||
@@ -408,6 +415,8 @@
|
|||||||
<string name="error_saving_file">Error saving file</string>
|
<string name="error_saving_file">Error saving file</string>
|
||||||
<string name="loading_remote_file_title">Loading the file </string>
|
<string name="loading_remote_file_title">Loading the file </string>
|
||||||
<string name="loading_remote_file_desc">Please, wait while the file is being loaded from the linked mobile</string>
|
<string name="loading_remote_file_desc">Please, wait while the file is being loaded from the linked mobile</string>
|
||||||
|
<string name="file_error">File error</string>
|
||||||
|
<string name="temporary_file_error">Temporary file error</string>
|
||||||
|
|
||||||
<!-- Voice messages -->
|
<!-- Voice messages -->
|
||||||
<string name="voice_message">Voice message</string>
|
<string name="voice_message">Voice message</string>
|
||||||
@@ -1403,6 +1412,8 @@
|
|||||||
<string name="info_row_database_id">Database ID</string>
|
<string name="info_row_database_id">Database ID</string>
|
||||||
<string name="info_row_debug_delivery">Debug delivery</string>
|
<string name="info_row_debug_delivery">Debug delivery</string>
|
||||||
<string name="info_row_updated_at">Record updated at</string>
|
<string name="info_row_updated_at">Record updated at</string>
|
||||||
|
<string name="info_row_message_status">Message status</string>
|
||||||
|
<string name="info_row_file_status">File status</string>
|
||||||
<string name="info_row_sent_at">Sent at</string>
|
<string name="info_row_sent_at">Sent at</string>
|
||||||
<string name="info_row_created_at">Created at</string>
|
<string name="info_row_created_at">Created at</string>
|
||||||
<string name="info_row_received_at">Received at</string>
|
<string name="info_row_received_at">Received at</string>
|
||||||
@@ -1411,6 +1422,8 @@
|
|||||||
<string name="info_row_disappears_at">Disappears at</string>
|
<string name="info_row_disappears_at">Disappears at</string>
|
||||||
<string name="share_text_database_id">Database ID: %d</string>
|
<string name="share_text_database_id">Database ID: %d</string>
|
||||||
<string name="share_text_updated_at">Record updated at: %s</string>
|
<string name="share_text_updated_at">Record updated at: %s</string>
|
||||||
|
<string name="share_text_message_status">Message status: %s</string>
|
||||||
|
<string name="share_text_file_status">File status: %s</string>
|
||||||
<string name="share_text_sent_at">Sent at: %s</string>
|
<string name="share_text_sent_at">Sent at: %s</string>
|
||||||
<string name="share_text_created_at">Created at: %s</string>
|
<string name="share_text_created_at">Created at: %s</string>
|
||||||
<string name="share_text_received_at">Received at: %s</string>
|
<string name="share_text_received_at">Received at: %s</string>
|
||||||
@@ -1910,6 +1923,9 @@
|
|||||||
<string name="remote_ctrl_was_disconnected_title">Connection stopped</string>
|
<string name="remote_ctrl_was_disconnected_title">Connection stopped</string>
|
||||||
<string name="remote_host_disconnected_from"><![CDATA[Disconnected from mobile <b>%s</b> with the reason: %s]]></string>
|
<string name="remote_host_disconnected_from"><![CDATA[Disconnected from mobile <b>%s</b> with the reason: %s]]></string>
|
||||||
<string name="remote_ctrl_disconnected_with_reason">Disconnected with the reason: %s</string>
|
<string name="remote_ctrl_disconnected_with_reason">Disconnected with the reason: %s</string>
|
||||||
|
<string name="remote_ctrl_connection_stopped_desc">Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers.</string>
|
||||||
|
<string name="remote_ctrl_connection_stopped_identity_desc">This link was used with another mobile device, please create a new link on the desktop.</string>
|
||||||
|
<string name="copy_error">Copy error</string>
|
||||||
<string name="disconnect_desktop_question">Disconnect desktop?</string>
|
<string name="disconnect_desktop_question">Disconnect desktop?</string>
|
||||||
<string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string>
|
<string name="only_one_device_can_work_at_the_same_time">Only one device can work at the same time</string>
|
||||||
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code.]]></string>
|
<string name="open_on_mobile_and_scan_qr_code"><![CDATA[Open <i>Use from desktop</i> in mobile app and scan QR code.]]></string>
|
||||||
|
|||||||
+7
-2
@@ -14,8 +14,10 @@ import com.russhwolf.settings.*
|
|||||||
import dev.icerock.moko.resources.ImageResource
|
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
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font =
|
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font =
|
||||||
@@ -59,8 +61,11 @@ private val settingsThemesProps =
|
|||||||
Properties()
|
Properties()
|
||||||
.also { props -> try { settingsThemesFile.reader().use { props.load(it) } } catch (e: Exception) { /**/ } }
|
.also { props -> try { settingsThemesFile.reader().use { props.load(it) } } catch (e: Exception) { /**/ } }
|
||||||
|
|
||||||
actual val settings: Settings = PropertiesSettings(settingsProps) { withApi { settingsFile.writer().use { settingsProps.store(it, "") } } }
|
|
||||||
actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { withApi { settingsThemesFile.writer().use { settingsThemesProps.store(it, "") } } }
|
private val settingsWriterThread = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
|
|
||||||
|
actual val settings: Settings = PropertiesSettings(settingsProps) { CoroutineScope(settingsWriterThread).launch { settingsFile.writer().use { settingsProps.store(it, "") } } }
|
||||||
|
actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { CoroutineScope(settingsWriterThread).launch { settingsThemesFile.writer().use { settingsThemesProps.store(it, "") } } }
|
||||||
|
|
||||||
actual fun windowOrientation(): WindowOrientation =
|
actual fun windowOrientation(): WindowOrientation =
|
||||||
if (simplexWindowState.windowState.size.width > simplexWindowState.windowState.size.height) {
|
if (simplexWindowState.windowState.size.width > simplexWindowState.windowState.size.height) {
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ android.enableJetifier=true
|
|||||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||||
kotlin.jvm.target=11
|
kotlin.jvm.target=11
|
||||||
|
|
||||||
android.version_name=5.8-beta.5
|
android.version_name=5.8.1
|
||||||
android.version_code=218
|
android.version_code=221
|
||||||
|
|
||||||
desktop.version_name=5.8-beta.5
|
desktop.version_name=5.8.1
|
||||||
desktop.version_code=52
|
desktop.version_code=54
|
||||||
|
|
||||||
kotlin.version=1.9.23
|
kotlin.version=1.9.23
|
||||||
gradle.plugin.version=8.2.0
|
gradle.plugin.version=8.2.0
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
layout: layouts/article.html
|
layout: layouts/article.html
|
||||||
title: "SimpleX network: private message routing, v5.8 released with IP address protection and chat themes"
|
title: "SimpleX network: private message routing, v5.8 released with IP address protection and chat themes"
|
||||||
date: 2024-06-04
|
date: 2024-06-04
|
||||||
# previewBody: blog_previews/20240426.html
|
previewBody: blog_previews/20240604.html
|
||||||
draft: true
|
image: images/20240604-routing.png
|
||||||
# image: images/20240426-profile.png
|
imageBottom: true
|
||||||
# imageBottom: true
|
|
||||||
permalink: "/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html"
|
permalink: "/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -13,6 +12,159 @@ permalink: "/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes
|
|||||||
|
|
||||||
**Published:** June 4, 2024
|
**Published:** June 4, 2024
|
||||||
|
|
||||||
TODO
|
What's new in v5.8:
|
||||||
|
- [private message routing](#private-message-routing).
|
||||||
|
- [server transparency](#server-transparency).
|
||||||
|
- [protect IP address when downloading files & media](#protect-ip-address-when-downloading-files--media).
|
||||||
|
- [chat themes](#chat-themes) for better conversation privacy - in Android and desktop apps.
|
||||||
|
- [group improvements](#group-improvements) - reduced traffic and additional preferences.
|
||||||
|
- improved networking, message and file delivery.
|
||||||
|
|
||||||
This is a permalink for release announcement.
|
Also, we added Persian interface language to the Android and desktop apps, thanks to [our users and Weblate](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat).
|
||||||
|
|
||||||
|
## Private message routing
|
||||||
|
|
||||||
|
### What's the problem?
|
||||||
|
|
||||||
|
<img src="./images/simplex-explained.svg" width="37%" class="float-right">
|
||||||
|
|
||||||
|
SimpleX network design has always been focussed on protecting user identity on the messaging protocol level - there is no user profile identifiers of any kind in the protocol design, not even random numbers or cryptographic keys.
|
||||||
|
|
||||||
|
Until this release though, SimpleX network had no built-in protection of user transport identities - IP addresses. As previously the users could only choose which messaging relays to use to receive messages, these relays could observe the IP addresses of the senders, and if these relays were controlled by the recipients, the recipients themselves could observe them too - either by modifying server code or simply by tracking all connecting IP addresses.
|
||||||
|
|
||||||
|
To work around this limitation, many users connected to SimpleX network relays via Tor or VPN - so that the recipients' relays could not observe IP addresses of the users when they send messages. Still, it was the most important and the most criticized limitation of SimpleX network for the users.
|
||||||
|
|
||||||
|
### Why didn't we just embed Tor in the app?
|
||||||
|
|
||||||
|
Tor is the best transport overlay network in existence, and it provides network anonymity for millions of Internet users.
|
||||||
|
|
||||||
|
SimpleX Chat has many integration points with Tor:
|
||||||
|
- it allows [dual server addresses](./20220901-simplex-chat-v3.2-incognito-mode.md#using-onion-server-addresses-with-tor), when the same messaging relay can be reached both via Tor and via clearnet.
|
||||||
|
- it utilises Tor's SOCKS proxy "isolate-by-auth" feature to create a new Tor circuit for each user profile, and with an additional option - for each contact. Per-contact [transport isolation](./20230204-simplex-chat-v4-5-user-chat-profiles.md#transport-isolation) is still experimental, as it doesn't work if you connect to groups with many members, and it's only available if you enable developer tools.
|
||||||
|
|
||||||
|
Many SimpleX network design ideas are borrowed from Tor network design:
|
||||||
|
- mitigation of [MITM attack](../docs/GLOSSARY.md#man-in-the-middle-attack) on client-server connection is done in the same way as Tor relays do it - the fingerprint of offline certificate is included in server address and validated by the client.
|
||||||
|
- the private routing itself uses the approach similar to onion routing, by adding encryption layers on each hop.
|
||||||
|
- we are also considering to implement Tor's [Proof-of-work DoS defence](https://blog.torproject.org/introducing-proof-of-work-defense-for-onion-services/) mechanism.
|
||||||
|
|
||||||
|
So why didn't we just embed Tor into the messaging clients to provide IP address protection?
|
||||||
|
|
||||||
|
We believe that Tor may be the wrong solution for some users for one of the reasons:
|
||||||
|
- much higher latency, error rate and resource usage.
|
||||||
|
- people who want to use Tor are better served by specialized apps, such as [Orbot](https://guardianproject.info/apps/org.torproject.android/).
|
||||||
|
- Tor usage is restricted in some networks, so it would require complex configuration in the app UI.
|
||||||
|
- some countries have legislative restrictions on Tor usage, so embedding Tor would require supporting multiple app versions, and it would leave the original problem unsolved in these countries.
|
||||||
|
|
||||||
|
Also, while Tor solves the problem of IP address protection, it doesn't solve the problem of meta-data correlation by user's transport session. When the client connects to the messaging relays via Tor, the relays can still observe which messaging queues a user sends messages to via a single TCP connection. The client can mitigate it with per-contact transport isolation, but it uses too much traffic and battery for most users.
|
||||||
|
|
||||||
|
So we believed we would create more value to the users of SimpleX network with private message routing. This new message routing protocol provides IP address and transport session protection out of the box, once released. It can also be extended to support delayed delivery and other functions, improving both usability and transport privacy in the future.
|
||||||
|
|
||||||
|
At the same time, we plan to continue supporting Tor and other overlay networks. Any overlay network that supports SOCKS proxy with "isolate-by-auth" feature will work with SimpleX Chat app.
|
||||||
|
|
||||||
|
### What is private message routing and how does it work?
|
||||||
|
|
||||||
|
Private message routing is a major milestone for SimpleX network evolution. It is a new message routing protocol that protects both users' IP addresses and transport sessions from the messaging relays chosen by their contacts. Private message routing is, effectively, a 2-hop onion routing protocol inspired by Tor design, but with one important difference - the first (forwarding) relay is always chosen by message sender and the second (destination) - by the message recipient. In this way, neither side of the conversation can observe IP address or transport session of another.
|
||||||
|
|
||||||
|
At the same time, the relays chosen by the sending clients to forward the messages cannot observe to which connections (messaging queues) the messages are sent, because of the additional layer of end-to-end encryption between the sender and the destination relay, similar to how onion routing works in Tor network, and also thanks to the protocol design that avoids any repeated or non-random identifiers associated with the messages, that would otherwise allow correlating the messages sent to different connections as sent by the same user. Each message forwarded to the destination relay is additionally encrypted with one-time ephemeral key, to be independent of messages sent to different connections.
|
||||||
|
|
||||||
|
The routing protocol also prevents the possibility of MITM attack by the forwarding relay, which provides the certificate the session keys of the destination server to the sending client that are cryptographically signed by the same certificate that is included in destination server address, so the client can verify that the messages are sent to the intended destination, and not intercepted.
|
||||||
|
|
||||||
|
The diagram below shows all the encryption layers used in private message routing:
|
||||||
|
|
||||||
|
```
|
||||||
|
----------------- ----------------- -- TLS -- ----------------- -----------------
|
||||||
|
| | -- TLS -- | | -- f2d -- | | -- TLS -- | |
|
||||||
|
| | -- s2d -- | | -- s2d -- | | -- d2r -- | |
|
||||||
|
| Sending | -- e2e -- | sender's | -- e2e -- | recipient's | -- e2e -- | Receiving |
|
||||||
|
| client | message -> | Forwarding | message -> | Destination | message -> | client |
|
||||||
|
| | -- e2e -- | relay | -- e2e -- | relay | -- e2e -- | |
|
||||||
|
| | -- s2d -- | | -- s2d -- | | -- d2r -- | |
|
||||||
|
| | -- TLS -- | | -- f2d -- | | -- TLS -- | |
|
||||||
|
----------------- ----------------- -- TLS -- ----------------- -----------------
|
||||||
|
```
|
||||||
|
|
||||||
|
**e2e** - two end-to-end encryption layers between **sending** and **receiving** clients, one of which uses double ratchet algorithm. These encryption layers are present in the previous version of message routing protocol too.
|
||||||
|
|
||||||
|
**s2d** - encryption between the **sending** client and recipient's **destination** relay. This new encryption layer hides the message metadata (destination connection address and message notification flag) from the forwarding relay.
|
||||||
|
|
||||||
|
**f2d** - additional new encryption layer between **forwarding** and **destination** relays, protecting from traffic correlation in case TLS is compromised - there are no identifiers or cyphertext in common between incoming and outgoing traffic of both relays inside TLS connection.
|
||||||
|
|
||||||
|
**d2r** - additional encryption layer between destination relay and the recipient, also protecting from traffic correlation in case TLS is compromised.
|
||||||
|
|
||||||
|
**TLS** - TLS 1.3 transport encryption.
|
||||||
|
|
||||||
|
For private routing to work, both the forwardig and the destination relays should support the updated messaging protocol - it is supported from v5.8 of the messaging relays. It is already released to all relays preset in the app, and available as a self-hosted server. We updated [the guide](../docs/SERVER.md) about how to host your own messaging relays.
|
||||||
|
|
||||||
|
Because many self-hosted relays did not upgrade yet, private routing is not enabled by default. To enable it, you can open *Network & servers* settings in the app and change the settings in *Private message routing* section. We recommend setting *Private routing* option to *Unprotected* (to use it only with unknown relays and when not connecting via Tor) and *Allow downgrade* to *Yes* (so messages can still be delivered to the messaging relays that didn't upgrade yet) or to *When IP hidden* (in which case the messages will fail to deliver to unknown relays that didn't upgrade yet unless you connect to them via Tor).
|
||||||
|
|
||||||
|
Read more about the technical design of the private message routing in [this document](https://github.com/simplex-chat/simplexmq/blob/stable/rfcs/2023-09-12-second-relays.md).
|
||||||
|
|
||||||
|
## Server transparency
|
||||||
|
|
||||||
|
<img src="./images/20240604-server.png" width="40%" class="float-to-right">
|
||||||
|
|
||||||
|
Even with very limited information available to the messaging relays, there are [several things](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#simplex-messaging-protocol-server) that would reduce users' privacy that a compromised relay can do.
|
||||||
|
|
||||||
|
We [wrote previously](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2024-03-20-server-metadata.md) that it is important that server operators commit to running unmodified server code or disclose any code modifications, and also disclose server ownership and any other relevant information.
|
||||||
|
|
||||||
|
While we cannot require the operators of self-hosted and private servers to disclose any information about them (apart from which server code they use - this is the requirement of the AGPLv3 license to share this information with users connecting to the server), as we add other server operators to the app, it is important for the users to have all important information about these operators and servers location.
|
||||||
|
|
||||||
|
This server release adds server information page where all this information can be made available to the users. For example, this is <a href="https://smp8.simplex.im" target="_blank">the information</a> about one of the servers preset in the app.
|
||||||
|
|
||||||
|
The updated server guide also includes [the instruction](../docs/SERVER.md#) about how to host this page for your server. It is generated as a static page when the server starts. We recommend using Caddy webserver to serve it.
|
||||||
|
|
||||||
|
## More new things in v5.8
|
||||||
|
|
||||||
|
### Protect IP address when downloading files & media
|
||||||
|
|
||||||
|
This version added the protection of your IP address when receiving files from unknown file servers without Tor. Images and voice messages won't automatically download from unknown servers too until you tap them, and confirm that you trust the file server where they were uploaded.
|
||||||
|
|
||||||
|
### Chat themes
|
||||||
|
|
||||||
|
<img src="./images/20240604-theme1.png" width="244" class="float-to-right"> <img src="./images/20240604-theme2.png" width="244" class="float-to-right">
|
||||||
|
|
||||||
|
In Android and desktop app you can now customize how the app looks by choosing wallpapers with one of the preset themes or choose your own image as a wallpaper.
|
||||||
|
|
||||||
|
But this feature is not only about customization - it allows to set different colors and wallpaper for different user profiles and even specific conversations. You can also choose different themes for different chat profiles.
|
||||||
|
|
||||||
|
In case you use different identities for different conversations, it helps avoiding mistakes.
|
||||||
|
|
||||||
|
### Group improvements
|
||||||
|
|
||||||
|
This version adds additional group configuration options to allow sending images, files and media, and also SimpleX links only to group administrators and owners. So with this release group owners can have more control over content shared in the groups.
|
||||||
|
|
||||||
|
We also stopped unnecessary traffic caused by the members who became inactive without leaving the groups - it should substantially reduce traffic and battery consumption to the users who send messages in large groups.
|
||||||
|
|
||||||
|
## SimpleX network
|
||||||
|
|
||||||
|
Some links to answer the most common questions:
|
||||||
|
|
||||||
|
[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers).
|
||||||
|
|
||||||
|
[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users).
|
||||||
|
|
||||||
|
[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations).
|
||||||
|
|
||||||
|
[Frequently asked questions](../docs/FAQ.md).
|
||||||
|
|
||||||
|
Please also see our [website](https://simplex.chat).
|
||||||
|
|
||||||
|
## Help us with donations
|
||||||
|
|
||||||
|
Huge thank you to everybody who donates to SimpleX Chat!
|
||||||
|
|
||||||
|
We are planning a 3rd party security audit for the protocols and cryptography design in July 2024, and also the security audit for an implementation in December 2024/January 2025, and it would hugely help us if some part of this $50,000+ expense is covered with donations.
|
||||||
|
|
||||||
|
We are prioritizing users privacy and security - it would be impossible without your support.
|
||||||
|
|
||||||
|
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX network based on the same principles as email and web, but much more private and secure.
|
||||||
|
|
||||||
|
Your donations help us raise more funds – any amount, even the price of the cup of coffee, makes a big difference for us.
|
||||||
|
|
||||||
|
See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate.
|
||||||
|
|
||||||
|
Thank you,
|
||||||
|
|
||||||
|
Evgeny
|
||||||
|
|
||||||
|
SimpleX Chat founder
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# Blog
|
# Blog
|
||||||
|
|
||||||
|
Jun 4, 2024 [SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md)
|
||||||
|
|
||||||
|
What's new in v5.8:
|
||||||
|
- private message routing.
|
||||||
|
- server transparency.
|
||||||
|
- protect IP address when downloading files & media.
|
||||||
|
- chat themes for better conversation privacy - in Android and desktop apps.
|
||||||
|
- group improvements - reduced traffic and additional preferences.
|
||||||
|
- improved networking, message and file delivery.
|
||||||
|
|
||||||
|
Also, we added Persian interface language to the Android and desktop apps, thanks to our users and Weblate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
Jun 1, 2024 [Children's Safety Requires End-to-End Encryption](./20240601-children-safety-requires-e2e-encryption.md)
|
Jun 1, 2024 [Children's Safety Requires End-to-End Encryption](./20240601-children-safety-requires-e2e-encryption.md)
|
||||||
|
|
||||||
As lawmakers grapple with the serious issue of child exploitation online, some proposed solutions would fuel the very problem they aim to solve.
|
As lawmakers grapple with the serious issue of child exploitation online, some proposed solutions would fuel the very problem they aim to solve.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 658 KiB |
+1
-1
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/simplex-chat/simplexmq.git
|
location: https://github.com/simplex-chat/simplexmq.git
|
||||||
tag: 2e4f507919dd3a08e5c4106fde193fee4414de3c
|
tag: 8a3b72458f917e9867f4e3640dda0fa1827ff6cf
|
||||||
|
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ revision: 11.02.2024
|
|||||||
| Updated 23.03.2024 | Languages: EN |
|
| Updated 23.03.2024 | Languages: EN |
|
||||||
# Download SimpleX apps
|
# Download SimpleX apps
|
||||||
|
|
||||||
The latest stable version is v5.6.
|
The latest stable version is v5.8.
|
||||||
|
|
||||||
You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases).
|
You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases).
|
||||||
|
|
||||||
|
|||||||
+220
-19
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Hosting your own SMP Server
|
title: Hosting your own SMP Server
|
||||||
revision: 28.05.2024
|
revision: 03.06.2024
|
||||||
---
|
---
|
||||||
|
|
||||||
| Updated 28.05.2024 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md), [PL](/docs/lang/pl/SERVER.md) |
|
| Updated 28.05.2024 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md), [PL](/docs/lang/pl/SERVER.md) |
|
||||||
@@ -21,6 +21,7 @@ revision: 28.05.2024
|
|||||||
- [Tor: installation and configuration](#tor-installation-and-configuration)
|
- [Tor: installation and configuration](#tor-installation-and-configuration)
|
||||||
- [Installation for onion address](#installation-for-onion-address)
|
- [Installation for onion address](#installation-for-onion-address)
|
||||||
- [SOCKS port for SMP PROXY](#socks-port-for-smp-proxy)
|
- [SOCKS port for SMP PROXY](#socks-port-for-smp-proxy)
|
||||||
|
- [Server information page](#server-information-page)
|
||||||
- [Documentation](#documentation)
|
- [Documentation](#documentation)
|
||||||
- [SMP server address](#smp-server-address)
|
- [SMP server address](#smp-server-address)
|
||||||
- [Systemd commands](#systemd-commands)
|
- [Systemd commands](#systemd-commands)
|
||||||
@@ -227,6 +228,44 @@ The server address above should be used in your client configuration, and if you
|
|||||||
All generated configuration, along with a description for each parameter, is available inside configuration file in `/etc/opt/simplex/smp-server.ini` for further customization. Depending on the smp-server version, the configuration file looks something like this:
|
All generated configuration, along with a description for each parameter, is available inside configuration file in `/etc/opt/simplex/smp-server.ini` for further customization. Depending on the smp-server version, the configuration file looks something like this:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
|
[INFORMATION]
|
||||||
|
# AGPLv3 license requires that you make any source code modifications
|
||||||
|
# available to the end users of the server.
|
||||||
|
# LICENSE: https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE
|
||||||
|
# Include correct source code URI in case the server source code is modified in any way.
|
||||||
|
# If any other information fields are present, source code property also MUST be present.
|
||||||
|
|
||||||
|
source_code: https://github.com/simplex-chat/simplexmq
|
||||||
|
|
||||||
|
# Declaring all below information is optional, any of these fields can be omitted.
|
||||||
|
|
||||||
|
# Server usage conditions and amendments.
|
||||||
|
# It is recommended to use standard conditions with any amendments in a separate document.
|
||||||
|
# usage_conditions: https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md
|
||||||
|
# condition_amendments: link
|
||||||
|
|
||||||
|
# Server location and operator.
|
||||||
|
server_country: <YOUR_SERVER_LOCATION>
|
||||||
|
operator: <YOUR_NAME>
|
||||||
|
operator_country: <YOUR_LOCATION>
|
||||||
|
website: <WEBSITE_IF_AVAILABLE>
|
||||||
|
|
||||||
|
# Administrative contacts.
|
||||||
|
#admin_simplex: SimpleX address
|
||||||
|
admin_email: <EMAIL>
|
||||||
|
# admin_pgp:
|
||||||
|
# admin_pgp_fingerprint:
|
||||||
|
|
||||||
|
# Contacts for complaints and feedback.
|
||||||
|
# complaints_simplex: SimpleX address
|
||||||
|
complaints_email: <COMPLAINTS_EMAIL>
|
||||||
|
# complaints_pgp:
|
||||||
|
# complaints_pgp_fingerprint:
|
||||||
|
|
||||||
|
# Hosting provider.
|
||||||
|
hosting: <HOSTING_PROVIDER_NAME>
|
||||||
|
hosting_country: <HOSTING_PROVIDER_LOCATION>
|
||||||
|
|
||||||
[STORE_LOG]
|
[STORE_LOG]
|
||||||
# The server uses STM memory for persistence,
|
# The server uses STM memory for persistence,
|
||||||
# that will be lost on restart (e.g., as with redis).
|
# that will be lost on restart (e.g., as with redis).
|
||||||
@@ -289,6 +328,21 @@ websockets: off
|
|||||||
disconnect: off
|
disconnect: off
|
||||||
# ttl: 43200
|
# ttl: 43200
|
||||||
# check_interval: 3600
|
# check_interval: 3600
|
||||||
|
|
||||||
|
[WEB]
|
||||||
|
# Set path to generate static mini-site for server information and qr codes/links
|
||||||
|
static_path: /var/opt/simplex/www
|
||||||
|
|
||||||
|
# Run an embedded server on this port
|
||||||
|
# Onion sites can use any port and register it in the hidden service config.
|
||||||
|
# Running on a port 80 may require setting process capabilities.
|
||||||
|
# http: 8000
|
||||||
|
|
||||||
|
# You can run an embedded TLS web server too if you provide port and cert and key files.
|
||||||
|
# Not required for running relay on onion address.
|
||||||
|
# https: 443
|
||||||
|
# cert: /etc/opt/simplex/web.cert
|
||||||
|
# key: /etc/opt/simplex/web.key
|
||||||
```
|
```
|
||||||
|
|
||||||
## Server security
|
## Server security
|
||||||
@@ -543,6 +597,110 @@ SMP-server versions starting from `v5.8.0-beta.0` can be configured to PROXY smp
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Server information page
|
||||||
|
|
||||||
|
SMP-server versions starting from `v5.8.0` can be configured to serve Web page with server information that can include admin info, server info, provider info, etc. Run the following commands as `root` user.
|
||||||
|
|
||||||
|
1. Add the following to your smp-server configuration (please modify fields in [INFORMATION] section to include relevant information):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vim /etc/opt/simplex/smp-server.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[WEB]
|
||||||
|
static_path: /var/opt/simplex/www
|
||||||
|
|
||||||
|
[INFORMATION]
|
||||||
|
# AGPLv3 license requires that you make any source code modifications
|
||||||
|
# available to the end users of the server.
|
||||||
|
# LICENSE: https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE
|
||||||
|
# Include correct source code URI in case the server source code is modified in any way.
|
||||||
|
# If any other information fields are present, source code property also MUST be present.
|
||||||
|
|
||||||
|
source_code: https://github.com/simplex-chat/simplexmq
|
||||||
|
|
||||||
|
# Declaring all below information is optional, any of these fields can be omitted.
|
||||||
|
|
||||||
|
# Server usage conditions and amendments.
|
||||||
|
# It is recommended to use standard conditions with any amendments in a separate document.
|
||||||
|
# usage_conditions: https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md
|
||||||
|
# condition_amendments: link
|
||||||
|
|
||||||
|
# Server location and operator.
|
||||||
|
server_country: <YOUR_SERVER_LOCATION>
|
||||||
|
operator: <YOUR_NAME>
|
||||||
|
operator_country: <YOUR_LOCATION>
|
||||||
|
website: <WEBSITE_IF_AVAILABLE>
|
||||||
|
|
||||||
|
# Administrative contacts.
|
||||||
|
#admin_simplex: SimpleX address
|
||||||
|
admin_email: <EMAIL>
|
||||||
|
# admin_pgp:
|
||||||
|
# admin_pgp_fingerprint:
|
||||||
|
|
||||||
|
# Contacts for complaints and feedback.
|
||||||
|
# complaints_simplex: SimpleX address
|
||||||
|
complaints_email: <COMPLAINTS_EMAIL>
|
||||||
|
# complaints_pgp:
|
||||||
|
# complaints_pgp_fingerprint:
|
||||||
|
|
||||||
|
# Hosting provider.
|
||||||
|
hosting: <HOSTING_PROVIDER_NAME>
|
||||||
|
hosting_country: <HOSTING_PROVIDER_LOCATION>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install the webserver. For easy deployment we'll describe the installtion process of [Caddy](https://caddyserver.com) webserver on Ubuntu server:
|
||||||
|
|
||||||
|
1. Install the packages:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install caddy gpg key for repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install Caddy repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Install Caddy:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt update && sudo apt install caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
[Full Caddy instllation instructions](https://caddyserver.com/docs/install)
|
||||||
|
|
||||||
|
3. Replace Caddy configuration with the following (don't forget to replace `<YOUR_DOMAIN>`):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vim /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
<YOUR_DOMAIN> {
|
||||||
|
root * /var/opt/simplex/www
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Enable and start Caddy service:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
systemctl enable --now caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Upgrade your smp-server to latest version - [Updating your smp server](#updating-your-smp-server)
|
||||||
|
|
||||||
|
6. Access the webpage you've deployed from your browser. You should see the smp-server information that you've provided in your ini file.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
All necessary files for `smp-server` are located in `/etc/opt/simplex/` folder.
|
All necessary files for `smp-server` are located in `/etc/opt/simplex/` folder.
|
||||||
@@ -635,26 +793,69 @@ You can enable `smp-server` statistics for `Grafana` dashboard by setting value
|
|||||||
Logs will be stored as `csv` file in `/var/opt/simplex/smp-server-stats.daily.log`. Fields for the `csv` file are:
|
Logs will be stored as `csv` file in `/var/opt/simplex/smp-server-stats.daily.log`. Fields for the `csv` file are:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,monthMsgQueues
|
fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,monthMsgQueues,msgSentNtf,msgRecvNtf,dayCountNtf,weekCountNtf,monthCountNtf,qCount,msgCount,msgExpired,qDeletedNew,qDeletedSecured,pRelays_pRequests,pRelays_pSuccesses,pRelays_pErrorsConnect,pRelays_pErrorsCompat,pRelays_pErrorsOther,pRelaysOwn_pRequests,pRelaysOwn_pSuccesses,pRelaysOwn_pErrorsConnect,pRelaysOwn_pErrorsCompat,pRelaysOwn_pErrorsOther,pMsgFwds_pRequests,pMsgFwds_pSuccesses,pMsgFwds_pErrorsConnect,pMsgFwds_pErrorsCompat,pMsgFwds_pErrorsOther,pMsgFwdsOwn_pRequests,pMsgFwdsOwn_pSuccesses,pMsgFwdsOwn_pErrorsConnect,pMsgFwdsOwn_pErrorsCompat,pMsgFwdsOwn_pErrorsOther,pMsgFwdsRecv,qSub,qSubAuth,qSubDuplicate,qSubProhibited,msgSentAuth,msgSentQuota,msgSentLarge
|
||||||
```
|
```
|
||||||
|
|
||||||
- `fromTime` - timestamp; date and time of event
|
| Field number | Field name | Field Description |
|
||||||
|
| ------------- | ---------------------------- | -------------------------- |
|
||||||
- `qCreated` - int; created queues
|
| 1 | `fromTime` | Date of statistics |
|
||||||
|
| Messaging queue: |
|
||||||
- `qSecured` - int; established queues
|
| 2 | `qCreated` | Created |
|
||||||
|
| 3 | `qSecured` | Established |
|
||||||
- `qDeleted` - int; deleted queues
|
| 4 | `qDeleted` | Deleted |
|
||||||
|
| Messages: |
|
||||||
- `msgSent` - int; sent messages
|
| 5 | `msgSent` | Sent |
|
||||||
|
| 6 | `msgRecv` | Received |
|
||||||
- `msgRecv` - int; received messages
|
| 7 | `dayMsgQueues` | Active queues in a day |
|
||||||
|
| 8 | `weekMsgQueues` | Active queues in a week |
|
||||||
- `dayMsgQueues` - int; active queues in a day
|
| 9 | `monthMsgQueues` | Active queues in a month |
|
||||||
|
| Messages with "notification" flag |
|
||||||
- `weekMsgQueues` - int; active queues in a week
|
| 10 | `msgSentNtf` | Sent |
|
||||||
|
| 11 | `msgRecvNtf` | Received |
|
||||||
- `monthMsgQueues` - int; active queues in a month
|
| 12 | `dayCountNtf` | Active queues in a day |
|
||||||
|
| 13 | `weekCountNtf` | Active queues in a week |
|
||||||
|
| 14 | `monthCountNtf` | Active queues in a month |
|
||||||
|
| Additional statistics: |
|
||||||
|
| 15 | `qCount` | Stored queues |
|
||||||
|
| 16 | `msgCount` | Stored messages |
|
||||||
|
| 17 | `msgExpired` | Expired messages |
|
||||||
|
| 18 | `qDeletedNew` | New deleted queues |
|
||||||
|
| 19 | `qDeletedSecured` | Secured deleted queues |
|
||||||
|
| Requested sessions with all relays: |
|
||||||
|
| 20 | `pRelays_pRequests` | - requests |
|
||||||
|
| 21 | `pRelays_pSuccesses` | - successes |
|
||||||
|
| 22 | `pRelays_pErrorsConnect` | - connection errors |
|
||||||
|
| 23 | `pRelays_pErrorsCompat` | - compatability errors |
|
||||||
|
| 24 | `pRelays_pErrorsOther` | - other errors |
|
||||||
|
| Requested sessions with own relays: |
|
||||||
|
| 25 | `pRelaysOwn_pRequests` | - requests |
|
||||||
|
| 26 | `pRelaysOwn_pSuccesses` | - successes |
|
||||||
|
| 27 | `pRelaysOwn_pErrorsConnect` | - connection errors |
|
||||||
|
| 28 | `pRelaysOwn_pErrorsCompat` | - compatability errors |
|
||||||
|
| 29 | `pRelaysOwn_pErrorsOther` | - other errors |
|
||||||
|
| Message forwards to all relays: |
|
||||||
|
| 30 | `pMsgFwds_pRequests` | - requests |
|
||||||
|
| 31 | `pMsgFwds_pSuccesses` | - successes |
|
||||||
|
| 32 | `pMsgFwds_pErrorsConnect` | - connection errors |
|
||||||
|
| 33 | `pMsgFwds_pErrorsCompat` | - compatability errors |
|
||||||
|
| 34 | `pMsgFwds_pErrorsOther` | - other errors |
|
||||||
|
| Message forward to own relays: |
|
||||||
|
| 35 | `pMsgFwdsOwn_pRequests` | - requests |
|
||||||
|
| 36 | `pMsgFwdsOwn_pSuccesses` | - successes |
|
||||||
|
| 37 | `pMsgFwdsOwn_pErrorsConnect` | - connection errors |
|
||||||
|
| 38 | `pMsgFwdsOwn_pErrorsCompat` | - compatability errors |
|
||||||
|
| 39 | `pMsgFwdsOwn_pErrorsOther` | - other errors |
|
||||||
|
| Received message forwards: |
|
||||||
|
| 40 | `pMsgFwdsRecv` | |
|
||||||
|
| Message queue subscribtion errors: |
|
||||||
|
| 41 | `qSub` | All |
|
||||||
|
| 42 | `qSubAuth` | Authentication erorrs |
|
||||||
|
| 43 | `qSubDuplicate` | Duplicate SUB errors |
|
||||||
|
| 44 | `qSubProhibited` | Prohibited SUB errors |
|
||||||
|
| Message errors: |
|
||||||
|
| 45 | `msgSentAuth` | Authentication errors |
|
||||||
|
| 46 | `msgSentQuota` | Quota errors |
|
||||||
|
| 47 | `msgSentLarge` | Large message errors |
|
||||||
|
|
||||||
To import `csv` to `Grafana` one should:
|
To import `csv` to `Grafana` one should:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
# Flexible user records
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Currently user records work as rigid containers for conversations. New conversations can be created only for an active user. Users want to be able to select a user record for a new conversation right at the connection screen (i.e. when scanning QR code or pasting a link). A similar problem was previously solved regarding Incognito mode - initially it required changing a global setting, then it was moved as a toggle to the connection screen, then it was reworked to be offered after scanning the link.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
## UI
|
||||||
|
|
||||||
|
Connection UI would offer to join connections as other users.
|
||||||
|
|
||||||
|
Current options when joining:
|
||||||
|
|
||||||
|
```
|
||||||
|
- "Use current profile"
|
||||||
|
- "Use new incognito profile"
|
||||||
|
```
|
||||||
|
|
||||||
|
Will change to:
|
||||||
|
|
||||||
|
If there're only 2 users:
|
||||||
|
|
||||||
|
```
|
||||||
|
- "Use current profile"
|
||||||
|
- "Use new incognito profile"
|
||||||
|
- "Use <user 2 name> profile"
|
||||||
|
```
|
||||||
|
|
||||||
|
If there's more than 2 users:
|
||||||
|
|
||||||
|
```
|
||||||
|
- "Use current profile"
|
||||||
|
- "Use new incognito profile"
|
||||||
|
- "Use other profile" (opens sheet with list of users)
|
||||||
|
```
|
||||||
|
|
||||||
|
Things to consider:
|
||||||
|
|
||||||
|
- hidden users should be excluded from this selection
|
||||||
|
- choosing different user should make it active and open chat list for this user, then create pending connection there
|
||||||
|
- should connection plan api take into account all users?
|
||||||
|
|
||||||
|
## Other ideas
|
||||||
|
|
||||||
|
### Incognito chats in a separate user "profile"
|
||||||
|
|
||||||
|
Having incognito conversations interleaved with "main profile" conversations is another point of confusion, as incognito profile is offered as an alternative to main profile, but conversations are still "attached" to it and inherit some of its settings (e.g. servers). We could unite all incognito chats under a new dummy "incognito" user profile. It would have a special representation in UI, not as a regular user profile, but as an incognito mode. It would allow customizing a specific theme, servers, preferences and other settings for all incognito chats.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
-- Types
|
||||||
|
|
||||||
|
data User = User
|
||||||
|
{ ...
|
||||||
|
incognitoUser :: Bool,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Controller
|
||||||
|
|
||||||
|
APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri)
|
||||||
|
-- -> changed to ->
|
||||||
|
APIConnect UserId (Maybe AConnectionRequestUri)
|
||||||
|
-- since conversation being incognito would be defined by user
|
||||||
|
```
|
||||||
|
|
||||||
|
Considerations / problems:
|
||||||
|
|
||||||
|
- migration of existing incognito conversations is non trivial, as it requires migrating both agent and chat connection records to a new user record, in addition to migrating other chat entities.
|
||||||
|
- some programmatic one-time migration on chat start would be required, e.g.:
|
||||||
|
- create new user in agent;
|
||||||
|
- create new user in chat with incognito_user set to true;
|
||||||
|
- for each (non hidden, see below) user read chat list, update user_id for all chat entities:
|
||||||
|
- contacts,
|
||||||
|
- groups,
|
||||||
|
- group_members,
|
||||||
|
- contact_profiles,
|
||||||
|
- chat_items, etc.
|
||||||
|
- this new user servers would include all servers from users that had incognito conversations (?).
|
||||||
|
- this seems quite complex error-prone.
|
||||||
|
- it may be more pragmatic to not migrate old conversations to new user record, but instead filter them out in their respective user chat lists, and filter them in incognito user profile.
|
||||||
|
- in this case "legacy" incognito conversations would be marked by their user record (avatar/name inside chat list; note inside chat view saying that such and such settings are inherited from user x).
|
||||||
|
- we could still make a hack to apply same incognito user theme for "legacy" incognito conversations.
|
||||||
|
- on the other hand the second approach requires loading all chats for the incognito user (this may be related to "All chats view", see below).
|
||||||
|
- when creating an incognito conversation for a hidden user, it should still be attached to that user.
|
||||||
|
- or we could create an "incognito hidden user".
|
||||||
|
- considering complexities, this all seems quite a rabbit hole and may be not worth it..
|
||||||
|
- MVP may be to do nothing for legacy incognito contacts and just explain it in app. A-la "New incognito conversations will appear here, previously created incognito conversations will stay attached to user profiles they were created in".
|
||||||
|
|
||||||
|
### Forward messages between users
|
||||||
|
|
||||||
|
Should be somewhat easy in backend:
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
APIForwardChatItem {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemId :: ChatItemId, ttl :: Maybe Int}
|
||||||
|
-- -> changed to ->
|
||||||
|
APIForwardChatItem {toUserId :: UserId, toChatRef :: ChatRef, fromUserId :: UserId, fromChatRef :: ChatRef, chatItemId :: ChatItemId, ttl :: Maybe Int}
|
||||||
|
-- or include UserId into ChatRef
|
||||||
|
```
|
||||||
|
|
||||||
|
More complex in UI - requires "knowing" conversations for other / all users:
|
||||||
|
- either have all conversations for all users in model.
|
||||||
|
- or have other users expand in forward list, and request their chat lists at that point.
|
||||||
|
|
||||||
|
### Per user network settings
|
||||||
|
|
||||||
|
Requires changes in agent and in backend.
|
||||||
|
|
||||||
|
In agent requires storing network settings in UserId to settings maps, similar to servers:
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
data AgentClient = AgentClient
|
||||||
|
{ ...
|
||||||
|
useNetworkConfig :: TVar (NetworkConfig, NetworkConfig),
|
||||||
|
-- -> changed to ->
|
||||||
|
useNetworkConfig :: TMap UserId (NetworkConfig, NetworkConfig), -- slow/fast per user
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Chat APIs:
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
APISetNetworkConfig NetworkConfig
|
||||||
|
APIGetNetworkConfig
|
||||||
|
-- -> changed to ->
|
||||||
|
APISetNetworkConfig UserId NetworkConfig
|
||||||
|
APIGetNetworkConfig UserId
|
||||||
|
```
|
||||||
|
|
||||||
|
### All chats in united list
|
||||||
|
|
||||||
|
We could add a view where chats for all users could be viewed in a single list / filtered by users.
|
||||||
|
|
||||||
|
We could:
|
||||||
|
- either always load all chats for all users (see Incognito user, Forward between users above) and have a single api, then filter conversations by user in UI
|
||||||
|
- or modify/duplicate APIGetChats api and queries.
|
||||||
|
- in any case may require some rework of pagination queries, as indexes might become inefficient.
|
||||||
|
|
||||||
|
``` haskell
|
||||||
|
APIGetChats {userId :: UserId, pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery}
|
||||||
|
-- -> changed to ->
|
||||||
|
APIGetChats {pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery}
|
||||||
|
-- or
|
||||||
|
APIGetChats {userId :: Maybe UserId, pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery}
|
||||||
|
-- with Nothing meaning all
|
||||||
|
-- or
|
||||||
|
APIGetChats {userIds :: [UserId], pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery}
|
||||||
|
-- to filter for multiple users? or only filter in UI?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Move chats between user profiles
|
||||||
|
|
||||||
|
This would further deepen the illusion of user record being a conversation tag rather than a rigid container for conversations.
|
||||||
|
|
||||||
|
There are some of the same issues as described in migration of incognito conversation settings.
|
||||||
|
|
||||||
|
"Moved" conversation would still be using servers that were configured for the previous user.
|
||||||
|
Perhaps it makes more sense to implement after automated queue rotation.
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 5.8.0.5
|
version: 5.8.1.0
|
||||||
#synopsis:
|
#synopsis:
|
||||||
#description:
|
#description:
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ for ORIG_NAME in "${ORIG_NAMES[@]}"; do
|
|||||||
#(cd apk && 7z a -r -mx=0 -tzip ../$ORIG_NAME resources.arsc)
|
#(cd apk && 7z a -r -mx=0 -tzip ../$ORIG_NAME resources.arsc)
|
||||||
|
|
||||||
ALL_TOOLS=("$sdk_dir"/build-tools/*/)
|
ALL_TOOLS=("$sdk_dir"/build-tools/*/)
|
||||||
BIN_DIR="${ALL_TOOLS[1]}"
|
BIN_DIR="${ALL_TOOLS[${#ALL_TOOLS[@]}-1]}"
|
||||||
|
|
||||||
"$BIN_DIR"/zipalign -p -f 4 "$ORIG_NAME" "$ORIG_NAME"-2
|
"$BIN_DIR"/zipalign -p -f 4 "$ORIG_NAME" "$ORIG_NAME"-2
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"https://github.com/simplex-chat/simplexmq.git"."2e4f507919dd3a08e5c4106fde193fee4414de3c" = "1s035v77rpk8xs8gcfqm2ddp1mf5n35zrkq68gxqr634r2r9vbaj";
|
"https://github.com/simplex-chat/simplexmq.git"."8a3b72458f917e9867f4e3640dda0fa1827ff6cf" = "1mmxdaj563kjmlkacxdnq62n6mzw9khampzaqghnk6iiwzdig0qy";
|
||||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
|||||||
-- see: https://github.com/sol/hpack
|
-- see: https://github.com/sol/hpack
|
||||||
|
|
||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 5.8.0.5
|
version: 5.8.1.0
|
||||||
category: Web, System, Services, Cryptography
|
category: Web, System, Services, Cryptography
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
author: simplex.chat
|
author: simplex.chat
|
||||||
|
|||||||
+131
-83
@@ -93,8 +93,9 @@ import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescr
|
|||||||
import qualified Simplex.FileTransfer.Description as FD
|
import qualified Simplex.FileTransfer.Description as FD
|
||||||
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
|
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
|
||||||
import qualified Simplex.FileTransfer.Transport as XFTP
|
import qualified Simplex.FileTransfer.Transport as XFTP
|
||||||
|
import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId)
|
||||||
import Simplex.Messaging.Agent as Agent
|
import Simplex.Messaging.Agent as Agent
|
||||||
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getNetworkConfig', ipAddressProtected, temporaryAgentError, withLockMap)
|
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getNetworkConfig', ipAddressProtected, withLockMap)
|
||||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
|
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
|
||||||
import Simplex.Messaging.Agent.Lock (withLock)
|
import Simplex.Messaging.Agent.Lock (withLock)
|
||||||
import Simplex.Messaging.Agent.Protocol
|
import Simplex.Messaging.Agent.Protocol
|
||||||
@@ -103,7 +104,7 @@ import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), Migrati
|
|||||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||||
import Simplex.Messaging.Client (ProxyClientError (..), NetworkConfig (..), defaultNetworkConfig)
|
import Simplex.Messaging.Client (NetworkConfig (..), ProxyClientError (..), defaultNetworkConfig)
|
||||||
import qualified Simplex.Messaging.Crypto as C
|
import qualified Simplex.Messaging.Crypto as C
|
||||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
||||||
import qualified Simplex.Messaging.Crypto.File as CF
|
import qualified Simplex.Messaging.Crypto.File as CF
|
||||||
@@ -217,11 +218,12 @@ newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Boo
|
|||||||
newChatController
|
newChatController
|
||||||
ChatDatabase {chatStore, agentStore}
|
ChatDatabase {chatStore, agentStore}
|
||||||
user
|
user
|
||||||
cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote}
|
cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote, confirmMigrations}
|
||||||
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
|
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
|
||||||
backgroundMode = do
|
backgroundMode = do
|
||||||
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
|
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
|
||||||
config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable}
|
confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations
|
||||||
|
config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'}
|
||||||
firstTime = dbNew chatStore
|
firstTime = dbNew chatStore
|
||||||
currentUser <- newTVarIO user
|
currentUser <- newTVarIO user
|
||||||
currentRemoteHost <- newTVarIO Nothing
|
currentRemoteHost <- newTVarIO Nothing
|
||||||
@@ -761,28 +763,31 @@ processChatCommand' vr = \case
|
|||||||
_ -> throwChatError CEInvalidChatItemUpdate
|
_ -> throwChatError CEInvalidChatItemUpdate
|
||||||
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
|
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
|
||||||
CTGroup -> withGroupLock "updateChatItem" chatId $ do
|
CTGroup -> withGroupLock "updateChatItem" chatId $ do
|
||||||
Group gInfo@GroupInfo {groupId} ms <- withStore $ \db -> getGroup db vr user chatId
|
Group gInfo@GroupInfo {groupId, membership} ms <- withStore $ \db -> getGroup db vr user chatId
|
||||||
assertUserGroupRole gInfo GRAuthor
|
assertUserGroupRole gInfo GRAuthor
|
||||||
cci <- withStore $ \db -> getGroupCIWithReactions db user gInfo itemId
|
if prohibitedSimplexLinks gInfo membership mc
|
||||||
case cci of
|
then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText GFSimplexLinks))
|
||||||
CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do
|
else do
|
||||||
case (ciContent, itemSharedMsgId, editable) of
|
cci <- withStore $ \db -> getGroupCIWithReactions db user gInfo itemId
|
||||||
(CISndMsgContent oldMC, Just itemSharedMId, True) -> do
|
case cci of
|
||||||
let changed = mc /= oldMC
|
CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do
|
||||||
if changed || fromMaybe False itemLive
|
case (ciContent, itemSharedMsgId, editable) of
|
||||||
then do
|
(CISndMsgContent oldMC, Just itemSharedMId, True) -> do
|
||||||
(SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive))
|
let changed = mc /= oldMC
|
||||||
ci' <- withStore' $ \db -> do
|
if changed || fromMaybe False itemLive
|
||||||
currentTs <- liftIO getCurrentTime
|
then do
|
||||||
when changed $
|
(SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive))
|
||||||
addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc)
|
ci' <- withStore' $ \db -> do
|
||||||
let edited = itemLive /= Just True
|
currentTs <- liftIO getCurrentTime
|
||||||
updateGroupChatItem db user groupId ci (CISndMsgContent mc) edited live $ Just msgId
|
when changed $
|
||||||
startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci'
|
addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc)
|
||||||
pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci')
|
let edited = itemLive /= Just True
|
||||||
else pure $ CRChatItemNotChanged user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci)
|
updateGroupChatItem db user groupId ci (CISndMsgContent mc) edited live $ Just msgId
|
||||||
_ -> throwChatError CEInvalidChatItemUpdate
|
startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci'
|
||||||
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
|
pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci')
|
||||||
|
else pure $ CRChatItemNotChanged user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci)
|
||||||
|
_ -> throwChatError CEInvalidChatItemUpdate
|
||||||
|
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
|
||||||
CTLocal -> do
|
CTLocal -> do
|
||||||
(nf@NoteFolder {noteFolderId}, cci) <- withStore $ \db -> (,) <$> getNoteFolder db user chatId <*> getLocalChatItem db user chatId itemId
|
(nf@NoteFolder {noteFolderId}, cci) <- withStore $ \db -> (,) <$> getNoteFolder db user chatId <*> getLocalChatItem db user chatId itemId
|
||||||
case cci of
|
case cci of
|
||||||
@@ -1356,6 +1361,9 @@ processChatCommand' vr = \case
|
|||||||
pure $ CRNetworkConfig cfg
|
pure $ CRNetworkConfig cfg
|
||||||
APISetNetworkInfo info -> lift (withAgent' (`setUserNetworkInfo` info)) >> ok_
|
APISetNetworkInfo info -> lift (withAgent' (`setUserNetworkInfo` info)) >> ok_
|
||||||
ReconnectAllServers -> withUser' $ \_ -> lift (withAgent' reconnectAllServers) >> ok_
|
ReconnectAllServers -> withUser' $ \_ -> lift (withAgent' reconnectAllServers) >> ok_
|
||||||
|
ReconnectServer userId srv -> withUserId userId $ \user -> do
|
||||||
|
lift (withAgent' $ \a -> reconnectSMPServer a (aUserId user) srv)
|
||||||
|
ok_
|
||||||
APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user -> case cType of
|
APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user -> case cType of
|
||||||
CTDirect -> do
|
CTDirect -> do
|
||||||
ct <- withStore $ \db -> do
|
ct <- withStore $ \db -> do
|
||||||
@@ -2917,9 +2925,17 @@ prohibitedGroupContent :: GroupInfo -> GroupMember -> MsgContent -> Maybe f -> M
|
|||||||
prohibitedGroupContent gInfo m mc file_
|
prohibitedGroupContent gInfo m mc file_
|
||||||
| isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice
|
| isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice
|
||||||
| not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles
|
| not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles
|
||||||
| not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo) && containsFormat isSimplexLink (parseMarkdown $ msgContentText mc) = Just GFSimplexLinks
|
| prohibitedSimplexLinks gInfo m mc = Just GFSimplexLinks
|
||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
|
|
||||||
|
prohibitedSimplexLinks :: GroupInfo -> GroupMember -> MsgContent -> Bool
|
||||||
|
prohibitedSimplexLinks gInfo m mc =
|
||||||
|
not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo)
|
||||||
|
&& maybe False (any ftIsSimplexLink) (parseMaybeMarkdownList $ msgContentText mc)
|
||||||
|
where
|
||||||
|
ftIsSimplexLink :: FormattedText -> Bool
|
||||||
|
ftIsSimplexLink FormattedText {format} = maybe False isSimplexLink format
|
||||||
|
|
||||||
roundedFDCount :: Int -> Int
|
roundedFDCount :: Int -> Int
|
||||||
roundedFDCount n
|
roundedFDCount n
|
||||||
| n <= 0 = 4
|
| n <= 0 = 4
|
||||||
@@ -3369,8 +3385,8 @@ agentSubscriber = do
|
|||||||
toView' $ CRChatError Nothing $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) Nothing
|
toView' $ CRChatError Nothing $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) Nothing
|
||||||
E.throwIO e
|
E.throwIO e
|
||||||
where
|
where
|
||||||
process :: (ACorrId, EntityId, APartyCmd 'Agent) -> CM' ()
|
process :: (ACorrId, EntityId, AEvt) -> CM' ()
|
||||||
process (corrId, entId, APC e msg) = run $ case e of
|
process (corrId, entId, AEvt e msg) = run $ case e of
|
||||||
SAENone -> processAgentMessageNoConn msg
|
SAENone -> processAgentMessageNoConn msg
|
||||||
SAEConn -> processAgentMessage corrId entId msg
|
SAEConn -> processAgentMessage corrId entId msg
|
||||||
SAERcvFile -> processAgentMsgRcvFile corrId entId msg
|
SAERcvFile -> processAgentMsgRcvFile corrId entId msg
|
||||||
@@ -3684,7 +3700,7 @@ expireChatItems user@User {userId} ttl sync = do
|
|||||||
membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db vr user gInfo
|
membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db vr user gInfo
|
||||||
forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m
|
forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m
|
||||||
|
|
||||||
processAgentMessage :: ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> CM ()
|
processAgentMessage :: ACorrId -> ConnId -> AEvent 'AEConn -> CM ()
|
||||||
processAgentMessage _ connId (DEL_RCVQ srv qId err_) =
|
processAgentMessage _ connId (DEL_RCVQ srv qId err_) =
|
||||||
toView $ CRAgentRcvQueueDeleted (AgentConnId connId) srv (AgentQueueId qId) err_
|
toView $ CRAgentRcvQueueDeleted (AgentConnId connId) srv (AgentQueueId qId) err_
|
||||||
processAgentMessage _ connId DEL_CONN =
|
processAgentMessage _ connId DEL_CONN =
|
||||||
@@ -3713,7 +3729,7 @@ critical a =
|
|||||||
ChatErrorStore SEDBBusyError {message} -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing
|
ChatErrorStore SEDBBusyError {message} -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing
|
||||||
e -> throwError e
|
e -> throwError e
|
||||||
|
|
||||||
processAgentMessageNoConn :: ACommand 'Agent 'AENone -> CM ()
|
processAgentMessageNoConn :: AEvent 'AENone -> CM ()
|
||||||
processAgentMessageNoConn = \case
|
processAgentMessageNoConn = \case
|
||||||
CONNECT p h -> hostEvent $ CRHostConnected p h
|
CONNECT p h -> hostEvent $ CRHostConnected p h
|
||||||
DISCONNECT p h -> hostEvent $ CRHostDisconnected p h
|
DISCONNECT p h -> hostEvent $ CRHostDisconnected p h
|
||||||
@@ -3734,7 +3750,7 @@ processAgentMessageNoConn = \case
|
|||||||
cs <- withStore' (`getConnectionsContacts` conns)
|
cs <- withStore' (`getConnectionsContacts` conns)
|
||||||
toView $ event srv cs
|
toView $ event srv cs
|
||||||
|
|
||||||
processAgentMsgSndFile :: ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> CM ()
|
processAgentMsgSndFile :: ACorrId -> SndFileId -> AEvent 'AESndFile -> CM ()
|
||||||
processAgentMsgSndFile _corrId aFileId msg = do
|
processAgentMsgSndFile _corrId aFileId msg = do
|
||||||
(cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId)
|
(cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId)
|
||||||
withEntityLock_ cRef_ . withFileLock "processAgentMsgSndFile" fileId $
|
withEntityLock_ cRef_ . withFileLock "processAgentMsgSndFile" fileId $
|
||||||
@@ -3768,11 +3784,11 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
|||||||
lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId)
|
lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId)
|
||||||
withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText rfds)
|
withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText rfds)
|
||||||
case rfds of
|
case rfds of
|
||||||
[] -> sendFileError "no receiver descriptions" vr ft
|
[] -> sendFileError (FileErrOther "no receiver descriptions") "no receiver descriptions" vr ft
|
||||||
rfd : _ -> case [fd | fd@(FD.ValidFileDescription FD.FileDescription {chunks = [_]}) <- rfds] of
|
rfd : _ -> case [fd | fd@(FD.ValidFileDescription FD.FileDescription {chunks = [_]}) <- rfds] of
|
||||||
[] -> case xftpRedirectFor of
|
[] -> case xftpRedirectFor of
|
||||||
Nothing -> xftpSndFileRedirect user fileId rfd >>= toView . CRSndFileRedirectStartXFTP user ft
|
Nothing -> xftpSndFileRedirect user fileId rfd >>= toView . CRSndFileRedirectStartXFTP user ft
|
||||||
Just _ -> sendFileError "Prohibit chaining redirects" vr ft
|
Just _ -> sendFileError (FileErrOther "chaining redirects") "Prohibit chaining redirects" vr ft
|
||||||
rfds' -> do
|
rfds' -> do
|
||||||
-- we have 1 chunk - use it as URI whether it is redirect or not
|
-- we have 1 chunk - use it as URI whether it is redirect or not
|
||||||
ft' <- maybe (pure ft) (\fId -> withStore $ \db -> getFileTransferMeta db user fId) xftpRedirectFor
|
ft' <- maybe (pure ft) (\fId -> withStore $ \db -> getFileTransferMeta db user fId) xftpRedirectFor
|
||||||
@@ -3817,11 +3833,15 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
|||||||
pure (sndMsg, msgDeliveryId)
|
pure (sndMsg, msgDeliveryId)
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
_ -> pure () -- TODO error?
|
_ -> pure () -- TODO error?
|
||||||
SFERR e
|
SFWARN e -> do
|
||||||
| temporaryAgentError e ->
|
let err = tshow e
|
||||||
throwChatError $ CEXFTPSndFile fileId (AgentSndFileId aFileId) e
|
logWarn $ "Sent file warning: " <> err
|
||||||
| otherwise ->
|
ci <- withStore $ \db -> do
|
||||||
sendFileError (tshow e) vr ft
|
liftIO $ updateCIFileStatus db user fileId (CIFSSndWarning $ agentFileError e)
|
||||||
|
lookupChatItemByFileId db vr user fileId
|
||||||
|
toView $ CRSndFileWarning user ci ft err
|
||||||
|
SFERR e ->
|
||||||
|
sendFileError (agentFileError e) (tshow e) vr ft
|
||||||
where
|
where
|
||||||
fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text
|
fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text
|
||||||
fileDescrText = safeDecodeUtf8 . strEncode
|
fileDescrText = safeDecodeUtf8 . strEncode
|
||||||
@@ -3839,15 +3859,27 @@ processAgentMsgSndFile _corrId aFileId msg = do
|
|||||||
case L.nonEmpty fds of
|
case L.nonEmpty fds of
|
||||||
Just fds' -> loopSend fds'
|
Just fds' -> loopSend fds'
|
||||||
Nothing -> pure msgDeliveryId
|
Nothing -> pure msgDeliveryId
|
||||||
sendFileError :: Text -> VersionRangeChat -> FileTransferMeta -> CM ()
|
sendFileError :: FileError -> Text -> VersionRangeChat -> FileTransferMeta -> CM ()
|
||||||
sendFileError err vr ft = do
|
sendFileError ferr err vr ft = do
|
||||||
logError $ "Sent file error: " <> err
|
logError $ "Sent file error: " <> err
|
||||||
ci <- withStore $ \db -> do
|
ci <- withStore $ \db -> do
|
||||||
liftIO $ updateFileCancelled db user fileId CIFSSndError
|
liftIO $ updateFileCancelled db user fileId (CIFSSndError ferr)
|
||||||
lookupChatItemByFileId db vr user fileId
|
lookupChatItemByFileId db vr user fileId
|
||||||
lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId)
|
lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId)
|
||||||
toView $ CRSndFileError user ci ft err
|
toView $ CRSndFileError user ci ft err
|
||||||
|
|
||||||
|
agentFileError :: AgentErrorType -> FileError
|
||||||
|
agentFileError = \case
|
||||||
|
XFTP _ XFTP.AUTH -> FileErrAuth
|
||||||
|
FILE NO_FILE -> FileErrNoFile
|
||||||
|
BROKER _ e -> brokerError FileErrRelay e
|
||||||
|
e -> FileErrOther $ tshow e
|
||||||
|
where
|
||||||
|
brokerError srvErr = \case
|
||||||
|
HOST -> srvErr SrvErrHost
|
||||||
|
SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion
|
||||||
|
e -> srvErr . SrvErrOther $ tshow e
|
||||||
|
|
||||||
splitFileDescr :: RcvFileDescrText -> CM (NonEmpty FileDescr)
|
splitFileDescr :: RcvFileDescrText -> CM (NonEmpty FileDescr)
|
||||||
splitFileDescr rfdText = do
|
splitFileDescr rfdText = do
|
||||||
partSize <- asks $ xftpDescrPartSize . config
|
partSize <- asks $ xftpDescrPartSize . config
|
||||||
@@ -3861,7 +3893,7 @@ splitFileDescr rfdText = do
|
|||||||
then fileDescr :| []
|
then fileDescr :| []
|
||||||
else fileDescr <| splitParts (partNo + 1) partSize rest
|
else fileDescr <| splitParts (partNo + 1) partSize rest
|
||||||
|
|
||||||
processAgentMsgRcvFile :: ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> CM ()
|
processAgentMsgRcvFile :: ACorrId -> RcvFileId -> AEvent 'AERcvFile -> CM ()
|
||||||
processAgentMsgRcvFile _corrId aFileId msg = do
|
processAgentMsgRcvFile _corrId aFileId msg = do
|
||||||
(cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId)
|
(cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId)
|
||||||
withEntityLock_ cRef_ . withFileLock "processAgentMsgRcvFile" fileId $
|
withEntityLock_ cRef_ . withFileLock "processAgentMsgRcvFile" fileId $
|
||||||
@@ -3900,21 +3932,24 @@ processAgentMsgRcvFile _corrId aFileId msg = do
|
|||||||
lookupChatItemByFileId db vr user fileId
|
lookupChatItemByFileId db vr user fileId
|
||||||
agentXFTPDeleteRcvFile aFileId fileId
|
agentXFTPDeleteRcvFile aFileId fileId
|
||||||
toView $ maybe (CRRcvStandaloneFileComplete user fsTargetPath ft) (CRRcvFileComplete user) ci_
|
toView $ maybe (CRRcvStandaloneFileComplete user fsTargetPath ft) (CRRcvFileComplete user) ci_
|
||||||
|
RFWARN e -> do
|
||||||
|
ci <- withStore $ \db -> do
|
||||||
|
liftIO $ updateCIFileStatus db user fileId (CIFSRcvWarning $ agentFileError e)
|
||||||
|
lookupChatItemByFileId db vr user fileId
|
||||||
|
toView $ CRRcvFileWarning user ci e ft
|
||||||
RFERR e
|
RFERR e
|
||||||
| temporaryAgentError e ->
|
| e == FILE NOT_APPROVED -> do
|
||||||
throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e
|
|
||||||
| e == XFTP "" XFTP.NOT_APPROVED -> do
|
|
||||||
aci_ <- resetRcvCIFileStatus user fileId CIFSRcvAborted
|
aci_ <- resetRcvCIFileStatus user fileId CIFSRcvAborted
|
||||||
agentXFTPDeleteRcvFile aFileId fileId
|
agentXFTPDeleteRcvFile aFileId fileId
|
||||||
forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci
|
forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci
|
||||||
| otherwise -> do
|
| otherwise -> do
|
||||||
ci <- withStore $ \db -> do
|
ci <- withStore $ \db -> do
|
||||||
liftIO $ updateFileCancelled db user fileId CIFSRcvError
|
liftIO $ updateFileCancelled db user fileId (CIFSRcvError $ agentFileError e)
|
||||||
lookupChatItemByFileId db vr user fileId
|
lookupChatItemByFileId db vr user fileId
|
||||||
agentXFTPDeleteRcvFile aFileId fileId
|
agentXFTPDeleteRcvFile aFileId fileId
|
||||||
toView $ CRRcvFileError user ci e ft
|
toView $ CRRcvFileError user ci e ft
|
||||||
|
|
||||||
processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> CM ()
|
processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> AEvent 'AEConn -> CM ()
|
||||||
processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = do
|
processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = do
|
||||||
-- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert,
|
-- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert,
|
||||||
-- as in this case no need to ACK message - we can't process messages for this connection anyway.
|
-- as in this case no need to ACK message - we can't process messages for this connection anyway.
|
||||||
@@ -3946,7 +3981,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
pure $ updateEntityConnStatus acEntity connStatus
|
pure $ updateEntityConnStatus acEntity connStatus
|
||||||
Nothing -> pure acEntity
|
Nothing -> pure acEntity
|
||||||
|
|
||||||
agentMsgConnStatus :: ACommand 'Agent e -> Maybe ConnStatus
|
agentMsgConnStatus :: AEvent e -> Maybe ConnStatus
|
||||||
agentMsgConnStatus = \case
|
agentMsgConnStatus = \case
|
||||||
CONF {} -> Just ConnRequested
|
CONF {} -> Just ConnRequested
|
||||||
INFO {} -> Just ConnSndReady
|
INFO {} -> Just ConnSndReady
|
||||||
@@ -3968,7 +4003,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
processINFOpqSupport Connection {pqSupport = pq} pq' =
|
processINFOpqSupport Connection {pqSupport = pq} pq' =
|
||||||
when (pq /= pq') $ messageWarning "processINFOpqSupport: unexpected pqSupport change"
|
when (pq /= pq') $ messageWarning "processINFOpqSupport: unexpected pqSupport change"
|
||||||
|
|
||||||
processDirectMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> Maybe Contact -> CM ()
|
processDirectMessage :: AEvent e -> ConnectionEntity -> Connection -> Maybe Contact -> CM ()
|
||||||
processDirectMessage agentMsg connEntity conn@Connection {connId, connChatVersion, peerChatVRange, viaUserContactLink, customUserProfileId, connectionCode} = \case
|
processDirectMessage agentMsg connEntity conn@Connection {connId, connChatVersion, peerChatVRange, viaUserContactLink, customUserProfileId, connectionCode} = \case
|
||||||
Nothing -> case agentMsg of
|
Nothing -> case agentMsg of
|
||||||
CONF confId pqSupport _ connInfo -> do
|
CONF confId pqSupport _ connInfo -> do
|
||||||
@@ -4193,7 +4228,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
-- TODO add debugging output
|
-- TODO add debugging output
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
|
|
||||||
processGroupMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM ()
|
processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM ()
|
||||||
processGroupMessage agentMsg connEntity conn@Connection {connId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of
|
processGroupMessage agentMsg connEntity conn@Connection {connId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of
|
||||||
INV (ACR _ cReq) ->
|
INV (ACR _ cReq) ->
|
||||||
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} ->
|
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} ->
|
||||||
@@ -4621,7 +4656,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
r n'' = Just (ci, CIRcvDecryptionError mde n'')
|
r n'' = Just (ci, CIRcvDecryptionError mde n'')
|
||||||
mdeUpdatedCI _ _ = Nothing
|
mdeUpdatedCI _ _ = Nothing
|
||||||
|
|
||||||
processSndFileConn :: ACommand 'Agent e -> ConnectionEntity -> Connection -> SndFileTransfer -> CM ()
|
processSndFileConn :: AEvent e -> ConnectionEntity -> Connection -> SndFileTransfer -> CM ()
|
||||||
processSndFileConn agentMsg connEntity conn ft@SndFileTransfer {fileId, fileName, fileStatus} =
|
processSndFileConn agentMsg connEntity conn ft@SndFileTransfer {fileId, fileName, fileStatus} =
|
||||||
case agentMsg of
|
case agentMsg of
|
||||||
-- SMP CONF for SndFileConnection happens for direct file protocol
|
-- SMP CONF for SndFileConnection happens for direct file protocol
|
||||||
@@ -4669,7 +4704,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
-- TODO add debugging output
|
-- TODO add debugging output
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
|
|
||||||
processRcvFileConn :: ACommand 'Agent e -> ConnectionEntity -> Connection -> RcvFileTransfer -> CM ()
|
processRcvFileConn :: AEvent e -> ConnectionEntity -> Connection -> RcvFileTransfer -> CM ()
|
||||||
processRcvFileConn agentMsg connEntity conn ft@RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}, grpMemberId} =
|
processRcvFileConn agentMsg connEntity conn ft@RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}, grpMemberId} =
|
||||||
case agentMsg of
|
case agentMsg of
|
||||||
INV (ACR _ cReq) ->
|
INV (ACR _ cReq) ->
|
||||||
@@ -4752,7 +4787,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure ()
|
RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure ()
|
||||||
RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo
|
RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo
|
||||||
|
|
||||||
processUserContactRequest :: ACommand 'Agent e -> ConnectionEntity -> Connection -> UserContact -> CM ()
|
processUserContactRequest :: AEvent e -> ConnectionEntity -> Connection -> UserContact -> CM ()
|
||||||
processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of
|
processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of
|
||||||
REQ invId pqSupport _ connInfo -> do
|
REQ invId pqSupport _ connInfo -> do
|
||||||
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
|
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
|
||||||
@@ -4825,7 +4860,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
unless (connInactive conn) $ do
|
unless (connInactive conn) $ do
|
||||||
quotaErrCounter' <- withStore' $ \db -> incQuotaErrCounter db user conn
|
quotaErrCounter' <- withStore' $ \db -> incQuotaErrCounter db user conn
|
||||||
when (quotaErrCounter' >= quotaErrInactiveCount) $
|
when (quotaErrCounter' >= quotaErrInactiveCount) $
|
||||||
toView $ CRConnectionInactive connEntity True
|
toView $
|
||||||
|
CRConnectionInactive connEntity True
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
|
|
||||||
continueSending :: ConnectionEntity -> Connection -> CM Bool
|
continueSending :: ConnectionEntity -> Connection -> CM Bool
|
||||||
@@ -4839,13 +4875,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
|
|
||||||
-- TODO v5.7 / v6.0 - together with deprecating old group protocol establishing direct connections?
|
-- TODO v5.7 / v6.0 - together with deprecating old group protocol establishing direct connections?
|
||||||
-- we could save command records only for agent APIs we process continuations for (INV)
|
-- we could save command records only for agent APIs we process continuations for (INV)
|
||||||
withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> CM ()) -> CM ()
|
withCompletedCommand :: forall e. AEntityI e => Connection -> AEvent e -> (CommandData -> CM ()) -> CM ()
|
||||||
withCompletedCommand Connection {connId} agentMsg action = do
|
withCompletedCommand Connection {connId} agentMsg action = do
|
||||||
let agentMsgTag = APCT (sAEntity @e) $ aCommandTag agentMsg
|
let agentMsgTag = AEvtTag (sAEntity @e) $ aEventTag agentMsg
|
||||||
cmdData_ <- withStore' $ \db -> getCommandDataByCorrId db user corrId
|
cmdData_ <- withStore' $ \db -> getCommandDataByCorrId db user corrId
|
||||||
case cmdData_ of
|
case cmdData_ of
|
||||||
Just cmdData@CommandData {cmdId, cmdConnId = Just cmdConnId', cmdFunction}
|
Just cmdData@CommandData {cmdId, cmdConnId = Just cmdConnId', cmdFunction}
|
||||||
| connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == APCT SAEConn ERR_) -> do
|
| connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == AEvtTag SAEConn ERR_) -> do
|
||||||
withStore' $ \db -> deleteCommand db user cmdId
|
withStore' $ \db -> deleteCommand db user cmdId
|
||||||
action cmdData
|
action cmdData
|
||||||
| otherwise -> err cmdId $ "not matching connection id or unexpected response, corrId = " <> show corrId
|
| otherwise -> err cmdId $ "not matching connection id or unexpected response, corrId = " <> show corrId
|
||||||
@@ -4909,14 +4945,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
BROKER _ e -> brokerError SndErrRelay e
|
BROKER _ e -> brokerError SndErrRelay e
|
||||||
SMP proxySrv (SMP.PROXY (SMP.BROKER e)) -> brokerError (SndErrProxy proxySrv) e
|
SMP proxySrv (SMP.PROXY (SMP.BROKER e)) -> brokerError (SndErrProxy proxySrv) e
|
||||||
AP.PROXY proxySrv _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> brokerError (SndErrProxyRelay proxySrv) e
|
AP.PROXY proxySrv _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> brokerError (SndErrProxyRelay proxySrv) e
|
||||||
e -> SndErrOther . safeDecodeUtf8 $ strEncode e
|
e -> SndErrOther $ tshow e
|
||||||
where
|
where
|
||||||
brokerError srvErr = \case
|
brokerError srvErr = \case
|
||||||
NETWORK -> SndErrExpired
|
NETWORK -> SndErrExpired
|
||||||
TIMEOUT -> SndErrExpired
|
TIMEOUT -> SndErrExpired
|
||||||
HOST -> srvErr SrvErrHost
|
HOST -> srvErr SrvErrHost
|
||||||
SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion
|
SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion
|
||||||
e -> srvErr . SrvErrOther . safeDecodeUtf8 $ strEncode e
|
e -> srvErr . SrvErrOther $ tshow e
|
||||||
|
|
||||||
badRcvFileChunk :: RcvFileTransfer -> String -> CM ()
|
badRcvFileChunk :: RcvFileTransfer -> String -> CM ()
|
||||||
badRcvFileChunk ft err =
|
badRcvFileChunk ft err =
|
||||||
@@ -5229,18 +5265,21 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
groupMsgToView gInfo ci' {reactions}
|
groupMsgToView gInfo ci' {reactions}
|
||||||
|
|
||||||
groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> CM ()
|
groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> CM ()
|
||||||
groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc msg@RcvMessage {msgId} brokerTs ttl_ live_ =
|
groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc msg@RcvMessage {msgId} brokerTs ttl_ live_
|
||||||
updateRcvChatItem `catchCINotFound` \_ -> do
|
| prohibitedSimplexLinks gInfo m mc =
|
||||||
-- This patches initial sharedMsgId into chat item when locally deleted chat item
|
messageWarning $ "x.msg.update ignored: feature not allowed " <> groupFeatureNameText GFSimplexLinks
|
||||||
-- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete).
|
| otherwise = do
|
||||||
-- Chat item and update message which created it will have different sharedMsgId in this case...
|
updateRcvChatItem `catchCINotFound` \_ -> do
|
||||||
let timed_ = rcvGroupCITimed gInfo ttl_
|
-- This patches initial sharedMsgId into chat item when locally deleted chat item
|
||||||
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) brokerTs content Nothing timed_ live
|
-- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete).
|
||||||
ci' <- withStore' $ \db -> do
|
-- Chat item and update message which created it will have different sharedMsgId in this case...
|
||||||
createChatItemVersion db (chatItemId' ci) brokerTs mc
|
let timed_ = rcvGroupCITimed gInfo ttl_
|
||||||
ci' <- updateGroupChatItem db user groupId ci content True live Nothing
|
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) brokerTs content Nothing timed_ live
|
||||||
blockedMember m ci' $ markGroupChatItemBlocked db user gInfo ci'
|
ci' <- withStore' $ \db -> do
|
||||||
toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci')
|
createChatItemVersion db (chatItemId' ci) brokerTs mc
|
||||||
|
ci' <- updateGroupChatItem db user groupId ci content True live Nothing
|
||||||
|
blockedMember m ci' $ markGroupChatItemBlocked db user gInfo ci'
|
||||||
|
toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci')
|
||||||
where
|
where
|
||||||
content = CIRcvMsgContent mc
|
content = CIRcvMsgContent mc
|
||||||
live = fromMaybe False live_
|
live = fromMaybe False live_
|
||||||
@@ -7048,22 +7087,30 @@ agentXFTPDeleteSndFilesRemote user sndFiles = do
|
|||||||
let redirects' = mapMaybe mapRedirectMeta $ concat redirects
|
let redirects' = mapMaybe mapRedirectMeta $ concat redirects
|
||||||
sndFilesAll = redirects' <> sndFiles
|
sndFilesAll = redirects' <> sndFiles
|
||||||
sndFilesAll' = filter (not . agentSndFileDeleted . fst) sndFilesAll
|
sndFilesAll' = filter (not . agentSndFileDeleted . fst) sndFilesAll
|
||||||
sndFilesAll'' <- catMaybes <$> mapM sndFileDescr sndFilesAll'
|
-- while file is being prepared and uploaded, it would not have description available;
|
||||||
let sfs = map (\(XFTPSndFile {agentSndFileId = AgentSndFileId aFileId}, sfd, _) -> (aFileId, sfd)) sndFilesAll''
|
-- this partitions files into those with and without descriptions -
|
||||||
withAgent' $ \a -> xftpDeleteSndFilesRemote a (aUserId user) sfs
|
-- files with description are deleted remotely, files without description are deleted internally
|
||||||
void . withStoreBatch' $ \db -> map (setSndFTAgentDeleted db user . (\(_, _, fId) -> fId)) sndFilesAll''
|
(sfsNoDescr, sfsWithDescr) <- partitionSndDescr sndFilesAll' [] []
|
||||||
|
withAgent' $ \a -> xftpDeleteSndFilesInternal a sfsNoDescr
|
||||||
|
withAgent' $ \a -> xftpDeleteSndFilesRemote a (aUserId user) sfsWithDescr
|
||||||
|
void . withStoreBatch' $ \db -> map (setSndFTAgentDeleted db user . (\(_, fId) -> fId)) sndFilesAll'
|
||||||
where
|
where
|
||||||
mapRedirectMeta :: FileTransferMeta -> Maybe (XFTPSndFile, FileTransferId)
|
mapRedirectMeta :: FileTransferMeta -> Maybe (XFTPSndFile, FileTransferId)
|
||||||
mapRedirectMeta FileTransferMeta {fileId = fileId, xftpSndFile = Just sndFileRedirect} = Just (sndFileRedirect, fileId)
|
mapRedirectMeta FileTransferMeta {fileId = fileId, xftpSndFile = Just sndFileRedirect} = Just (sndFileRedirect, fileId)
|
||||||
mapRedirectMeta _ = Nothing
|
mapRedirectMeta _ = Nothing
|
||||||
sndFileDescr :: (XFTPSndFile, FileTransferId) -> CM' (Maybe (XFTPSndFile, ValidFileDescription 'FSender, FileTransferId))
|
partitionSndDescr ::
|
||||||
sndFileDescr (xsf@XFTPSndFile {privateSndFileDescr}, fileId) =
|
[(XFTPSndFile, FileTransferId)] ->
|
||||||
join <$> forM privateSndFileDescr parseSndDescr
|
[SndFileId] ->
|
||||||
where
|
[(SndFileId, ValidFileDescription 'FSender)] ->
|
||||||
parseSndDescr sfdText =
|
CM' ([SndFileId], [(SndFileId, ValidFileDescription 'FSender)])
|
||||||
|
partitionSndDescr [] filesWithoutDescr filesWithDescr = pure (filesWithoutDescr, filesWithDescr)
|
||||||
|
partitionSndDescr ((XFTPSndFile {agentSndFileId = AgentSndFileId aFileId, privateSndFileDescr}, _) : xsfs) filesWithoutDescr filesWithDescr =
|
||||||
|
case privateSndFileDescr of
|
||||||
|
Nothing -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr
|
||||||
|
Just sfdText ->
|
||||||
tryChatError' (parseFileDescription sfdText) >>= \case
|
tryChatError' (parseFileDescription sfdText) >>= \case
|
||||||
Left _ -> pure Nothing
|
Left _ -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr
|
||||||
Right sd -> pure $ Just (xsf, sd, fileId)
|
Right sfd -> partitionSndDescr xsfs filesWithoutDescr ((aFileId, sfd) : filesWithDescr)
|
||||||
|
|
||||||
userProfileToSend :: User -> Maybe Profile -> Maybe Contact -> Bool -> Profile
|
userProfileToSend :: User -> Maybe Profile -> Maybe Contact -> Bool -> Profile
|
||||||
userProfileToSend user@User {profile = p} incognitoProfile ct inGroup = do
|
userProfileToSend user@User {profile = p} incognitoProfile ct inGroup = do
|
||||||
@@ -7381,6 +7428,7 @@ chatCommandP =
|
|||||||
"/_network " *> (APISetNetworkConfig <$> jsonP),
|
"/_network " *> (APISetNetworkConfig <$> jsonP),
|
||||||
("/network " <|> "/net ") *> (SetNetworkConfig <$> netCfgP),
|
("/network " <|> "/net ") *> (SetNetworkConfig <$> netCfgP),
|
||||||
("/network" <|> "/net") $> APIGetNetworkConfig,
|
("/network" <|> "/net") $> APIGetNetworkConfig,
|
||||||
|
"/reconnect " *> (ReconnectServer <$> A.decimal <* A.space <*> strP),
|
||||||
"/reconnect" $> ReconnectAllServers,
|
"/reconnect" $> ReconnectAllServers,
|
||||||
"/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP),
|
"/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP),
|
||||||
"/_member settings #" *> (APISetMemberSettings <$> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP),
|
"/_member settings #" *> (APISetMemberSettings <$> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP),
|
||||||
|
|||||||
@@ -355,6 +355,7 @@ data ChatCommand
|
|||||||
| SetNetworkConfig SimpleNetCfg
|
| SetNetworkConfig SimpleNetCfg
|
||||||
| APISetNetworkInfo UserNetworkInfo
|
| APISetNetworkInfo UserNetworkInfo
|
||||||
| ReconnectAllServers
|
| ReconnectAllServers
|
||||||
|
| ReconnectServer UserId SMPServer
|
||||||
| APISetChatSettings ChatRef ChatSettings
|
| APISetChatSettings ChatRef ChatSettings
|
||||||
| APISetMemberSettings GroupId GroupMemberId GroupMemberSettings
|
| APISetMemberSettings GroupId GroupMemberId GroupMemberSettings
|
||||||
| APIContactInfo ContactId
|
| APIContactInfo ContactId
|
||||||
@@ -654,6 +655,7 @@ data ChatResponse
|
|||||||
| CRRcvFileCancelled {user :: User, chatItem_ :: Maybe AChatItem, rcvFileTransfer :: RcvFileTransfer}
|
| CRRcvFileCancelled {user :: User, chatItem_ :: Maybe AChatItem, rcvFileTransfer :: RcvFileTransfer}
|
||||||
| CRRcvFileSndCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer}
|
| CRRcvFileSndCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer}
|
||||||
| CRRcvFileError {user :: User, chatItem_ :: Maybe AChatItem, agentError :: AgentErrorType, rcvFileTransfer :: RcvFileTransfer}
|
| CRRcvFileError {user :: User, chatItem_ :: Maybe AChatItem, agentError :: AgentErrorType, rcvFileTransfer :: RcvFileTransfer}
|
||||||
|
| CRRcvFileWarning {user :: User, chatItem_ :: Maybe AChatItem, agentError :: AgentErrorType, rcvFileTransfer :: RcvFileTransfer}
|
||||||
| CRSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
| CRSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||||
| CRSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
| CRSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||||
| CRSndFileRcvCancelled {user :: User, chatItem_ :: Maybe AChatItem, sndFileTransfer :: SndFileTransfer}
|
| CRSndFileRcvCancelled {user :: User, chatItem_ :: Maybe AChatItem, sndFileTransfer :: SndFileTransfer}
|
||||||
@@ -666,6 +668,7 @@ data ChatResponse
|
|||||||
| CRSndStandaloneFileComplete {user :: User, fileTransferMeta :: FileTransferMeta, rcvURIs :: [Text]}
|
| CRSndStandaloneFileComplete {user :: User, fileTransferMeta :: FileTransferMeta, rcvURIs :: [Text]}
|
||||||
| CRSndFileCancelledXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta}
|
| CRSndFileCancelledXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta}
|
||||||
| CRSndFileError {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text}
|
| CRSndFileError {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text}
|
||||||
|
| CRSndFileWarning {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text}
|
||||||
| CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile, updateSummary :: UserProfileUpdateSummary}
|
| CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile, updateSummary :: UserProfileUpdateSummary}
|
||||||
| CRUserProfileImage {user :: User, profile :: Profile}
|
| CRUserProfileImage {user :: User, profile :: Profile}
|
||||||
| CRContactAliasUpdated {user :: User, toContact :: Contact}
|
| CRContactAliasUpdated {user :: User, toContact :: Contact}
|
||||||
@@ -1157,8 +1160,6 @@ data ChatErrorType
|
|||||||
| CEFileImageSize {filePath :: FilePath}
|
| CEFileImageSize {filePath :: FilePath}
|
||||||
| CEFileNotReceived {fileId :: FileTransferId}
|
| CEFileNotReceived {fileId :: FileTransferId}
|
||||||
| CEFileNotApproved {fileId :: FileTransferId, unknownServers :: [XFTPServer]}
|
| CEFileNotApproved {fileId :: FileTransferId, unknownServers :: [XFTPServer]}
|
||||||
| CEXFTPRcvFile {fileId :: FileTransferId, agentRcvFileId :: AgentRcvFileId, agentError :: AgentErrorType}
|
|
||||||
| CEXFTPSndFile {fileId :: FileTransferId, agentSndFileId :: AgentSndFileId, agentError :: AgentErrorType}
|
|
||||||
| CEFallbackToSMPProhibited {fileId :: FileTransferId}
|
| CEFallbackToSMPProhibited {fileId :: FileTransferId}
|
||||||
| CEInlineFileProhibited {fileId :: FileTransferId}
|
| CEInlineFileProhibited {fileId :: FileTransferId}
|
||||||
| CEInvalidQuote
|
| CEInvalidQuote
|
||||||
|
|||||||
@@ -144,10 +144,6 @@ markdownToList (m1 :|: m2) = markdownToList m1 <> markdownToList m2
|
|||||||
parseMarkdown :: Text -> Markdown
|
parseMarkdown :: Text -> Markdown
|
||||||
parseMarkdown s = fromRight (unmarked s) $ A.parseOnly (markdownP <* A.endOfInput) s
|
parseMarkdown s = fromRight (unmarked s) $ A.parseOnly (markdownP <* A.endOfInput) s
|
||||||
|
|
||||||
containsFormat :: (Format -> Bool) -> Markdown -> Bool
|
|
||||||
containsFormat p (Markdown f _) = maybe False p f
|
|
||||||
containsFormat p (m1 :|: m2) = containsFormat p m1 || containsFormat p m2
|
|
||||||
|
|
||||||
isSimplexLink :: Format -> Bool
|
isSimplexLink :: Format -> Bool
|
||||||
isSimplexLink = \case
|
isSimplexLink = \case
|
||||||
SimplexLink {} -> True;
|
SimplexLink {} -> True;
|
||||||
|
|||||||
@@ -536,14 +536,16 @@ data CIFileStatus (d :: MsgDirection) where
|
|||||||
CIFSSndTransfer :: {sndProgress :: Int64, sndTotal :: Int64} -> CIFileStatus 'MDSnd
|
CIFSSndTransfer :: {sndProgress :: Int64, sndTotal :: Int64} -> CIFileStatus 'MDSnd
|
||||||
CIFSSndCancelled :: CIFileStatus 'MDSnd
|
CIFSSndCancelled :: CIFileStatus 'MDSnd
|
||||||
CIFSSndComplete :: CIFileStatus 'MDSnd
|
CIFSSndComplete :: CIFileStatus 'MDSnd
|
||||||
CIFSSndError :: CIFileStatus 'MDSnd
|
CIFSSndError :: {sndFileError :: FileError} -> CIFileStatus 'MDSnd
|
||||||
|
CIFSSndWarning :: {sndFileError :: FileError} -> CIFileStatus 'MDSnd
|
||||||
CIFSRcvInvitation :: CIFileStatus 'MDRcv
|
CIFSRcvInvitation :: CIFileStatus 'MDRcv
|
||||||
CIFSRcvAccepted :: CIFileStatus 'MDRcv
|
CIFSRcvAccepted :: CIFileStatus 'MDRcv
|
||||||
CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv
|
CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv
|
||||||
CIFSRcvAborted :: CIFileStatus 'MDRcv
|
CIFSRcvAborted :: CIFileStatus 'MDRcv
|
||||||
CIFSRcvComplete :: CIFileStatus 'MDRcv
|
CIFSRcvComplete :: CIFileStatus 'MDRcv
|
||||||
CIFSRcvCancelled :: CIFileStatus 'MDRcv
|
CIFSRcvCancelled :: CIFileStatus 'MDRcv
|
||||||
CIFSRcvError :: CIFileStatus 'MDRcv
|
CIFSRcvError :: {rcvFileError :: FileError} -> CIFileStatus 'MDRcv
|
||||||
|
CIFSRcvWarning :: {rcvFileError :: FileError} -> CIFileStatus 'MDRcv
|
||||||
CIFSInvalid :: {text :: Text} -> CIFileStatus 'MDSnd
|
CIFSInvalid :: {text :: Text} -> CIFileStatus 'MDSnd
|
||||||
|
|
||||||
deriving instance Eq (CIFileStatus d)
|
deriving instance Eq (CIFileStatus d)
|
||||||
@@ -556,14 +558,16 @@ ciFileEnded = \case
|
|||||||
CIFSSndTransfer {} -> False
|
CIFSSndTransfer {} -> False
|
||||||
CIFSSndCancelled -> True
|
CIFSSndCancelled -> True
|
||||||
CIFSSndComplete -> True
|
CIFSSndComplete -> True
|
||||||
CIFSSndError -> True
|
CIFSSndError {} -> True
|
||||||
|
CIFSSndWarning {} -> False
|
||||||
CIFSRcvInvitation -> False
|
CIFSRcvInvitation -> False
|
||||||
CIFSRcvAccepted -> False
|
CIFSRcvAccepted -> False
|
||||||
CIFSRcvTransfer {} -> False
|
CIFSRcvTransfer {} -> False
|
||||||
CIFSRcvAborted -> True
|
CIFSRcvAborted -> True
|
||||||
CIFSRcvCancelled -> True
|
CIFSRcvCancelled -> True
|
||||||
CIFSRcvComplete -> True
|
CIFSRcvComplete -> True
|
||||||
CIFSRcvError -> True
|
CIFSRcvError {} -> True
|
||||||
|
CIFSRcvWarning {} -> False
|
||||||
CIFSInvalid {} -> True
|
CIFSInvalid {} -> True
|
||||||
|
|
||||||
ciFileLoaded :: CIFileStatus d -> Bool
|
ciFileLoaded :: CIFileStatus d -> Bool
|
||||||
@@ -572,14 +576,16 @@ ciFileLoaded = \case
|
|||||||
CIFSSndTransfer {} -> True
|
CIFSSndTransfer {} -> True
|
||||||
CIFSSndComplete -> True
|
CIFSSndComplete -> True
|
||||||
CIFSSndCancelled -> True
|
CIFSSndCancelled -> True
|
||||||
CIFSSndError -> True
|
CIFSSndError {} -> True
|
||||||
|
CIFSSndWarning {} -> True
|
||||||
CIFSRcvInvitation -> False
|
CIFSRcvInvitation -> False
|
||||||
CIFSRcvAccepted -> False
|
CIFSRcvAccepted -> False
|
||||||
CIFSRcvTransfer {} -> False
|
CIFSRcvTransfer {} -> False
|
||||||
CIFSRcvAborted -> False
|
CIFSRcvAborted -> False
|
||||||
CIFSRcvCancelled -> False
|
CIFSRcvCancelled -> False
|
||||||
CIFSRcvComplete -> True
|
CIFSRcvComplete -> True
|
||||||
CIFSRcvError -> False
|
CIFSRcvError {} -> False
|
||||||
|
CIFSRcvWarning {} -> False
|
||||||
CIFSInvalid {} -> False
|
CIFSInvalid {} -> False
|
||||||
|
|
||||||
data ACIFileStatus = forall d. MsgDirectionI d => AFS (SMsgDirection d) (CIFileStatus d)
|
data ACIFileStatus = forall d. MsgDirectionI d => AFS (SMsgDirection d) (CIFileStatus d)
|
||||||
@@ -592,14 +598,16 @@ instance MsgDirectionI d => StrEncoding (CIFileStatus d) where
|
|||||||
CIFSSndTransfer sent total -> strEncode (Str "snd_transfer", sent, total)
|
CIFSSndTransfer sent total -> strEncode (Str "snd_transfer", sent, total)
|
||||||
CIFSSndCancelled -> "snd_cancelled"
|
CIFSSndCancelled -> "snd_cancelled"
|
||||||
CIFSSndComplete -> "snd_complete"
|
CIFSSndComplete -> "snd_complete"
|
||||||
CIFSSndError -> "snd_error"
|
CIFSSndError sndFileErr -> "snd_error " <> strEncode sndFileErr
|
||||||
|
CIFSSndWarning sndFileErr -> "snd_warning " <> strEncode sndFileErr
|
||||||
CIFSRcvInvitation -> "rcv_invitation"
|
CIFSRcvInvitation -> "rcv_invitation"
|
||||||
CIFSRcvAccepted -> "rcv_accepted"
|
CIFSRcvAccepted -> "rcv_accepted"
|
||||||
CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total)
|
CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total)
|
||||||
CIFSRcvAborted -> "rcv_aborted"
|
CIFSRcvAborted -> "rcv_aborted"
|
||||||
CIFSRcvComplete -> "rcv_complete"
|
CIFSRcvComplete -> "rcv_complete"
|
||||||
CIFSRcvCancelled -> "rcv_cancelled"
|
CIFSRcvCancelled -> "rcv_cancelled"
|
||||||
CIFSRcvError -> "rcv_error"
|
CIFSRcvError rcvFileErr -> "rcv_error " <> strEncode rcvFileErr
|
||||||
|
CIFSRcvWarning rcvFileErr -> "rcv_warning " <> strEncode rcvFileErr
|
||||||
CIFSInvalid {} -> "invalid"
|
CIFSInvalid {} -> "invalid"
|
||||||
strP = (\(AFS _ st) -> checkDirection st) <$?> strP
|
strP = (\(AFS _ st) -> checkDirection st) <$?> strP
|
||||||
|
|
||||||
@@ -615,14 +623,16 @@ instance StrEncoding ACIFileStatus where
|
|||||||
"snd_transfer" -> AFS SMDSnd <$> progress CIFSSndTransfer
|
"snd_transfer" -> AFS SMDSnd <$> progress CIFSSndTransfer
|
||||||
"snd_cancelled" -> pure $ AFS SMDSnd CIFSSndCancelled
|
"snd_cancelled" -> pure $ AFS SMDSnd CIFSSndCancelled
|
||||||
"snd_complete" -> pure $ AFS SMDSnd CIFSSndComplete
|
"snd_complete" -> pure $ AFS SMDSnd CIFSSndComplete
|
||||||
"snd_error" -> pure $ AFS SMDSnd CIFSSndError
|
"snd_error" -> AFS SMDSnd . CIFSSndError <$> ((A.space *> strP) <|> pure (FileErrOther "")) -- alternative for backwards compatibility
|
||||||
|
"snd_warning" -> AFS SMDSnd . CIFSSndWarning <$> (A.space *> strP)
|
||||||
"rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation
|
"rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation
|
||||||
"rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted
|
"rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted
|
||||||
"rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer
|
"rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer
|
||||||
"rcv_aborted" -> pure $ AFS SMDRcv CIFSRcvAborted
|
"rcv_aborted" -> pure $ AFS SMDRcv CIFSRcvAborted
|
||||||
"rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete
|
"rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete
|
||||||
"rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled
|
"rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled
|
||||||
"rcv_error" -> pure $ AFS SMDRcv CIFSRcvError
|
"rcv_error" -> AFS SMDRcv . CIFSRcvError <$> ((A.space *> strP) <|> pure (FileErrOther "")) -- alternative for backwards compatibility
|
||||||
|
"rcv_warning" -> AFS SMDRcv . CIFSRcvWarning <$> (A.space *> strP)
|
||||||
_ -> fail "bad file status"
|
_ -> fail "bad file status"
|
||||||
progress :: (Int64 -> Int64 -> a) -> A.Parser a
|
progress :: (Int64 -> Int64 -> a) -> A.Parser a
|
||||||
progress f = f <$> num <*> num <|> pure (f 0 1)
|
progress f = f <$> num <*> num <|> pure (f 0 1)
|
||||||
@@ -633,14 +643,16 @@ data JSONCIFileStatus
|
|||||||
| JCIFSSndTransfer {sndProgress :: Int64, sndTotal :: Int64}
|
| JCIFSSndTransfer {sndProgress :: Int64, sndTotal :: Int64}
|
||||||
| JCIFSSndCancelled
|
| JCIFSSndCancelled
|
||||||
| JCIFSSndComplete
|
| JCIFSSndComplete
|
||||||
| JCIFSSndError
|
| JCIFSSndError {sndFileError :: FileError}
|
||||||
|
| JCIFSSndWarning {sndFileError :: FileError}
|
||||||
| JCIFSRcvInvitation
|
| JCIFSRcvInvitation
|
||||||
| JCIFSRcvAccepted
|
| JCIFSRcvAccepted
|
||||||
| JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64}
|
| JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64}
|
||||||
| JCIFSRcvAborted
|
| JCIFSRcvAborted
|
||||||
| JCIFSRcvComplete
|
| JCIFSRcvComplete
|
||||||
| JCIFSRcvCancelled
|
| JCIFSRcvCancelled
|
||||||
| JCIFSRcvError
|
| JCIFSRcvError {rcvFileError :: FileError}
|
||||||
|
| JCIFSRcvWarning {rcvFileError :: FileError}
|
||||||
| JCIFSInvalid {text :: Text}
|
| JCIFSInvalid {text :: Text}
|
||||||
|
|
||||||
jsonCIFileStatus :: CIFileStatus d -> JSONCIFileStatus
|
jsonCIFileStatus :: CIFileStatus d -> JSONCIFileStatus
|
||||||
@@ -649,14 +661,16 @@ jsonCIFileStatus = \case
|
|||||||
CIFSSndTransfer sent total -> JCIFSSndTransfer sent total
|
CIFSSndTransfer sent total -> JCIFSSndTransfer sent total
|
||||||
CIFSSndCancelled -> JCIFSSndCancelled
|
CIFSSndCancelled -> JCIFSSndCancelled
|
||||||
CIFSSndComplete -> JCIFSSndComplete
|
CIFSSndComplete -> JCIFSSndComplete
|
||||||
CIFSSndError -> JCIFSSndError
|
CIFSSndError sndFileErr -> JCIFSSndError sndFileErr
|
||||||
|
CIFSSndWarning sndFileErr -> JCIFSSndWarning sndFileErr
|
||||||
CIFSRcvInvitation -> JCIFSRcvInvitation
|
CIFSRcvInvitation -> JCIFSRcvInvitation
|
||||||
CIFSRcvAccepted -> JCIFSRcvAccepted
|
CIFSRcvAccepted -> JCIFSRcvAccepted
|
||||||
CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total
|
CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total
|
||||||
CIFSRcvAborted -> JCIFSRcvAborted
|
CIFSRcvAborted -> JCIFSRcvAborted
|
||||||
CIFSRcvComplete -> JCIFSRcvComplete
|
CIFSRcvComplete -> JCIFSRcvComplete
|
||||||
CIFSRcvCancelled -> JCIFSRcvCancelled
|
CIFSRcvCancelled -> JCIFSRcvCancelled
|
||||||
CIFSRcvError -> JCIFSRcvError
|
CIFSRcvError rcvFileErr -> JCIFSRcvError rcvFileErr
|
||||||
|
CIFSRcvWarning rcvFileErr -> JCIFSRcvWarning rcvFileErr
|
||||||
CIFSInvalid text -> JCIFSInvalid text
|
CIFSInvalid text -> JCIFSInvalid text
|
||||||
|
|
||||||
aciFileStatusJSON :: JSONCIFileStatus -> ACIFileStatus
|
aciFileStatusJSON :: JSONCIFileStatus -> ACIFileStatus
|
||||||
@@ -665,16 +679,39 @@ aciFileStatusJSON = \case
|
|||||||
JCIFSSndTransfer sent total -> AFS SMDSnd $ CIFSSndTransfer sent total
|
JCIFSSndTransfer sent total -> AFS SMDSnd $ CIFSSndTransfer sent total
|
||||||
JCIFSSndCancelled -> AFS SMDSnd CIFSSndCancelled
|
JCIFSSndCancelled -> AFS SMDSnd CIFSSndCancelled
|
||||||
JCIFSSndComplete -> AFS SMDSnd CIFSSndComplete
|
JCIFSSndComplete -> AFS SMDSnd CIFSSndComplete
|
||||||
JCIFSSndError -> AFS SMDSnd CIFSSndError
|
JCIFSSndError sndFileErr -> AFS SMDSnd (CIFSSndError sndFileErr)
|
||||||
|
JCIFSSndWarning sndFileErr -> AFS SMDSnd (CIFSSndWarning sndFileErr)
|
||||||
JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation
|
JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation
|
||||||
JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted
|
JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted
|
||||||
JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total
|
JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total
|
||||||
JCIFSRcvAborted -> AFS SMDRcv CIFSRcvAborted
|
JCIFSRcvAborted -> AFS SMDRcv CIFSRcvAborted
|
||||||
JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete
|
JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete
|
||||||
JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled
|
JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled
|
||||||
JCIFSRcvError -> AFS SMDRcv CIFSRcvError
|
JCIFSRcvError rcvFileErr -> AFS SMDRcv (CIFSRcvError rcvFileErr)
|
||||||
|
JCIFSRcvWarning rcvFileErr -> AFS SMDRcv (CIFSRcvWarning rcvFileErr)
|
||||||
JCIFSInvalid text -> AFS SMDSnd $ CIFSInvalid text
|
JCIFSInvalid text -> AFS SMDSnd $ CIFSInvalid text
|
||||||
|
|
||||||
|
data FileError
|
||||||
|
= FileErrAuth
|
||||||
|
| FileErrNoFile
|
||||||
|
| FileErrRelay {srvError :: SrvError}
|
||||||
|
| FileErrOther {fileError :: Text}
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
instance StrEncoding FileError where
|
||||||
|
strEncode = \case
|
||||||
|
FileErrAuth -> "auth"
|
||||||
|
FileErrNoFile -> "no_file"
|
||||||
|
FileErrRelay srvErr -> "relay " <> strEncode srvErr
|
||||||
|
FileErrOther e -> "other " <> encodeUtf8 e
|
||||||
|
strP =
|
||||||
|
A.takeWhile1 (/= ' ') >>= \case
|
||||||
|
"auth" -> pure FileErrAuth
|
||||||
|
"no_file" -> pure FileErrNoFile
|
||||||
|
"relay" -> FileErrRelay <$> (A.space *> strP)
|
||||||
|
"other" -> FileErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString)
|
||||||
|
s -> FileErrOther . safeDecodeUtf8 . (s <>) <$> A.takeByteString
|
||||||
|
|
||||||
-- to conveniently read file data from db
|
-- to conveniently read file data from db
|
||||||
data CIFileInfo = CIFileInfo
|
data CIFileInfo = CIFileInfo
|
||||||
{ fileId :: Int64,
|
{ fileId :: Int64,
|
||||||
@@ -1208,6 +1245,8 @@ instance ChatTypeI c => ToJSON (CIMeta c d) where
|
|||||||
toJSON = $(JQ.mkToJSON defaultJSON ''CIMeta)
|
toJSON = $(JQ.mkToJSON defaultJSON ''CIMeta)
|
||||||
toEncoding = $(JQ.mkToEncoding defaultJSON ''CIMeta)
|
toEncoding = $(JQ.mkToEncoding defaultJSON ''CIMeta)
|
||||||
|
|
||||||
|
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FileErr") ''FileError)
|
||||||
|
|
||||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCIFS") ''JSONCIFileStatus)
|
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCIFS") ''JSONCIFileStatus)
|
||||||
|
|
||||||
instance MsgDirectionI d => FromJSON (CIFileStatus d) where
|
instance MsgDirectionI d => FromJSON (CIFileStatus d) where
|
||||||
|
|||||||
@@ -198,7 +198,8 @@ mobileChatOpts dbFilePrefix =
|
|||||||
logAgent = Nothing,
|
logAgent = Nothing,
|
||||||
logFile = Nothing,
|
logFile = Nothing,
|
||||||
tbqSize = 1024,
|
tbqSize = 1024,
|
||||||
highlyAvailable = False
|
highlyAvailable = False,
|
||||||
|
yesToUpMigrations = False
|
||||||
},
|
},
|
||||||
deviceName = Nothing,
|
deviceName = Nothing,
|
||||||
chatCmd = "",
|
chatCmd = "",
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import Numeric.Natural (Natural)
|
|||||||
import Options.Applicative
|
import Options.Applicative
|
||||||
import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString)
|
import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString)
|
||||||
import Simplex.FileTransfer.Description (mb)
|
import Simplex.FileTransfer.Description (mb)
|
||||||
import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback (..))
|
|
||||||
import Simplex.Messaging.Encoding.String
|
import Simplex.Messaging.Encoding.String
|
||||||
import Simplex.Messaging.Parsers (parseAll)
|
import Simplex.Messaging.Parsers (parseAll)
|
||||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth)
|
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth)
|
||||||
@@ -63,7 +62,8 @@ data CoreChatOpts = CoreChatOpts
|
|||||||
logAgent :: Maybe LogLevel,
|
logAgent :: Maybe LogLevel,
|
||||||
logFile :: Maybe FilePath,
|
logFile :: Maybe FilePath,
|
||||||
tbqSize :: Natural,
|
tbqSize :: Natural,
|
||||||
highlyAvailable :: Bool
|
highlyAvailable :: Bool,
|
||||||
|
yesToUpMigrations :: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
data ChatCmdLog = CCLAll | CCLMessages | CCLNone
|
data ChatCmdLog = CCLAll | CCLMessages | CCLNone
|
||||||
@@ -205,6 +205,12 @@ coreChatOptsP appDir defaultDbFileName = do
|
|||||||
( long "ha"
|
( long "ha"
|
||||||
<> help "Run as a highly available client (this may increase traffic in groups)"
|
<> help "Run as a highly available client (this may increase traffic in groups)"
|
||||||
)
|
)
|
||||||
|
yesToUpMigrations <-
|
||||||
|
switch
|
||||||
|
( long "--yes-migrate"
|
||||||
|
<> short 'y'
|
||||||
|
<> help "Automatically confirm \"up\" database migrations"
|
||||||
|
)
|
||||||
pure
|
pure
|
||||||
CoreChatOpts
|
CoreChatOpts
|
||||||
{ dbFilePrefix,
|
{ dbFilePrefix,
|
||||||
@@ -218,7 +224,8 @@ coreChatOptsP appDir defaultDbFileName = do
|
|||||||
logAgent = if logAgent || logLevel == CLLDebug then Just $ agentLogLevel logLevel else Nothing,
|
logAgent = if logAgent || logLevel == CLLDebug then Just $ agentLogLevel logLevel else Nothing,
|
||||||
logFile,
|
logFile,
|
||||||
tbqSize,
|
tbqSize,
|
||||||
highlyAvailable
|
highlyAvailable,
|
||||||
|
yesToUpMigrations
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 7 (const 15) p
|
useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 7 (const 15) p
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ import Simplex.Chat.Types.Shared
|
|||||||
import Simplex.Chat.Types.UITheme
|
import Simplex.Chat.Types.UITheme
|
||||||
import Simplex.Chat.Types.Util
|
import Simplex.Chat.Types.Util
|
||||||
import Simplex.FileTransfer.Description (FileDigest)
|
import Simplex.FileTransfer.Description (FileDigest)
|
||||||
import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, RcvFileId, SAEntity (..), SndFileId, UserId)
|
import Simplex.FileTransfer.Types (RcvFileId, SndFileId)
|
||||||
|
import Simplex.Messaging.Agent.Protocol (ACorrId, AEventTag (..), AEvtTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId)
|
||||||
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
|
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
|
||||||
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff)
|
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff)
|
||||||
import Simplex.Messaging.Encoding.String
|
import Simplex.Messaging.Encoding.String
|
||||||
@@ -1582,7 +1583,7 @@ instance TextEncoding CommandFunction where
|
|||||||
CFAckMessage -> "ack_message"
|
CFAckMessage -> "ack_message"
|
||||||
CFDeleteConn -> "delete_conn"
|
CFDeleteConn -> "delete_conn"
|
||||||
|
|
||||||
commandExpectedResponse :: CommandFunction -> APartyCmdTag 'Agent
|
commandExpectedResponse :: CommandFunction -> AEvtTag
|
||||||
commandExpectedResponse = \case
|
commandExpectedResponse = \case
|
||||||
CFCreateConnGrpMemInv -> t INV_
|
CFCreateConnGrpMemInv -> t INV_
|
||||||
CFCreateConnGrpInv -> t INV_
|
CFCreateConnGrpInv -> t INV_
|
||||||
@@ -1594,7 +1595,7 @@ commandExpectedResponse = \case
|
|||||||
CFAckMessage -> t OK_
|
CFAckMessage -> t OK_
|
||||||
CFDeleteConn -> t OK_
|
CFDeleteConn -> t OK_
|
||||||
where
|
where
|
||||||
t = APCT SAEConn
|
t = AEvtTag SAEConn
|
||||||
|
|
||||||
data CommandData = CommandData
|
data CommandData = CommandData
|
||||||
{ cmdId :: CommandId,
|
{ cmdId :: CommandId,
|
||||||
|
|||||||
+21
-10
@@ -54,8 +54,9 @@ import qualified Simplex.FileTransfer.Transport as XFTP
|
|||||||
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..))
|
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..))
|
||||||
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
|
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
|
||||||
import Simplex.Messaging.Agent.Protocol
|
import Simplex.Messaging.Agent.Protocol
|
||||||
|
import Simplex.Messaging.Agent.Protocol (AgentErrorType (RCP))
|
||||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||||
import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback)
|
import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..))
|
||||||
import qualified Simplex.Messaging.Crypto as C
|
import qualified Simplex.Messaging.Crypto as C
|
||||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
||||||
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
||||||
@@ -65,9 +66,9 @@ import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON)
|
|||||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtoServerWithAuth, ProtocolServer (..), ProtocolTypeI, SProtocolType (..))
|
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtoServerWithAuth, ProtocolServer (..), ProtocolTypeI, SProtocolType (..))
|
||||||
import qualified Simplex.Messaging.Protocol as SMP
|
import qualified Simplex.Messaging.Protocol as SMP
|
||||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||||
import Simplex.Messaging.Util (bshow, safeDecodeUtf8, tshow)
|
import Simplex.Messaging.Util (safeDecodeUtf8, tshow)
|
||||||
import Simplex.Messaging.Version hiding (version)
|
import Simplex.Messaging.Version hiding (version)
|
||||||
import Simplex.RemoteControl.Types (RCCtrlAddress (..))
|
import Simplex.RemoteControl.Types (RCCtrlAddress (..), RCErrorType (..))
|
||||||
import System.Console.ANSI.Types
|
import System.Console.ANSI.Types
|
||||||
|
|
||||||
type CurrentTime = UTCTime
|
type CurrentTime = UTCTime
|
||||||
@@ -211,6 +212,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
|||||||
CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft
|
CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft
|
||||||
CRRcvFileError u (Just ci) e _ -> ttyUser u $ receivingFile_' hu testView "error" ci <> [sShow e]
|
CRRcvFileError u (Just ci) e _ -> ttyUser u $ receivingFile_' hu testView "error" ci <> [sShow e]
|
||||||
CRRcvFileError u Nothing e ft -> ttyUser u $ receivingFileStandalone "error" ft <> [sShow e]
|
CRRcvFileError u Nothing e ft -> ttyUser u $ receivingFileStandalone "error" ft <> [sShow e]
|
||||||
|
CRRcvFileWarning u (Just ci) e _ -> ttyUser u $ receivingFile_' hu testView "warning: " ci <> [sShow e]
|
||||||
|
CRRcvFileWarning u Nothing e ft -> ttyUser u $ receivingFileStandalone "warning: " ft <> [sShow e]
|
||||||
CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft
|
CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft
|
||||||
CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft
|
CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft
|
||||||
CRSndStandaloneFileCreated u ft -> ttyUser u $ uploadingFileStandalone "started" ft
|
CRSndStandaloneFileCreated u ft -> ttyUser u $ uploadingFileStandalone "started" ft
|
||||||
@@ -222,6 +225,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
|||||||
CRSndFileCancelledXFTP {} -> []
|
CRSndFileCancelledXFTP {} -> []
|
||||||
CRSndFileError u Nothing ft e -> ttyUser u $ uploadingFileStandalone "error" ft <> [plain e]
|
CRSndFileError u Nothing ft e -> ttyUser u $ uploadingFileStandalone "error" ft <> [plain e]
|
||||||
CRSndFileError u (Just ci) _ e -> ttyUser u $ uploadingFile "error" ci <> [plain e]
|
CRSndFileError u (Just ci) _ e -> ttyUser u $ uploadingFile "error" ci <> [plain e]
|
||||||
|
CRSndFileWarning u Nothing ft e -> ttyUser u $ uploadingFileStandalone "warning: " ft <> [plain e]
|
||||||
|
CRSndFileWarning u (Just ci) _ e -> ttyUser u $ uploadingFile "warning: " ci <> [plain e]
|
||||||
CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} ->
|
CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} ->
|
||||||
ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft]
|
ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft]
|
||||||
CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [viewJSON j]) info_
|
CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [viewJSON j]) info_
|
||||||
@@ -346,7 +351,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
|
|||||||
]
|
]
|
||||||
CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, ctrlDeviceName} ->
|
CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, ctrlDeviceName} ->
|
||||||
["remote controller " <> sShow rcId <> " session started with " <> plain ctrlDeviceName]
|
["remote controller " <> sShow rcId <> " session started with " <> plain ctrlDeviceName]
|
||||||
CRRemoteCtrlStopped {} -> ["remote controller stopped"]
|
CRRemoteCtrlStopped {rcStopReason} -> viewRemoteCtrlStopped rcStopReason
|
||||||
CRContactPQEnabled u c (CR.PQEncryption pqOn) -> ttyUser u [ttyContact' c <> ": " <> (if pqOn then "quantum resistant" else "standard") <> " end-to-end encryption enabled"]
|
CRContactPQEnabled u c (CR.PQEncryption pqOn) -> ttyUser u [ttyContact' c <> ": " <> (if pqOn then "quantum resistant" else "standard") <> " end-to-end encryption enabled"]
|
||||||
CRSQLResult rows -> map plain rows
|
CRSQLResult rows -> map plain rows
|
||||||
CRSlowSQLQueries {chatQueries, agentQueries} ->
|
CRSlowSQLQueries {chatQueries, agentQueries} ->
|
||||||
@@ -1194,7 +1199,7 @@ viewServerTestResult (AProtoServerWithAuth p _) = \case
|
|||||||
<> [pName <> " server requires authorization to upload files, check password" | testStep == TSCreateFile && (case testError of XFTP _ XFTP.AUTH -> True; _ -> False)]
|
<> [pName <> " server requires authorization to upload files, check password" | testStep == TSCreateFile && (case testError of XFTP _ XFTP.AUTH -> True; _ -> False)]
|
||||||
<> ["Possibly, certificate fingerprint in " <> pName <> " server address is incorrect" | testStep == TSConnect && brokerErr]
|
<> ["Possibly, certificate fingerprint in " <> pName <> " server address is incorrect" | testStep == TSConnect && brokerErr]
|
||||||
where
|
where
|
||||||
result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> plain (strEncode testError)]
|
result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> sShow testError]
|
||||||
brokerErr = case testError of
|
brokerErr = case testError of
|
||||||
BROKER _ NETWORK -> True
|
BROKER _ NETWORK -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
@@ -1776,14 +1781,16 @@ viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId
|
|||||||
CIFSSndTransfer progress total -> ["sending " <> fstr <> " in progress " <> fileProgressXFTP progress total fileSize]
|
CIFSSndTransfer progress total -> ["sending " <> fstr <> " in progress " <> fileProgressXFTP progress total fileSize]
|
||||||
CIFSSndCancelled -> ["sending " <> fstr <> " cancelled"]
|
CIFSSndCancelled -> ["sending " <> fstr <> " cancelled"]
|
||||||
CIFSSndComplete -> ["sending " <> fstr <> " complete"]
|
CIFSSndComplete -> ["sending " <> fstr <> " complete"]
|
||||||
CIFSSndError -> ["sending " <> fstr <> " error"]
|
CIFSSndError sndFileErr -> ["sending " <> fstr <> " error: " <> plain (show sndFileErr)]
|
||||||
|
CIFSSndWarning sndFileErr -> ["sending " <> fstr <> " warning: " <> plain (show sndFileErr)]
|
||||||
CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"]
|
CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"]
|
||||||
CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"]
|
CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"]
|
||||||
CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize]
|
CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize]
|
||||||
CIFSRcvAborted -> ["receiving " <> fstr <> " aborted, use " <> highlight ("/fr " <> show fileId) <> " to receive file"]
|
CIFSRcvAborted -> ["receiving " <> fstr <> " aborted, use " <> highlight ("/fr " <> show fileId) <> " to receive file"]
|
||||||
CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource]
|
CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource]
|
||||||
CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"]
|
CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"]
|
||||||
CIFSRcvError -> ["receiving " <> fstr <> " error"]
|
CIFSRcvError rcvFileErr -> ["receiving " <> fstr <> " error: " <> plain (show rcvFileErr)]
|
||||||
|
CIFSRcvWarning rcvFileErr -> ["receiving " <> fstr <> " warning: " <> plain (show rcvFileErr)]
|
||||||
CIFSInvalid text -> [fstr <> " invalid status: " <> plain text]
|
CIFSInvalid text -> [fstr <> " invalid status: " <> plain text]
|
||||||
where
|
where
|
||||||
fstr = fileTransferStr fileId fileName
|
fstr = fileTransferStr fileId fileName
|
||||||
@@ -1837,7 +1844,7 @@ viewCallAnswer ct WebRTCSession {rtcSession = answer, rtcIceCandidates = iceCand
|
|||||||
[ ttyContact' ct <> " continued the WebRTC call",
|
[ ttyContact' ct <> " continued the WebRTC call",
|
||||||
"To connect, please paste the data below in your browser window you opened earlier and click Connect button",
|
"To connect, please paste the data below in your browser window you opened earlier and click Connect button",
|
||||||
"",
|
"",
|
||||||
viewJSON WCCallAnswer {answer, iceCandidates}
|
viewJSON WCCallAnswer {answer, iceCandidates}
|
||||||
]
|
]
|
||||||
|
|
||||||
callMediaStr :: CallType -> StyledString
|
callMediaStr :: CallType -> StyledString
|
||||||
@@ -1908,6 +1915,12 @@ viewRemoteCtrl CtrlAppInfo {deviceName, appVersionRange = AppVersionRange _ (App
|
|||||||
| otherwise = ""
|
| otherwise = ""
|
||||||
showCompatible = if compatible then "" else ", " <> bold' "not compatible"
|
showCompatible = if compatible then "" else ", " <> bold' "not compatible"
|
||||||
|
|
||||||
|
viewRemoteCtrlStopped :: RemoteCtrlStopReason -> [StyledString]
|
||||||
|
viewRemoteCtrlStopped = \case
|
||||||
|
RCSRConnectionFailed (ChatErrorAgent (RCP RCEIdentity) _) ->
|
||||||
|
["remote controller stopped: this link was used with another controller, please create a new link on the host"]
|
||||||
|
_ -> ["remote controller stopped"]
|
||||||
|
|
||||||
viewChatError :: Bool -> ChatLogLevel -> Bool -> ChatError -> [StyledString]
|
viewChatError :: Bool -> ChatLogLevel -> Bool -> ChatError -> [StyledString]
|
||||||
viewChatError isCmd logLevel testView = \case
|
viewChatError isCmd logLevel testView = \case
|
||||||
ChatError err -> case err of
|
ChatError err -> case err of
|
||||||
@@ -1984,8 +1997,6 @@ viewChatError isCmd logLevel testView = \case
|
|||||||
CEFileImageSize _ -> ["max image size: " <> sShow maxImageSize <> " bytes, resize it or send as a file using " <> highlight' "/f"]
|
CEFileImageSize _ -> ["max image size: " <> sShow maxImageSize <> " bytes, resize it or send as a file using " <> highlight' "/f"]
|
||||||
CEFileNotReceived fileId -> ["file " <> sShow fileId <> " not received"]
|
CEFileNotReceived fileId -> ["file " <> sShow fileId <> " not received"]
|
||||||
CEFileNotApproved fileId unknownSrvs -> ["file " <> sShow fileId <> " aborted, unknwon XFTP servers:"] <> map (plain . show) unknownSrvs
|
CEFileNotApproved fileId unknownSrvs -> ["file " <> sShow fileId <> " aborted, unknwon XFTP servers:"] <> map (plain . show) unknownSrvs
|
||||||
CEXFTPRcvFile fileId aFileId e -> ["error receiving XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError]
|
|
||||||
CEXFTPSndFile fileId aFileId e -> ["error sending XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError]
|
|
||||||
CEFallbackToSMPProhibited fileId -> ["recipient tried to accept file " <> sShow fileId <> " via old protocol, prohibited"]
|
CEFallbackToSMPProhibited fileId -> ["recipient tried to accept file " <> sShow fileId <> " via old protocol, prohibited"]
|
||||||
CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."]
|
CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."]
|
||||||
CEInvalidQuote -> ["cannot reply to this message"]
|
CEInvalidQuote -> ["cannot reply to this message"]
|
||||||
|
|||||||
+3
-3
@@ -101,7 +101,8 @@ testCoreOpts =
|
|||||||
logAgent = Nothing,
|
logAgent = Nothing,
|
||||||
logFile = Nothing,
|
logFile = Nothing,
|
||||||
tbqSize = 16,
|
tbqSize = 16,
|
||||||
highlyAvailable = False
|
highlyAvailable = False,
|
||||||
|
yesToUpMigrations = False
|
||||||
}
|
}
|
||||||
|
|
||||||
getTestOpts :: Bool -> ScrubbedBytes -> ChatOpts
|
getTestOpts :: Bool -> ScrubbedBytes -> ChatOpts
|
||||||
@@ -131,8 +132,7 @@ aCfg = (agentConfig defaultChatConfig) {tbqSize = 16}
|
|||||||
testAgentCfg :: AgentConfig
|
testAgentCfg :: AgentConfig
|
||||||
testAgentCfg =
|
testAgentCfg =
|
||||||
aCfg
|
aCfg
|
||||||
{ reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000},
|
{ reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}
|
||||||
xftpNotifyErrsOnRetry = False
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testCfg :: ChatConfig
|
testCfg :: ChatConfig
|
||||||
|
|||||||
@@ -808,7 +808,7 @@ testTestSMPServerConnection =
|
|||||||
alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"
|
alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"
|
||||||
alice <## "SMP server test passed"
|
alice <## "SMP server test passed"
|
||||||
alice ##> "/smp test smp://LcJU@localhost:7001"
|
alice ##> "/smp test smp://LcJU@localhost:7001"
|
||||||
alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:7001 NETWORK"
|
alice <## "SMP server test failed at Connect, error: BROKER {brokerAddress = \"smp://LcJU@localhost:7001\", brokerErr = NETWORK}"
|
||||||
alice <## "Possibly, certificate fingerprint in SMP server address is incorrect"
|
alice <## "Possibly, certificate fingerprint in SMP server address is incorrect"
|
||||||
|
|
||||||
testGetSetXFTPServers :: HasCallStack => FilePath -> IO ()
|
testGetSetXFTPServers :: HasCallStack => FilePath -> IO ()
|
||||||
@@ -839,7 +839,7 @@ testTestXFTPServer =
|
|||||||
alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"
|
alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"
|
||||||
alice <## "XFTP server test passed"
|
alice <## "XFTP server test passed"
|
||||||
alice ##> "/xftp test xftp://LcJU@localhost:7002"
|
alice ##> "/xftp test xftp://LcJU@localhost:7002"
|
||||||
alice <## "XFTP server test failed at Connect, error: BROKER xftp://LcJU@localhost:7002 NETWORK"
|
alice <## "XFTP server test failed at Connect, error: BROKER {brokerAddress = \"xftp://LcJU@localhost:7002\", brokerErr = NETWORK}"
|
||||||
alice <## "Possibly, certificate fingerprint in XFTP server address is incorrect"
|
alice <## "Possibly, certificate fingerprint in XFTP server address is incorrect"
|
||||||
|
|
||||||
testAsyncInitiatingOffline :: HasCallStack => FilePath -> IO ()
|
testAsyncInitiatingOffline :: HasCallStack => FilePath -> IO ()
|
||||||
|
|||||||
@@ -738,7 +738,7 @@ testXFTPRcvError tmp = do
|
|||||||
_ <- getTermLine bob
|
_ <- getTermLine bob
|
||||||
|
|
||||||
bob ##> "/fs 1"
|
bob ##> "/fs 1"
|
||||||
bob <## "receiving file 1 (test.pdf) error"
|
bob <## "receiving file 1 (test.pdf) error: FileErrAuth"
|
||||||
|
|
||||||
testXFTPCancelRcvRepeat :: HasCallStack => FilePath -> IO ()
|
testXFTPCancelRcvRepeat :: HasCallStack => FilePath -> IO ()
|
||||||
testXFTPCancelRcvRepeat =
|
testXFTPCancelRcvRepeat =
|
||||||
|
|||||||
@@ -2032,10 +2032,19 @@ testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfil
|
|||||||
threadDelay 1000000
|
threadDelay 1000000
|
||||||
bob ##> "/c"
|
bob ##> "/c"
|
||||||
inv <- getInvitation bob
|
inv <- getInvitation bob
|
||||||
bob ##> ("#team " <> inv)
|
bob ##> ("#team \"" <> inv <> "\\ntest\"")
|
||||||
|
bob <## "bad chat command: feature not allowed SimpleX links"
|
||||||
|
bob ##> ("/_send #1 json {\"msgContent\": {\"type\": \"text\", \"text\": \"" <> inv <> "\\ntest\"}}")
|
||||||
bob <## "bad chat command: feature not allowed SimpleX links"
|
bob <## "bad chat command: feature not allowed SimpleX links"
|
||||||
(alice </)
|
(alice </)
|
||||||
(cath </)
|
(cath </)
|
||||||
|
bob `send` ("@alice \"" <> inv <> "\\ntest\"")
|
||||||
|
bob <# ("@alice " <> inv)
|
||||||
|
bob <## "test"
|
||||||
|
alice <# ("bob> " <> inv)
|
||||||
|
alice <## "test"
|
||||||
|
bob ##> "#team <- @alice https://simplex.chat"
|
||||||
|
bob <## "bad chat command: feature not allowed SimpleX links"
|
||||||
alice #> ("#team " <> inv)
|
alice #> ("#team " <> inv)
|
||||||
bob <# ("#team alice> " <> inv)
|
bob <# ("#team alice> " <> inv)
|
||||||
cath <# ("#team alice> " <> inv)
|
cath <# ("#team alice> " <> inv)
|
||||||
|
|||||||
@@ -210,3 +210,10 @@ multilineMarkdownList = describe "multiline markdown" do
|
|||||||
parseMaybeMarkdownList "http://simplex.chat\ntext 1\ntext 2\nhttp://app.simplex.chat" `shouldBe` Just [uri' "http://simplex.chat", "\ntext 1\ntext 2\n", uri' "http://app.simplex.chat"]
|
parseMaybeMarkdownList "http://simplex.chat\ntext 1\ntext 2\nhttp://app.simplex.chat" `shouldBe` Just [uri' "http://simplex.chat", "\ntext 1\ntext 2\n", uri' "http://app.simplex.chat"]
|
||||||
it "no markdown" do
|
it "no markdown" do
|
||||||
parseMaybeMarkdownList "not a\nmarkdown" `shouldBe` Nothing
|
parseMaybeMarkdownList "not a\nmarkdown" `shouldBe` Nothing
|
||||||
|
let inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D"
|
||||||
|
it "multiline with simplex link" do
|
||||||
|
parseMaybeMarkdownList ("https://simplex.chat" <> inv <> "\ntext")
|
||||||
|
`shouldBe` Just
|
||||||
|
[ FormattedText (Just $ SimplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"]) ("https://simplex.chat" <> inv),
|
||||||
|
"\ntext"
|
||||||
|
]
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfil
|
|||||||
inv <- getTermLine desktop
|
inv <- getTermLine desktop
|
||||||
mobileBob ##> ("/connect remote ctrl " <> inv)
|
mobileBob ##> ("/connect remote ctrl " <> inv)
|
||||||
mobileBob <## ("connecting new remote controller: My desktop, v" <> versionNumber)
|
mobileBob <## ("connecting new remote controller: My desktop, v" <> versionNumber)
|
||||||
mobileBob <## "remote controller stopped"
|
mobileBob <## "remote controller stopped: this link was used with another controller, please create a new link on the host"
|
||||||
|
|
||||||
-- the server remains active after rejecting invalid client
|
-- the server remains active after rejecting invalid client
|
||||||
mobile ##> ("/connect remote ctrl " <> inv)
|
mobile ##> ("/connect remote ctrl " <> inv)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
"simplex-explained-tab-2-p-1": "لكل اتصال، تستخدم قائمتي انتظار منفصلتين للمُراسلة لإرسال واستلام الرسائل عبر خوادم مختلفة.",
|
"simplex-explained-tab-2-p-1": "لكل اتصال، تستخدم قائمتي انتظار منفصلتين للمُراسلة لإرسال واستلام الرسائل عبر خوادم مختلفة.",
|
||||||
"simplex-explained-tab-2-p-2": "تقوم الخوادم بتمرير الرسائل في اتجاه واحد فقط، دون الحصول على الصورة الكاملة لمُحادثات المستخدم أو اتصالاته.",
|
"simplex-explained-tab-2-p-2": "تقوم الخوادم بتمرير الرسائل في اتجاه واحد فقط، دون الحصول على الصورة الكاملة لمُحادثات المستخدم أو اتصالاته.",
|
||||||
"simplex-explained-tab-3-p-1": "تحتوي الخوادم على بيانات اعتماد مجهولة منفصلة لكل قائمة انتظار، ولا تعرف المستخدمين الذين ينتمون إليهم.",
|
"simplex-explained-tab-3-p-1": "تحتوي الخوادم على بيانات اعتماد مجهولة منفصلة لكل قائمة انتظار، ولا تعرف المستخدمين الذين ينتمون إليهم.",
|
||||||
"copyright-label": "مشروع مفتوح المصدر © SimpleX 2020-2023",
|
"copyright-label": "مشروع مفتوح المصدر © SimpleX 2020-2024",
|
||||||
"simplex-chat-protocol": "بروتوكول دردشة SimpleX",
|
"simplex-chat-protocol": "بروتوكول دردشة SimpleX",
|
||||||
"developers": "المطورين",
|
"developers": "المطورين",
|
||||||
"hero-subheader": "أول نظام مُراسلة<br> دون معرّفات مُستخدم",
|
"hero-subheader": "أول نظام مُراسلة<br> دون معرّفات مُستخدم",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"smp-protocol": "СМП Протокол",
|
"smp-protocol": "СМП Протокол",
|
||||||
"chat-protocol": "Чат протокол",
|
"chat-protocol": "Чат протокол",
|
||||||
"donate": "Дарете",
|
"donate": "Дарете",
|
||||||
"copyright-label": "© 2020-2023 SimpleX | Проект с отворен код",
|
"copyright-label": "© 2020-2024 SimpleX | Проект с отворен код",
|
||||||
"simplex-chat-protocol": "SimpleX Чат протокол",
|
"simplex-chat-protocol": "SimpleX Чат протокол",
|
||||||
"terminal-cli": "Системна конзола",
|
"terminal-cli": "Системна конзола",
|
||||||
"terms-and-privacy-policy": "Условия и политика за поверителност",
|
"terms-and-privacy-policy": "Условия и политика за поверителност",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"smp-protocol": "SMP protokol",
|
"smp-protocol": "SMP protokol",
|
||||||
"chat-protocol": "Chat protokol",
|
"chat-protocol": "Chat protokol",
|
||||||
"donate": "Darovat",
|
"donate": "Darovat",
|
||||||
"copyright-label": "© 2020-2023 SimpleX | Projekt s otevřeným zdrojovým kódem",
|
"copyright-label": "© 2020-2024 SimpleX | Projekt s otevřeným zdrojovým kódem",
|
||||||
"simplex-chat-protocol": "SimpleX Chat protokol",
|
"simplex-chat-protocol": "SimpleX Chat protokol",
|
||||||
"terminal-cli": "Terminálové rozhraní příkazového řádku",
|
"terminal-cli": "Terminálové rozhraní příkazového řádku",
|
||||||
"terms-and-privacy-policy": "Podmínky a zásady ochrany osobních údajů",
|
"terms-and-privacy-policy": "Podmínky a zásady ochrany osobních údajů",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"smp-protocol": "SMP Protokoll",
|
"smp-protocol": "SMP Protokoll",
|
||||||
"chat-bot-example": "Beispiel für einen Chatbot",
|
"chat-bot-example": "Beispiel für einen Chatbot",
|
||||||
"donate": "Spenden",
|
"donate": "Spenden",
|
||||||
"copyright-label": "© 2020-2023 SimpleX | Open-Source Projekt",
|
"copyright-label": "© 2020-2024 SimpleX | Open-Source Projekt",
|
||||||
"chat-protocol": "Chat Protokoll",
|
"chat-protocol": "Chat Protokoll",
|
||||||
"simplex-chat-protocol": "SimpleX Chat Protokoll",
|
"simplex-chat-protocol": "SimpleX Chat Protokoll",
|
||||||
"terminal-cli": "Terminal Kommandozeilen-Schnittstelle",
|
"terminal-cli": "Terminal Kommandozeilen-Schnittstelle",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"smp-protocol": "SMP protocol",
|
"smp-protocol": "SMP protocol",
|
||||||
"chat-protocol": "Chat protocol",
|
"chat-protocol": "Chat protocol",
|
||||||
"donate": "Donate",
|
"donate": "Donate",
|
||||||
"copyright-label": "© 2020-2023 SimpleX | Open-Source Project",
|
"copyright-label": "© 2020-2024 SimpleX | Open-Source Project",
|
||||||
"simplex-chat-protocol": "SimpleX Chat protocol",
|
"simplex-chat-protocol": "SimpleX Chat protocol",
|
||||||
"terminal-cli": "Terminal CLI",
|
"terminal-cli": "Terminal CLI",
|
||||||
"terms-and-privacy-policy": "Privacy Policy",
|
"terms-and-privacy-policy": "Privacy Policy",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"simplex-explained-tab-3-p-2": "El usuario puede mejorar aún más la privacidad de sus metadatos haciendo uso de la red Tor para acceder a los servidores, evitando así la correlación por dirección IP.",
|
"simplex-explained-tab-3-p-2": "El usuario puede mejorar aún más la privacidad de sus metadatos haciendo uso de la red Tor para acceder a los servidores, evitando así la correlación por dirección IP.",
|
||||||
"smp-protocol": "Protocolo SMP",
|
"smp-protocol": "Protocolo SMP",
|
||||||
"donate": "Donación",
|
"donate": "Donación",
|
||||||
"copyright-label": "© 2020-2023 SimpleX | Proyecto de Código Abierto",
|
"copyright-label": "© 2020-2024 SimpleX | Proyecto de Código Abierto",
|
||||||
"simplex-chat-protocol": "Protocolo de SimpleX Chat",
|
"simplex-chat-protocol": "Protocolo de SimpleX Chat",
|
||||||
"terms-and-privacy-policy": "Términos y Política de Privacidad",
|
"terms-and-privacy-policy": "Términos y Política de Privacidad",
|
||||||
"hero-header": "Privacidad redefinida",
|
"hero-header": "Privacidad redefinida",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"smp-protocol": "Protocole SMP",
|
"smp-protocol": "Protocole SMP",
|
||||||
"chat-protocol": "Protocole de chat",
|
"chat-protocol": "Protocole de chat",
|
||||||
"donate": "Faire un don",
|
"donate": "Faire un don",
|
||||||
"copyright-label": "© 2020-2023 SimpleX | Projet Open-Source",
|
"copyright-label": "© 2020-2024 SimpleX | Projet Open-Source",
|
||||||
"simplex-chat-protocol": "Protocole SimpleX Chat",
|
"simplex-chat-protocol": "Protocole SimpleX Chat",
|
||||||
"terminal-cli": "Terminal CLI",
|
"terminal-cli": "Terminal CLI",
|
||||||
"terms-and-privacy-policy": "Politique de confidentialité",
|
"terms-and-privacy-policy": "Politique de confidentialité",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"smp-protocol": "SMP protokoll",
|
"smp-protocol": "SMP protokoll",
|
||||||
"chat-protocol": "Csevegés protokoll",
|
"chat-protocol": "Csevegés protokoll",
|
||||||
"donate": "Támogatás",
|
"donate": "Támogatás",
|
||||||
"copyright-label": "© 2020-2023 SimpleX | Nyílt forráskódú projekt",
|
"copyright-label": "© 2020-2024 SimpleX | Nyílt forráskódú projekt",
|
||||||
"simplex-chat-protocol": "SimpleX Chat protokoll",
|
"simplex-chat-protocol": "SimpleX Chat protokoll",
|
||||||
"terminal-cli": "Terminál CLI",
|
"terminal-cli": "Terminál CLI",
|
||||||
"terms-and-privacy-policy": "Adatvédelmi irányelvek",
|
"terms-and-privacy-policy": "Adatvédelmi irányelvek",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"simplex-explained-tab-3-p-1": "I server hanno credenziali anonime separate per ogni coda e non sanno a quali utenti appartengano.",
|
"simplex-explained-tab-3-p-1": "I server hanno credenziali anonime separate per ogni coda e non sanno a quali utenti appartengano.",
|
||||||
"chat-protocol": "Protocollo di chat",
|
"chat-protocol": "Protocollo di chat",
|
||||||
"donate": "Dona",
|
"donate": "Dona",
|
||||||
"copyright-label": "© 2020-2023 SimpleX | Progetto Open-Source",
|
"copyright-label": "© 2020-2024 SimpleX | Progetto Open-Source",
|
||||||
"simplex-chat-protocol": "Protocollo di SimpleX Chat",
|
"simplex-chat-protocol": "Protocollo di SimpleX Chat",
|
||||||
"terminal-cli": "Terminale CLI",
|
"terminal-cli": "Terminale CLI",
|
||||||
"terms-and-privacy-policy": "Informativa sulla privacy",
|
"terms-and-privacy-policy": "Informativa sulla privacy",
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
"chat-protocol": "チャットプロトコル",
|
"chat-protocol": "チャットプロトコル",
|
||||||
"chat-bot-example": "チャットボットの例",
|
"chat-bot-example": "チャットボットの例",
|
||||||
"donate": "寄付",
|
"donate": "寄付",
|
||||||
"copyright-label": "© 2020-2023 SimpleX | Open-Source Project",
|
"copyright-label": "© 2020-2024 SimpleX | Open-Source Project",
|
||||||
"hero-p-1": "他のアプリにはユーザー ID があります: Signal、Matrix、Session、Briar、Jami、Cwtch など。<br> SimpleX にはありません。<strong>乱数さえもありません</strong>。<br> これにより、プライバシーが大幅に向上します。",
|
"hero-p-1": "他のアプリにはユーザー ID があります: Signal、Matrix、Session、Briar、Jami、Cwtch など。<br> SimpleX にはありません。<strong>乱数さえもありません</strong>。<br> これにより、プライバシーが大幅に向上します。",
|
||||||
"copy-the-command-below-text": "以下のコマンドをコピーしてチャットで使用します:",
|
"copy-the-command-below-text": "以下のコマンドをコピーしてチャットで使用します:",
|
||||||
"simplex-private-card-9-point-1": "各メッセージ キューは、異なる送信アドレスと受信アドレスを使用してメッセージを一方向に渡します。",
|
"simplex-private-card-9-point-1": "各メッセージ キューは、異なる送信アドレスと受信アドレスを使用してメッセージを一方向に渡します。",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"chat-bot-example": "Chatbot voorbeeld",
|
"chat-bot-example": "Chatbot voorbeeld",
|
||||||
"smp-protocol": "SMP protocol",
|
"smp-protocol": "SMP protocol",
|
||||||
"donate": "Doneer",
|
"donate": "Doneer",
|
||||||
"copyright-label": "© 2020-2023 SimpleX | Open-sourceproject",
|
"copyright-label": "© 2020-2024 SimpleX | Open-sourceproject",
|
||||||
"simplex-chat-protocol": "SimpleX Chat protocol",
|
"simplex-chat-protocol": "SimpleX Chat protocol",
|
||||||
"terminal-cli": "Terminal CLI",
|
"terminal-cli": "Terminal CLI",
|
||||||
"terms-and-privacy-policy": "Privacybeleid",
|
"terms-and-privacy-policy": "Privacybeleid",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"smp-protocol": "Protokół SMP",
|
"smp-protocol": "Protokół SMP",
|
||||||
"chat-protocol": "Protokół czatu",
|
"chat-protocol": "Protokół czatu",
|
||||||
"donate": "Darowizna",
|
"donate": "Darowizna",
|
||||||
"copyright-label": "© 2020-2023 SimpleX | Projekt Open-Source",
|
"copyright-label": "© 2020-2024 SimpleX | Projekt Open-Source",
|
||||||
"simplex-chat-protocol": "Protokół SimpleX Chat",
|
"simplex-chat-protocol": "Protokół SimpleX Chat",
|
||||||
"terminal-cli": "Terminal CLI",
|
"terminal-cli": "Terminal CLI",
|
||||||
"terms-and-privacy-policy": "Polityka prywatności",
|
"terms-and-privacy-policy": "Polityka prywatności",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"smp-protocol": "Protocolo SMP",
|
"smp-protocol": "Protocolo SMP",
|
||||||
"chat-protocol": "Protocolo de bate-papo",
|
"chat-protocol": "Protocolo de bate-papo",
|
||||||
"donate": "Doar",
|
"donate": "Doar",
|
||||||
"copyright-label": "© 2020-2023 SimpleX | Projeto de Código Livre",
|
"copyright-label": "© 2020-2024 SimpleX | Projeto de Código Livre",
|
||||||
"simplex-chat-protocol": "Protocolo Chat SimpleX",
|
"simplex-chat-protocol": "Protocolo Chat SimpleX",
|
||||||
"terminal-cli": "CLI Terminal",
|
"terminal-cli": "CLI Terminal",
|
||||||
"hero-header": "Privacidade redefinida",
|
"hero-header": "Privacidade redefinida",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"copy-the-command-below-text": "скопируйте приведенную ниже команду и используйте ее в чате:",
|
"copy-the-command-below-text": "скопируйте приведенную ниже команду и используйте ее в чате:",
|
||||||
"copyright-label": "© 2020-2023 SimpleX | Проект с открытым исходным кодом",
|
"copyright-label": "© 2020-2024 SimpleX | Проект с открытым исходным кодом",
|
||||||
"chat-bot-example": "Пример Чат бота",
|
"chat-bot-example": "Пример Чат бота",
|
||||||
"simplex-private-card-9-point-1": "Каждая очередь сообщений передает сообщения в одном направлении с разными адресами отправки и получения.",
|
"simplex-private-card-9-point-1": "Каждая очередь сообщений передает сообщения в одном направлении с разными адресами отправки и получения.",
|
||||||
"simplex-private-card-1-point-2": "Криптобокс NaCL в каждой очереди для предотвращения корреляции трафика между очередями сообщений, в случае компрометации TLS.",
|
"simplex-private-card-1-point-2": "Криптобокс NaCL в каждой очереди для предотвращения корреляции трафика между очередями сообщений, в случае компрометации TLS.",
|
||||||
|
|||||||
@@ -59,6 +59,10 @@
|
|||||||
"term": "Man-in-the-middle attack",
|
"term": "Man-in-the-middle attack",
|
||||||
"definition": "Man-in-the-middle attack"
|
"definition": "Man-in-the-middle attack"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"term": "MITM attack",
|
||||||
|
"definition": "Man-in-the-middle attack"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"term": "Merkle directed acyclic graph",
|
"term": "Merkle directed acyclic graph",
|
||||||
"definition": "Merkle directed acyclic graph"
|
"definition": "Merkle directed acyclic graph"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<p><strong>v5.8 is released:</strong></p>
|
||||||
|
|
||||||
|
<ul class="mb-[12px]">
|
||||||
|
<li>private message routing.</li>
|
||||||
|
<li>server transparency.</li>
|
||||||
|
<li>protect IP address when downloading files & media.</li>
|
||||||
|
<li>chat themes* for better conversation privacy.</li>
|
||||||
|
<li>group improvements - reduced traffic and additional preferences.</li>
|
||||||
|
<li>improved networking, message and file delivery.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Also, we added Persian interface language*, thanks to our users and Weblate.</p>
|
||||||
|
|
||||||
|
<p>* Android and desktop apps only.</p>
|
||||||
@@ -58,7 +58,7 @@ active_blog: true
|
|||||||
</div>
|
</div>
|
||||||
<div class="p-6 md:py-8 flex-[2.5] flex flex-col">
|
<div class="p-6 md:py-8 flex-[2.5] flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-grey-black dark:text-white text-lg md:text-xl font-bold ">
|
<h1 class="text-grey-black dark:text-white !text-lg md:!text-xl font-bold ">
|
||||||
<a href="{{ blog.url }}">{{ blog.data.title | safe }}</a>
|
<a href="{{ blog.url }}">{{ blog.data.title | safe }}</a>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-sm text-[#A8B0B4] font-medium mt-2 mb-4 tracking-[0.03em]">
|
<p class="text-sm text-[#A8B0B4] font-medium mt-2 mb-4 tracking-[0.03em]">
|
||||||
|
|||||||
@@ -11,28 +11,31 @@ metadata:
|
|||||||
email: chat@simplex.chat
|
email: chat@simplex.chat
|
||||||
---
|
---
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="{{ metadata.url }}">
|
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="{{ metadata.language }}">
|
||||||
|
<id>{{ metadata.url }}</id>
|
||||||
|
<link type="text/html" rel="alternate" href="{{ metadata.url }}"/>
|
||||||
|
<link type="application/atom+xml" rel="self" href="{{ permalink | absoluteUrl(metadata.url) }}"/>
|
||||||
<title>{{ metadata.title }}</title>
|
<title>{{ metadata.title }}</title>
|
||||||
<subtitle>{{ metadata.subtitle }}</subtitle>
|
<subtitle>{{ metadata.subtitle }}</subtitle>
|
||||||
<link href="{{ permalink | absoluteUrl(metadata.url) }}" rel="self"/>
|
|
||||||
<link href="{{ metadata.url }}"/>
|
|
||||||
<updated>{{ collections.blogs | getNewestCollectionItemDate | dateToRfc3339 }}</updated>
|
<updated>{{ collections.blogs | getNewestCollectionItemDate | dateToRfc3339 }}</updated>
|
||||||
<id>{{ metadata.url }}</id>
|
|
||||||
<author>
|
<author>
|
||||||
<name>{{ metadata.author.name }}</name>
|
<name>{{ metadata.author.name }}</name>
|
||||||
<email>{{ metadata.author.email }}</email>
|
<email>{{ metadata.author.email }}</email>
|
||||||
</author>
|
</author>
|
||||||
{%- for blog in collections.blogs | reverse %}
|
{%- for blog in collections.blogs | reverse %}
|
||||||
{%- if not blog.data.draft %}
|
{%- if not blog.data.draft %}
|
||||||
{%- set absolutePostUrl = blog.url | absoluteUrl(metadata.url) %}
|
{%- set absolutePostUrl = blog.data.permalink | absoluteUrl(metadata.url) %}
|
||||||
<entry>
|
<entry>
|
||||||
|
<id>{{ blog.data.permalink | absoluteUrl(metadata.url) }}</id>
|
||||||
|
<!-- <updated>{{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }}</updated> -->
|
||||||
|
<updated>{{ blog.data.date | dateToRfc3339 }}</updated>
|
||||||
|
<link rel="alternate" type="text/html" href="{{ absolutePostUrl }}"/>
|
||||||
<title>{{ blog.data.title }}</title>
|
<title>{{ blog.data.title }}</title>
|
||||||
<link href="{{ absolutePostUrl }}"/>
|
<content type="html">{{ blog.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}</content>
|
||||||
{# <updated>{{ blog.date | dateToRfc3339 }}</updated> #}
|
<author>
|
||||||
<updated>{{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }}</updated>
|
<name>{{ metadata.author.name }}</name>
|
||||||
<id>{{ absolutePostUrl }}</id>
|
<email>{{ metadata.author.email }}</email>
|
||||||
<content xml:lang="{{ metadata.language }}" type="html">{{ blog.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}</content>
|
</author>
|
||||||
{# <content xml:lang="{{ metadata.language }}" type="html">{{ blog.templateContent | striptags | truncate(200) }}</content> #}
|
|
||||||
</entry>
|
</entry>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ metadata:
|
|||||||
<link>{{ absolutePostUrl }}</link>
|
<link>{{ absolutePostUrl }}</link>
|
||||||
<description>{{ blog.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}</description>
|
<description>{{ blog.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}</description>
|
||||||
{# <description>{{ blog.templateContent | striptags | truncate(200) }}</description> #}
|
{# <description>{{ blog.templateContent | striptags | truncate(200) }}</description> #}
|
||||||
{# <pubDate>{{ blog.data.date | dateToRfc822 }}</pubDate> #}
|
<pubDate>{{ blog.data.date | dateToRfc822 }}</pubDate>
|
||||||
<pubDate>{{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }}</pubDate>
|
{# <pubDate>{{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }}</pubDate> #}
|
||||||
<dc:creator>{{ metadata.author.name }}</dc:creator>
|
<dc:creator>{{ metadata.author.name }}</dc:creator>
|
||||||
<guid>{{ absolutePostUrl }}</guid>
|
<guid>{{ absolutePostUrl }}</guid>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ img{
|
|||||||
-ms-user-select: none; /* For Internet Explorer and Edge */
|
-ms-user-select: none; /* For Internet Explorer and Edge */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a{
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
/* #comparison::before {
|
/* #comparison::before {
|
||||||
display: block;
|
display: block;
|
||||||
content: " ";
|
content: " ";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
layout: layouts/group_link.html
|
layout: layouts/group_link.html
|
||||||
title: "SimpleX Chat - MoneroKon group"
|
title: "SimpleX Chat - MoneroKon group"
|
||||||
description: "Join the group of attendees of Monero Konferenco 3 - Praha 2023"
|
description: "Join the group of attendees of Monero Konferenco 3 - Praha 2023"
|
||||||
groupLink: "https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F1OXnPP15cK8HAJ3YM_7UfQhlW-9WFE8P%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHf0AClqIM2SnOJ7OP06pr7UXlcnzGaBUyx3MLmRP0ko%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22CIdAO_gOEDOsW9oZrtAHiA%3D%3D%22%7D"
|
groupLink: "https://simplex.chat/contact/#/?v=1-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FIE3ZKT3daRLKdQg1nSXK4U1cUK4A81XQ%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAN2vLBKKQiTG58nokhiBIpqvLTyfeyey6UbaFGy4cYH8%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%227LTn4BEWw4bD9Gs8snVEJA%3D%3D%22%7D"
|
||||||
groupLinkText: Open MoneroKon group link
|
groupLinkText: Open MoneroKon group link
|
||||||
templateEngineOverride: njk
|
templateEngineOverride: njk
|
||||||
---
|
---
|
||||||
Reference in New Issue
Block a user