Compare commits

..

2 Commits

Author SHA1 Message Date
Avently 1b254f0d22 revert Kotlin 2.0 update 2024-06-03 14:04:08 +07:00
Avently ee6f9c334a multiplatform: Kotlin 2.0 & Compose 1.6.10 2024-06-03 14:01:18 +07:00
100 changed files with 672 additions and 2196 deletions
+1 -3
View File
@@ -234,8 +234,6 @@ You can use SimpleX with your own servers and still communicate with people usin
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)
[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)
@@ -384,10 +382,10 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
- ✅ Private notes.
- ✅ Improve sending videos (including encryption of locally stored videos).
- ✅ Post-quantum resistant key exchange in double ratchet protocol.
- ✅ Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- 🏗 Improve stability and reduce battery usage.
- 🏗 Improve experience for the new users.
- 🏗 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.
- SMP queue redundancy and rotation (manual is supported).
- Include optional message into connection request sent via contact address.
+2 -26
View File
@@ -1826,15 +1826,11 @@ func processReceivedMsg(_ res: ChatResponse) async {
if let aChatItem = aChatItem {
await chatItemSimpleUpdate(user, aChatItem)
}
case let .rcvFileError(user, aChatItem, _, _):
case let .rcvFileError(user, aChatItem, _):
if let aChatItem = aChatItem {
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupFile(aChatItem) }
}
case let .rcvFileWarning(user, aChatItem, _, _):
if let aChatItem = aChatItem {
await chatItemSimpleUpdate(user, aChatItem)
}
case let .sndFileStart(user, aChatItem, _):
await chatItemSimpleUpdate(user, aChatItem)
case let .sndFileComplete(user, aChatItem, _):
@@ -1856,10 +1852,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupFile(aChatItem) }
}
case let .sndFileWarning(user, aChatItem, _, _):
if let aChatItem = aChatItem {
await chatItemSimpleUpdate(user, aChatItem)
}
case let .callInvitation(invitation):
await MainActor.run {
m.callInvitations[invitation.contact.id] = invitation
@@ -1957,28 +1949,12 @@ func processReceivedMsg(_ res: ChatResponse) async {
let state = UIRemoteCtrlSessionState.connected(remoteCtrl: remoteCtrl, sessionCode: m.remoteCtrlSession?.sessionCode ?? "")
m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state)
}
case let .remoteCtrlStopped(_, rcStopReason):
case .remoteCtrlStopped:
// 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.
if let sess = m.remoteCtrlSession {
await MainActor.run {
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 {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
@@ -56,16 +56,14 @@ struct CIFileView: View {
case .sndTransfer: return false
case .sndComplete: return true
case .sndCancelled: return false
case .sndError: return true
case .sndWarning: return true
case .sndError: return false
case .rcvInvitation: return true
case .rcvAccepted: return true
case .rcvTransfer: return false
case .rcvAborted: return true
case .rcvComplete: return true
case .rcvCancelled: return false
case .rcvError: return true
case .rcvWarning: return true
case .rcvError: return false
case .invalid: return false
}
}
@@ -110,18 +108,6 @@ struct CIFileView: View {
if let fileSource = getLoadedFileSource(file) {
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:
logger.debug("CIFileView fileAction - in .sndStored")
if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) {
@@ -132,18 +118,6 @@ struct CIFileView: View {
if let fileSource = getLoadedFileSource(file) {
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
}
}
@@ -167,7 +141,6 @@ struct CIFileView: View {
case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10)
case .sndCancelled: 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:
if fileSizeValid(file) {
fileIcon("arrow.down.doc.fill", color: .accentColor)
@@ -186,7 +159,6 @@ struct CIFileView: View {
case .rcvComplete: fileIcon("doc.fill")
case .rcvCancelled: 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)
}
} else {
@@ -60,26 +60,6 @@ struct CIImageView: View {
case .rcvTransfer: () // ?
case .rcvComplete: () // ?
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: ()
}
}
@@ -119,16 +99,14 @@ struct CIImageView: View {
case .sndComplete: fileIcon("checkmark", 10, 13)
case .sndCancelled: 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 .rcvAccepted: fileIcon("ellipsis", 14, 11)
case .rcvTransfer: progressView()
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
case .rcvComplete: EmptyView()
case .rcvCancelled: fileIcon("xmark", 10, 13)
case .rcvError: fileIcon("xmark", 10, 13)
case .rcvWarning: fileIcon("exclamationmark.triangle.fill", 10, 13)
case .invalid: fileIcon("questionmark", 10, 13)
default: EmptyView()
}
}
}
@@ -192,7 +192,7 @@ struct CIVideoView: View {
.disabled(!canBePlayed)
}
}
fileStatusIcon()
loadingIndicator()
}
.onAppear {
addObserver(player, url)
@@ -258,11 +258,11 @@ struct CIVideoView: View {
.resizable()
.scaledToFit()
.frame(width: w)
fileStatusIcon()
loadingIndicator()
}
}
@ViewBuilder private func fileStatusIcon() -> some View {
@ViewBuilder private func loadingIndicator() -> some View {
if let file = chatItem.file {
switch file.fileStatus {
case .sndStored:
@@ -279,22 +279,7 @@ struct CIVideoView: View {
}
case .sndComplete: fileIcon("checkmark", 10, 13)
case .sndCancelled: 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 .sndError: fileIcon("xmark", 10, 13)
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
case let .rcvTransfer(rcvProgress, rcvTotal):
@@ -304,25 +289,10 @@ struct CIVideoView: View {
progressView()
}
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
case .rcvComplete: EmptyView()
case .rcvCancelled: 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 .rcvError: fileIcon("xmark", 10, 13)
case .invalid: fileIcon("questionmark", 10, 13)
default: EmptyView()
}
}
}
@@ -134,53 +134,18 @@ struct VoiceMessagePlayer: View {
ZStack {
if let recordingFile = recordingFile {
switch recordingFile.fileStatus {
case .sndStored:
if recordingFile.fileProtocol == .local {
playbackButton()
} else {
loadingIcon()
}
case .sndTransfer: loadingIcon()
case .sndStored: playbackButton()
case .sndTransfer: playbackButton()
case .sndComplete: playbackButton()
case .sndCancelled: 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 .sndError: playbackButton()
case .rcvInvitation: downloadButton(recordingFile, "play.fill")
case .rcvAccepted: loadingIcon()
case .rcvTransfer: loadingIcon()
case .rcvAborted: downloadButton(recordingFile, "exclamationmark.arrow.circlepath")
case .rcvComplete: playbackButton()
case .rcvCancelled: 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 .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
case .invalid: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
}
} else {
@@ -281,17 +246,6 @@ 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 {
ProgressView()
.frame(width: 30, height: 30)
@@ -19,9 +19,6 @@ struct ChatItemForwardingView: View {
@State private var searchText: String = ""
@FocusState private var searchFocused
@State private var alert: SomeAlert?
@State private var hasSimplexLink_: Bool?
private let chatsToForwardTo = filterChatsToForwardTo()
var body: some View {
NavigationView {
@@ -38,29 +35,47 @@ struct ChatItemForwardingView: View {
}
}
}
.alert(item: $alert) { $0.alert }
}
@ViewBuilder private func forwardListView() -> some View {
VStack(alignment: .leading) {
let chatsToForwardTo = filterChatsToForwardTo()
if !chatsToForwardTo.isEmpty {
List {
searchFieldView(text: $searchText, focussed: $searchFocused)
.padding(.leading, 2)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { foundChat($0, s) }
ForEach(chats) { chat in
forwardListChatView(chat)
.disabled(chatModel.deletedChats.contains(chat.chatInfo.id))
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
searchFieldView(text: $searchText, focussed: $searchFocused)
.padding(.leading, 2)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { filterChatSearched($0, s) }
ForEach(chats) { chat in
Divider()
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 {
emptyList()
}
}
}
private func foundChat(_ chat: Chat, _ searchStr: String) -> Bool {
private func filterChatsToForwardTo() -> [Chat] {
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
return switch cInfo {
case let .direct(contact):
@@ -76,70 +91,42 @@ struct ChatItemForwardingView: View {
}
}
private func prohibitedByPref(_ chat: Chat) -> Bool {
// preference checks should match checks in compose view
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
let fileProhibited = (ci.content.msgContent?.isMediaOrFileAttachment ?? false) && !chat.groupFeatureEnabled(.files)
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
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
}
}
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 {
Text("No filtered chats")
.foregroundColor(.secondary)
.frame(maxWidth: .infinity)
}
@ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View {
let prohibited = prohibitedByPref(chat)
@ViewBuilder private func forwardListNavLinkView(_ chat: Chat) -> some View {
Button {
if prohibited {
alert = SomeAlert(
alert: mkAlert(
title: "Cannot forward message",
message: "Selected chat preferences prohibit this message."
),
id: "forward prohibited by preferences"
)
dismiss()
if chat.id == fromChatInfo.id {
composeState = ComposeState(
message: composeState.message,
preview: composeState.linkPreview != nil ? composeState.preview : .noPreview,
contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo)
)
} else {
dismiss()
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
}
composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo)
chatModel.chatId = chat.id
}
} label: {
HStack {
ChatInfoImage(chat: chat, size: 30)
.padding(.trailing, 2)
Text(chat.chatInfo.chatViewName)
.foregroundColor(prohibited ? .secondary : .primary)
.foregroundColor(.primary)
.lineLimit(1)
if chat.chatInfo.incognito {
Spacer()
@@ -155,27 +142,6 @@ 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 {
ChatItemForwardingView(
ci: ChatItem.getSample(1, .directSnd, .now, "hello"),
@@ -17,8 +17,6 @@ struct ChatItemInfoView: View {
@Binding var chatItemInfo: ChatItemInfo?
@State private var selection: CIInfoTab = .history
@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
enum CIInfoTab {
@@ -159,35 +157,6 @@ struct ChatItemInfoView: View {
if developerTools {
infoRow("Database ID", "\(meta.itemId)")
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()
}
}
}
}
}
}
@@ -504,12 +473,8 @@ struct ChatItemInfoView: View {
if developerTools {
shareText += [
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("Message status: %@", comment: "copied message info"), meta.itemStatus.id)
String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt))
]
if let file = ci.file {
shareText += [String.localizedStringWithFormat(NSLocalizedString("File status: %@", comment: "copied message info"), file.fileStatus.id)]
}
}
if let qi = ci.quotedItem {
shareText += ["", NSLocalizedString("## In reply to", comment: "copied message info")]
@@ -286,7 +286,6 @@ struct ComposeView: View {
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
ContextInvitingContactMemberView()
}
// preference checks should match checks in forwarding list
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files)
let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice)
@@ -1066,7 +1065,7 @@ struct ComposeView: View {
} else {
nil
}
let simplexLink = parsedMsgHasSimplexLink(parsedMsg)
let simplexLink = parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
return (url, simplexLink)
}
@@ -1106,10 +1105,6 @@ struct ComposeView: View {
}
}
func parsedMsgHasSimplexLink(_ parsedMsg: [FormattedText]) -> Bool {
parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
}
struct ComposeView_Previews: PreviewProvider {
static var previews: some View {
let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
+15 -12
View File
@@ -11,9 +11,14 @@ import SimpleXChat
import CodeScanner
import AVFoundation
struct SomeAlert: Identifiable {
var alert: Alert
var id: String
enum SomeAlert: Identifiable {
case someAlert(alert: Alert, id: String)
var id: String {
switch self {
case let .someAlert(_, id): return id
}
}
}
private enum NewChatViewAlert: Identifiable {
@@ -137,8 +142,8 @@ struct NewChatView: View {
switch(a) {
case let .planAndConnectAlert(alert):
return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" })
case let .newChatSomeAlert(a):
return a.alert
case let .newChatSomeAlert(.someAlert(alert, _)):
return alert
}
}
}
@@ -176,7 +181,7 @@ struct NewChatView: View {
await MainActor.run {
creatingConnReq = false
if let apiAlert = apiAlert {
alert = .newChatSomeAlert(alert: SomeAlert(alert: apiAlert, id: "createInvitation error"))
alert = .newChatSomeAlert(alert: .someAlert(alert: apiAlert, id: "createInvitation error"))
}
}
}
@@ -310,7 +315,7 @@ private struct ConnectView: View {
// showQRCodeScanner = false
connect(pastedLink)
} else {
alert = .newChatSomeAlert(alert: SomeAlert(
alert = .newChatSomeAlert(alert: .someAlert(
alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."),
id: "pasteLinkView: code is not a SimpleX link"
))
@@ -333,14 +338,14 @@ private struct ConnectView: View {
if strIsSimplexLink(r.string) {
connect(link)
} 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."),
id: "processQRCode: code is not a SimpleX link"
))
}
case let .failure(e):
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)"),
id: "processQRCode: failure"
))
@@ -362,12 +367,11 @@ struct ScannerInView: View {
@Binding var showQRCodeScanner: Bool
let processQRCode: (_ resp: Result<ScanResult, ScanError>) -> Void
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
var scanMode: ScanMode = .continuous
var body: some View {
Group {
if showQRCodeScanner, case .authorized = cameraAuthorizationStatus {
CodeScannerView(codeTypes: [.qr], scanMode: scanMode, completion: processQRCode)
CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
.listRowBackground(Color.clear)
@@ -432,7 +436,6 @@ struct ScannerInView: View {
}
}
private func linkTextView(_ link: String) -> some View {
Text(link)
.lineLimit(1)
@@ -181,27 +181,23 @@ struct ConnectDesktopView: View {
}
private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View {
ZStack {
List {
Section("Connecting to desktop") {
ctrlDeviceNameText(session, rc)
ctrlDeviceVersionText(session)
}
List {
Section("Connecting to desktop") {
ctrlDeviceNameText(session, rc)
ctrlDeviceVersionText(session)
}
if let sessCode = session.sessionCode {
Section("Session code") {
sessionCodeText(sessCode)
}
}
Section {
disconnectButton()
if let sessCode = session.sessionCode {
Section("Session code") {
sessionCodeText(sessCode)
}
}
.navigationTitle("Connecting to desktop")
ProgressView().scaleEffect(2)
Section {
disconnectButton()
}
}
.navigationTitle("Connecting to desktop")
}
private func searchingDesktopView() -> some View {
@@ -333,10 +329,16 @@ struct ConnectDesktopView: View {
}
}
}
private func scanDesctopAddressView() -> some View {
Section("Scan QR code from desktop") {
ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processDesktopQRCode, scanMode: .oncePerCode)
CodeScannerView(codeTypes: [.qr], scanMode: .oncePerCode, completion: processDesktopQRCode)
.aspectRatio(1, contentMode: .fit)
.cornerRadius(12)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.padding(.horizontal)
}
}
+32 -32
View File
@@ -24,6 +24,11 @@
5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; };
5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.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 */; };
5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */; };
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
@@ -192,11 +197,6 @@
D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; };
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
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 */
/* Begin PBXContainerItemProxy section */
@@ -273,6 +273,11 @@
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>"; };
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>"; };
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>"; };
@@ -487,11 +492,6 @@
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; };
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 */
/* Begin PBXFrameworksBuildPhase section */
@@ -529,13 +529,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E5D68D412C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */,
5C0EA13C2C0B176B00AD2E5E /* libgmpxx.a 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 */,
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 */,
5C0EA13E2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -601,11 +601,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
E5D68D3B2C22D78C00CBA347 /* libffi.a */,
E5D68D3D2C22D78C00CBA347 /* libgmp.a */,
E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */,
E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */,
E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */,
5C0EA1382C0B176B00AD2E5E /* libffi.a */,
5C0EA1362C0B176B00AD2E5E /* libgmp.a */,
5C0EA1372C0B176B00AD2E5E /* libgmpxx.a */,
5C0EA1392C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc-ghc9.6.3.a */,
5C0EA13A2C0B176B00AD2E5E /* libHSsimplex-chat-5.8.0.5-Idqi6HXqzzs2zrnyZtMyhc.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -1552,7 +1552,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 225;
CURRENT_PROJECT_VERSION = 223;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1577,7 +1577,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES_THIN;
MARKETING_VERSION = 5.8.1;
MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1601,7 +1601,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 225;
CURRENT_PROJECT_VERSION = 223;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1626,7 +1626,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8.1;
MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1687,7 +1687,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 225;
CURRENT_PROJECT_VERSION = 223;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -1702,7 +1702,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8.1;
MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1724,7 +1724,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 225;
CURRENT_PROJECT_VERSION = 223;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -1739,7 +1739,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8.1;
MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1761,7 +1761,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 225;
CURRENT_PROJECT_VERSION = 223;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1787,7 +1787,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8.1;
MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -1812,7 +1812,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 225;
CURRENT_PROJECT_VERSION = 223;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1838,7 +1838,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.8.1;
MARKETING_VERSION = 5.8;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
+5 -9
View File
@@ -622,8 +622,7 @@ public enum ChatResponse: Decodable, Error {
case rcvStandaloneFileComplete(user: UserRef, targetPath: String, rcvFileTransfer: RcvFileTransfer)
case rcvFileCancelled(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer)
case rcvFileSndCancelled(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)
case rcvFileError(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer)
// sending file events
case sndFileStart(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
@@ -637,7 +636,6 @@ public enum ChatResponse: Decodable, Error {
case sndStandaloneFileComplete(user: UserRef, fileTransferMeta: FileTransferMeta, rcvURIs: [String])
case sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta)
case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String)
case sndFileWarning(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String)
// call events
case callInvitation(callInvitation: RcvCallInvitation)
case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
@@ -786,7 +784,6 @@ public enum ChatResponse: Decodable, Error {
case .rcvFileCancelled: return "rcvFileCancelled"
case .rcvFileSndCancelled: return "rcvFileSndCancelled"
case .rcvFileError: return "rcvFileError"
case .rcvFileWarning: return "rcvFileWarning"
case .sndFileStart: return "sndFileStart"
case .sndFileComplete: return "sndFileComplete"
case .sndFileCancelled: return "sndFileCancelled"
@@ -799,7 +796,6 @@ public enum ChatResponse: Decodable, Error {
case .sndStandaloneFileComplete: return "sndStandaloneFileComplete"
case .sndFileCancelledXFTP: return "sndFileCancelledXFTP"
case .sndFileError: return "sndFileError"
case .sndFileWarning: return "sndFileWarning"
case .callInvitation: return "callInvitation"
case .callOffer: return "callOffer"
case .callAnswer: return "callAnswer"
@@ -948,8 +944,7 @@ public enum ChatResponse: Decodable, Error {
case let .rcvFileComplete(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 .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 .rcvFileError(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 .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
@@ -962,7 +957,6 @@ public enum ChatResponse: Decodable, Error {
case let .sndStandaloneFileComplete(u, _, rcvURIs): return withUser(u, String(rcvURIs.count))
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 .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 .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))")
@@ -980,7 +974,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 .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)"
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
case let .remoteCtrlStopped(rcsState, rcStopReason): return "rcsState: \(String(describing: rcsState))\nrcStopReason: \(String(describing: rcStopReason))"
case .remoteCtrlStopped: return noDetails
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 .cmdOk: return noDetails
@@ -1777,6 +1771,8 @@ public enum ChatErrorType: Decodable {
case fileImageSize(filePath: String)
case fileNotReceived(fileId: Int64)
case fileNotApproved(fileId: Int64, unknownServers: [String])
// case xFTPRcvFile
// case xFTPSndFile
case fallbackToSMPProhibited(fileId: Int64)
case inlineFileProhibited(fileId: Int64)
case invalidQuote
+7 -57
View File
@@ -2728,7 +2728,7 @@ public enum CIStatus: Decodable {
case rcvRead
case invalid(text: String)
public var id: String {
var id: String {
switch self {
case .sndNew: return "sndNew"
case .sndSent: return "sndSent"
@@ -2809,19 +2809,11 @@ public enum SndError: Decodable {
}
}
public enum SrvError: Decodable, Equatable {
public enum SrvError: Decodable {
case host
case version
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 {
switch self {
case .host: NSLocalizedString("Server address is incompatible with network settings.", comment: "srv error text.")
@@ -3179,7 +3171,6 @@ public struct CIFile: Decodable {
case .sndComplete: return true
case .sndCancelled: return true
case .sndError: return true
case .sndWarning: return true
case .rcvInvitation: return false
case .rcvAccepted: return false
case .rcvTransfer: return false
@@ -3187,7 +3178,6 @@ public struct CIFile: Decodable {
case .rcvCancelled: return false
case .rcvComplete: return true
case .rcvError: return false
case .rcvWarning: return false
case .invalid: return false
}
}
@@ -3206,14 +3196,12 @@ public struct CIFile: Decodable {
}
case .sndCancelled: return nil
case .sndError: return nil
case .sndWarning: return sndCancelAction
case .rcvInvitation: return nil
case .rcvAccepted: return rcvCancelAction
case .rcvTransfer: return rcvCancelAction
case .rcvAborted: return nil
case .rcvCancelled: return nil
case .rcvComplete: return nil
case .rcvWarning: return rcvCancelAction
case .rcvError: return nil
case .invalid: return nil
}
@@ -3322,64 +3310,35 @@ public enum CIFileStatus: Decodable, Equatable {
case sndTransfer(sndProgress: Int64, sndTotal: Int64)
case sndComplete
case sndCancelled
case sndError(sndFileError: FileError)
case sndWarning(sndFileError: FileError)
case sndError
case rcvInvitation
case rcvAccepted
case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64)
case rcvAborted
case rcvComplete
case rcvCancelled
case rcvError(rcvFileError: FileError)
case rcvWarning(rcvFileError: FileError)
case rcvError
case invalid(text: String)
public var id: String {
var id: String {
switch self {
case .sndStored: return "sndStored"
case let .sndTransfer(sndProgress, sndTotal): return "sndTransfer \(sndProgress) \(sndTotal)"
case .sndComplete: return "sndComplete"
case .sndCancelled: return "sndCancelled"
case let .sndError(sndFileError): return "sndError \(sndFileError)"
case let .sndWarning(sndFileError): return "sndWarning \(sndFileError)"
case .sndError: return "sndError"
case .rcvInvitation: return "rcvInvitation"
case .rcvAccepted: return "rcvAccepted"
case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)"
case .rcvAborted: return "rcvAborted"
case .rcvComplete: return "rcvComplete"
case .rcvCancelled: return "rcvCancelled"
case let .rcvError(rcvFileError): return "rcvError \(rcvFileError)"
case let .rcvWarning(rcvFileError): return "rcvWarning \(rcvFileError)"
case .rcvError: return "rcvError"
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 {
case text(String)
case link(text: String, preview: LinkPreview)
@@ -3438,15 +3397,6 @@ public enum MsgContent: Equatable {
}
}
public var isMediaOrFileAttachment: Bool {
switch self {
case .image: true
case .video: true
case .file: true
default: false
}
}
var cmdString: String {
"json \(encodeJSON(self))"
}
@@ -2,6 +2,5 @@
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="highOrLowLight">#8b8786</color>
<color name="window_background_dark">#121212</color>
</resources>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@color/highOrLowLight" />
<solid android:color="#8b8786" />
<size android:width="1dp" />
</shape>
@@ -2660,7 +2660,6 @@ data class CIFile(
is CIFileStatus.SndComplete -> true
is CIFileStatus.SndCancelled -> true
is CIFileStatus.SndError -> true
is CIFileStatus.SndWarning -> true
is CIFileStatus.RcvInvitation -> false
is CIFileStatus.RcvAccepted -> false
is CIFileStatus.RcvTransfer -> false
@@ -2668,7 +2667,6 @@ data class CIFile(
is CIFileStatus.RcvCancelled -> false
is CIFileStatus.RcvComplete -> true
is CIFileStatus.RcvError -> false
is CIFileStatus.RcvWarning -> false
is CIFileStatus.Invalid -> false
}
@@ -2684,7 +2682,6 @@ data class CIFile(
}
is CIFileStatus.SndCancelled -> null
is CIFileStatus.SndError -> null
is CIFileStatus.SndWarning -> sndCancelAction
is CIFileStatus.RcvInvitation -> null
is CIFileStatus.RcvAccepted -> rcvCancelAction
is CIFileStatus.RcvTransfer -> rcvCancelAction
@@ -2692,7 +2689,6 @@ data class CIFile(
is CIFileStatus.RcvCancelled -> null
is CIFileStatus.RcvComplete -> null
is CIFileStatus.RcvError -> null
is CIFileStatus.RcvWarning -> rcvCancelAction
is CIFileStatus.Invalid -> null
}
@@ -2866,16 +2862,14 @@ sealed class CIFileStatus {
@Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus()
@Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus()
@Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus()
@Serializable @SerialName("sndError") class SndError(val sndFileError: FileError): CIFileStatus()
@Serializable @SerialName("sndWarning") class SndWarning(val sndFileError: FileError): CIFileStatus()
@Serializable @SerialName("sndError") object SndError: CIFileStatus()
@Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
@Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
@Serializable @SerialName("rcvAborted") object RcvAborted: CIFileStatus()
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
@Serializable @SerialName("rcvError") class RcvError(val rcvFileError: FileError): CIFileStatus()
@Serializable @SerialName("rcvWarning") class RcvWarning(val rcvFileError: FileError): CIFileStatus()
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
@Serializable @SerialName("invalid") class Invalid(val text: String): CIFileStatus()
val sent: Boolean get() = when (this) {
@@ -2884,7 +2878,6 @@ sealed class CIFileStatus {
is SndComplete -> true
is SndCancelled -> true
is SndError -> true
is SndWarning -> true
is RcvInvitation -> false
is RcvAccepted -> false
is RcvTransfer -> false
@@ -2892,26 +2885,10 @@ sealed class CIFileStatus {
is RcvComplete -> false
is RcvCancelled -> false
is RcvError -> false
is RcvWarning -> 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")
@Serializable(with = MsgContentSerializer::class)
sealed class MsgContent {
@@ -2925,20 +2902,6 @@ sealed class 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()
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() =
if (this is MCUnknown) "json $json" else "json ${json.encodeToString(this)}"
}
@@ -1,18 +1,9 @@
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 androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.setNetCfg
import chat.simplex.common.model.ChatModel.updatingChatsMutex
@@ -21,6 +12,7 @@ import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
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.onboarding.OnboardingStage
import chat.simplex.common.views.usersettings.*
@@ -28,7 +20,6 @@ import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.YamlConfiguration
import chat.simplex.res.MR
import com.russhwolf.settings.Settings
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.sync.withLock
@@ -2035,11 +2026,6 @@ object ChatController {
cleanupFile(r.chatItem_)
}
}
is CR.RcvFileWarning -> {
if (r.chatItem_ != null) {
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
}
}
is CR.SndFileStart ->
chatItemSimpleUpdate(rhId, r.user, r.chatItem)
is CR.SndFileComplete -> {
@@ -2066,11 +2052,6 @@ object ChatController {
cleanupFile(r.chatItem_)
}
}
is CR.SndFileWarning -> {
if (r.chatItem_ != null) {
chatItemSimpleUpdate(rhId, r.user, r.chatItem_)
}
}
is CR.CallInvitation -> {
chatModel.callManager.reportNewIncomingCall(r.callInvitation.copy(remoteHostId = rhId))
}
@@ -2203,43 +2184,15 @@ object ChatController {
val sess = chatModel.remoteCtrlSession.value
if (sess != null) {
chatModel.remoteCtrlSession.value = null
ModalManager.fullscreen.closeModals()
fun showAlert(chatError: ChatError) {
when {
r.rcStopReason is RemoteCtrlStopReason.ConnectionFailed
&& r.rcStopReason.chatError is ChatError.ChatErrorAgent
&& r.rcStopReason.chatError.agentError is AgentErrorType.RCP
&& r.rcStopReason.chatError.agentError.rcpErr is RCErrorType.IDENTITY ->
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)
}
}
}
)
}
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.remote_ctrl_was_disconnected_title),
if (chatError is ChatError.ChatErrorRemoteCtrl) {
chatError.remoteCtrlError.localizedString
} else {
generalGetString(MR.strings.remote_ctrl_disconnected_with_reason).format(chatError.string)
}
)
}
when (r.rcStopReason) {
is RemoteCtrlStopReason.DiscoveryFailed -> showAlert(r.rcStopReason.chatError)
@@ -4369,7 +4322,6 @@ sealed class 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("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
@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()
@@ -4382,8 +4334,7 @@ sealed class 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("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, val errorMessage: String): CR()
@Serializable @SerialName("sndFileWarning") class SndFileWarning(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR()
@Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR()
// call events
@Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR()
@Serializable @SerialName("callInvitations") class CallInvitations(val callInvitations: List<RcvCallInvitation>): CR()
@@ -4542,7 +4493,6 @@ sealed class CR {
is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
is SndFileRedirectStartXFTP -> "sndFileRedirectStartXFTP"
is RcvFileError -> "rcvFileError"
is RcvFileWarning -> "rcvFileWarning"
is SndFileStart -> "sndFileStart"
is SndFileComplete -> "sndFileComplete"
is SndFileRcvCancelled -> "sndFileRcvCancelled"
@@ -4552,7 +4502,6 @@ sealed class CR {
is SndStandaloneFileComplete -> "sndStandaloneFileComplete"
is SndFileCancelledXFTP -> "sndFileCancelledXFTP"
is SndFileError -> "sndFileError"
is SndFileWarning -> "sndFileWarning"
is CallInvitations -> "callInvitations"
is CallInvitation -> "callInvitation"
is CallOffer -> "callOffer"
@@ -4703,7 +4652,6 @@ sealed class CR {
is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
is RcvStandaloneFileComplete -> withUser(user, targetPath)
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 SndStandaloneFileCreated -> noDetails()
is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem))
@@ -4715,8 +4663,7 @@ sealed class CR {
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString())
is SndFileCancelledXFTP -> 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 SndFileError -> withUser(user, json.encodeToString(chatItem_))
is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}"
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)}")
@@ -4753,7 +4700,7 @@ sealed class CR {
(if (remoteCtrl_ == null) "null" else json.encodeToString(remoteCtrl_)) +
"\nsessionCode: $sessionCode"
is RemoteCtrlConnected -> json.encodeToString(remoteCtrl)
is RemoteCtrlStopped -> "rcsState: $rcsState\nrcsStopReason: $rcStopReason"
is RemoteCtrlStopped -> noDetails()
is ContactPQAllowed -> withUser(user, "contact: ${contact.id}\npqEncryption: $pqEncryption")
is ContactPQEnabled -> withUser(user, "contact: ${contact.id}\npqEnabled: $pqEnabled")
is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" +
@@ -5035,6 +4982,8 @@ sealed class ChatErrorType {
is FileImageSize -> "fileImageSize"
is FileNotReceived -> "fileNotReceived"
is FileNotApproved -> "fileNotApproved"
// is XFTPRcvFile -> "xftpRcvFile"
// is XFTPSndFile -> "xftpSndFile"
is FallbackToSMPProhibited -> "fallbackToSMPProhibited"
is InlineFileProhibited -> "inlineFileProhibited"
is InvalidQuote -> "invalidQuote"
@@ -5112,6 +5061,8 @@ sealed class ChatErrorType {
@Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType()
@Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType()
@Serializable @SerialName("fileNotApproved") class FileNotApproved(val fileId: Long, val unknownServers: List<String>): ChatErrorType()
// @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType()
// @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType()
@Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType()
@Serializable @SerialName("inlineFileProhibited") class InlineFileProhibited(val fileId: Long): ChatErrorType()
@Serializable @SerialName("invalidQuote") object InvalidQuote: ChatErrorType()
@@ -1,7 +1,6 @@
package chat.simplex.common.platform
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.currentUser
import chat.simplex.common.views.helpers.*
@@ -58,9 +57,6 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
try {
if (chatModel.ctrlInitInProgress.value) return
chatModel.ctrlInitInProgress.value = true
if (!appPrefs.storeDBPassphrase.get() && !appPrefs.initialRandomDBPassphrase.get()) {
ksDatabasePassword.remove()
}
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
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)
@@ -224,8 +224,6 @@ object ThemeManager {
s.length == 1 -> "#ff$s$s$s$s$s$s"
s.length == 2 -> "#ff$s$s$s"
s.length == 3 -> "#ff$s$s"
s.length == 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 -> "#ff$s"
else -> "#$s"
@@ -31,7 +31,6 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import kotlinx.serialization.encodeToString
sealed class CIInfoTab {
class Delivery(val memberDeliveryStatuses: List<MemberDeliveryStatus>): CIInfoTab()
@@ -217,27 +216,6 @@ 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
fun Details() {
AppBarTitle(stringResource(if (ci.localNote) MR.strings.saved_message_title else if (sent) MR.strings.sent_message else MR.strings.received_message))
@@ -266,10 +244,6 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
if (devTools) {
InfoRow(stringResource(MR.strings.info_row_database_id), ci.meta.itemId.toString())
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))
}
}
}
}
@@ -557,10 +531,6 @@ fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItem
if (devTools) {
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_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
if (qi != null) {
@@ -962,8 +962,17 @@ fun BoxWithConstraintsScope.ChatItemsList(
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
) {
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
if (it == DismissValue.DismissedToStart) {
val dismissState = rememberDismissState(initialValue = DismissValue.Default) { false }
val directions = setOf(DismissDirection.EndToStart)
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 {
if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chat.chatInfo !is ChatInfo.Local) {
if (composeState.value.editing) {
@@ -974,13 +983,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
}
}
false
}
val swipeableModifier = SwipeToDismissModifier(
state = dismissState,
directions = setOf(DismissDirection.EndToStart),
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
)
val provider = {
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
scope.launch {
@@ -203,7 +203,7 @@ fun GroupChatInfoLayout(
scope.launch { listState.scrollToItem(0) }
}
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
val filteredMembers = remember(members) { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim().lowercase()) } } }
val filteredMembers = remember(members) { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } }
// LALAL strange scrolling
LazyColumnWithScrollBar(
Modifier
@@ -87,26 +87,6 @@ fun CIFileView(
)
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() -> {
withLongRunningApi(slow = 600_000) {
var filePath = getLoadedFilePath(file)
@@ -177,7 +157,6 @@ fun CIFileView(
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled))
is CIFileStatus.SndCancelled -> 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 ->
if (fileSizeValid(file))
fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary)
@@ -195,7 +174,6 @@ fun CIFileView(
is CIFileStatus.RcvComplete -> fileIcon()
is CIFileStatus.RcvCancelled -> 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))
}
} else {
@@ -70,16 +70,14 @@ fun CIImageView(
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.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.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image)
is CIFileStatus.RcvTransfer -> progressIndicator()
is CIFileStatus.RcvComplete -> {}
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.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)
else -> {}
}
}
}
@@ -203,8 +201,8 @@ fun CIImageView(
} else {
imageView(base64ToBitmap(image), onClick = {
if (file != null) {
when {
file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted ->
when (file.fileStatus) {
CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
if (fileSizeValid()) {
receiveFile(file.fileId)
} else {
@@ -213,7 +211,7 @@ fun CIImageView(
String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
)
}
file.fileStatus is CIFileStatus.RcvAccepted ->
CIFileStatus.RcvAccepted ->
when (file.fileProtocol) {
FileProtocol.XFTP ->
AlertManager.shared.showAlertMsg(
@@ -227,29 +225,9 @@ fun CIImageView(
)
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.fileStatus is CIFileStatus.RcvTransfer -> {} // ?
file.fileStatus is CIFileStatus.RcvComplete -> {} // ?
file.fileStatus is CIFileStatus.RcvCancelled -> {} // TODO
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
CIFileStatus.RcvComplete -> {} // ?
CIFileStatus.RcvCancelled -> {} // TODO
else -> {}
}
}
@@ -107,7 +107,7 @@ fun CIVideoView(
}
}
}
fileStatusIcon(file)
loadingIndicator(file)
}
}
@@ -339,13 +339,11 @@ private fun progressIndicator() {
}
@Composable
private fun fileIcon(icon: Painter, stringId: StringResource, onClick: (() -> Unit)? = null) {
var modifier = Modifier.fillMaxSize()
modifier = if (onClick != null) { modifier.clickable { onClick() } } else { modifier }
private fun fileIcon(icon: Painter, stringId: StringResource) {
Icon(
icon,
stringResource(stringId),
modifier,
Modifier.fillMaxSize(),
tint = Color.White
)
}
@@ -366,7 +364,7 @@ private fun progressCircle(progress: Long, total: Long) {
}
@Composable
private fun fileStatusIcon(file: CIFile?) {
private fun loadingIndicator(file: CIFile?) {
if (file != null) {
Box(
Modifier
@@ -389,28 +387,7 @@ private fun fileStatusIcon(file: CIFile?) {
}
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.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.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
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.RcvTransfer ->
@@ -420,31 +397,10 @@ private fun fileStatusIcon(file: CIFile?) {
progressIndicator()
}
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.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.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
else -> {}
}
}
}
@@ -252,81 +252,6 @@ 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
private fun VoiceMsgIndicator(
file: CIFile?,
@@ -343,73 +268,39 @@ private fun VoiceMsgIndicator(
) {
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
val strokeColor = MaterialTheme.colors.primary
when {
file?.fileStatus is CIFileStatus.SndStored ->
if (file.fileProtocol == FileProtocol.LOCAL && progress != null && duration != null) {
PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, play, pause, longClick = longClick)
} else {
VoiceMsgLoadingProgressIndicator()
if (file != null && file.loaded && progress != null && duration != null) {
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
)
}
file?.fileStatus is CIFileStatus.SndTransfer ->
VoiceMsgLoadingProgressIndicator()
file != null && file.fileStatus is CIFileStatus.SndError ->
FileStatusIcon(
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 ->
} else {
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick)
}
} else {
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick)
file?.fileStatus is CIFileStatus.RcvTransfer || file?.fileStatus is CIFileStatus.RcvAccepted ->
VoiceMsgLoadingProgressIndicator()
file?.fileStatus is CIFileStatus.RcvAborted ->
} else if (file?.fileStatus is CIFileStatus.RcvTransfer
|| file?.fileStatus is CIFileStatus.RcvAccepted
) {
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)
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 ->
} else {
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick)
}
}
}
@@ -189,6 +189,13 @@ fun FramedItemView(
val receivedColor = MaterialTheme.appColors.receivedMessage
Box(Modifier
.clip(RoundedCornerShape(18.dp))
.background(
when {
transparentBackground -> Color.Transparent
sent -> MaterialTheme.colors.background
else -> MaterialTheme.colors.background
}
)
.background(
when {
transparentBackground -> Color.Transparent
@@ -9,55 +9,30 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import chat.simplex.common.views.helpers.ProfileImage
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
@Composable
fun ShareListNavLinkView(
chat: Chat,
chatModel: ChatModel,
isMediaOrFileAttachment: Boolean,
isVoice: Boolean,
hasSimplexLink: Boolean
) {
fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) {
val stopped = chatModel.chatRunning.value == false
when (chat.chatInfo) {
is ChatInfo.Direct -> {
val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
is ChatInfo.Direct ->
ShareListNavLinkLayout(
chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited) },
click = {
if (voiceProhibited) {
showForwardProhibitedByPrefAlert()
} else {
directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel)
}
},
chatLinkPreview = { SharePreviewView(chat) },
click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) },
stopped
)
}
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
is ChatInfo.Group ->
ShareListNavLinkLayout(
chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref) },
click = {
if (prohibitedByPref) {
showForwardProhibitedByPrefAlert()
} else {
groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel)
}
},
chatLinkPreview = { SharePreviewView(chat) },
click = { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) },
stopped
)
}
is ChatInfo.Local ->
ShareListNavLinkLayout(
chatLinkPreview = { SharePreviewView(chat, disabled = false) },
chatLinkPreview = { SharePreviewView(chat) },
click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) },
stopped
)
@@ -65,13 +40,6 @@ fun ShareListNavLinkView(
}
}
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
private fun ShareListNavLinkLayout(
chatLinkPreview: @Composable () -> Unit,
@@ -85,7 +53,7 @@ private fun ShareListNavLinkLayout(
}
@Composable
private fun SharePreviewView(chat: Chat, disabled: Boolean) {
private fun SharePreviewView(chat: Chat) {
Row(
Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -102,7 +70,7 @@ private fun SharePreviewView(chat: Chat, disabled: Boolean) {
}
Text(
chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
color = if (disabled) MaterialTheme.colors.secondary else if (chat.chatInfo.incognito) Indigo else Color.Unspecified
color = if (chat.chatInfo.incognito) Indigo else Color.Unspecified
)
}
}
@@ -31,44 +31,13 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
scaffoldState = scaffoldState,
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)) {
Column(
modifier = Modifier
.fillMaxSize()
) {
if (chatModel.chats.isNotEmpty()) {
ShareList(
chatModel,
search = searchInList,
isMediaOrFileAttachment = isMediaOrFileAttachment,
isVoice = isVoice,
hasSimplexLink = hasSimplexLink
)
ShareList(chatModel, search = searchInList)
} else {
EmptyList()
}
@@ -85,11 +54,6 @@ 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
private fun EmptyList() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -177,13 +141,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
}
@Composable
private fun ShareList(
chatModel: ChatModel,
search: String,
isMediaOrFileAttachment: Boolean,
isVoice: Boolean,
hasSimplexLink: Boolean
) {
private fun ShareList(chatModel: ChatModel, search: String) {
val chats by remember(search) {
derivedStateOf {
val sorted = chatModel.chats.toList().sortedByDescending { it.chatInfo is ChatInfo.Local }
@@ -198,13 +156,7 @@ private fun ShareList(
modifier = Modifier.fillMaxWidth()
) {
items(chats) { chat ->
ShareListNavLinkView(
chat,
chatModel,
isMediaOrFileAttachment = isMediaOrFileAttachment,
isVoice = isVoice,
hasSimplexLink = hasSimplexLink
)
ShareListNavLinkView(chat, chatModel)
}
}
}
@@ -27,7 +27,6 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
@@ -41,8 +40,9 @@ import kotlin.math.log2
@Composable
fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
val progressIndicator = remember { mutableStateOf(false) }
val useKeychain = remember { mutableStateOf(appPrefs.storeDBPassphrase.get()) }
val initialRandomDBPassphrase = remember { mutableStateOf(appPrefs.initialRandomDBPassphrase.get()) }
val prefs = m.controller.appPrefs
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
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
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
@@ -54,6 +54,7 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
) {
DatabaseEncryptionLayout(
useKeychain,
prefs,
m.chatDbEncrypted.value,
currentKey,
newKey,
@@ -64,16 +65,7 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
migration,
onConfirmEncrypt = {
withLongRunningApi {
encryptDatabase(
currentKey = currentKey,
newKey = newKey,
confirmNewKey = confirmNewKey,
initialRandomDBPassphrase = initialRandomDBPassphrase,
useKeychain = useKeychain,
storedKey = storedKey,
progressIndicator = progressIndicator,
migration = migration
)
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator, migration)
}
}
)
@@ -97,6 +89,7 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) {
@Composable
fun DatabaseEncryptionLayout(
useKeychain: MutableState<Boolean>,
prefs: AppPreferences,
chatDbEncrypted: Boolean?,
currentKey: MutableState<String>,
newKey: MutableState<String>,
@@ -126,14 +119,14 @@ fun DatabaseEncryptionLayout(
enabled = (!initialRandomDBPassphrase.value && !progressIndicator.value) || migration
) { checked ->
if (checked) {
setUseKeychain(true, useKeychain, migration)
setUseKeychain(true, useKeychain, prefs, migration)
} else if (storedKey.value && !migration) {
// Don't show in migration process since it will remove the key after successful encryption
removePassphraseAlert {
removePassphraseFromKeyChain(useKeychain, storedKey, false)
removePassphraseFromKeyChain(useKeychain, prefs, storedKey, false)
}
} else {
setUseKeychain(false, useKeychain, migration)
setUseKeychain(false, useKeychain, prefs, migration)
}
}
@@ -279,17 +272,17 @@ fun resetFormAfterEncryption(
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
}
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, migration: Boolean) {
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences, migration: Boolean) {
useKeychain.value = value
// Postpone it when migrating to the end of encryption process
if (!migration) {
appPrefs.storeDBPassphrase.set(value)
prefs.storeDBPassphrase.set(value)
}
}
private fun removePassphraseFromKeyChain(useKeychain: MutableState<Boolean>, storedKey: MutableState<Boolean>, migration: Boolean) {
private fun removePassphraseFromKeyChain(useKeychain: MutableState<Boolean>, prefs: AppPreferences, storedKey: MutableState<Boolean>, migration: Boolean) {
DatabaseUtils.ksDatabasePassword.remove()
setUseKeychain(false, useKeychain, migration)
setUseKeychain(false, useKeychain, prefs, migration)
storedKey.value = false
}
@@ -422,14 +415,15 @@ suspend fun encryptDatabase(
migration: Boolean,
): Boolean {
val m = ChatModel
val prefs = ChatController.appPrefs
progressIndicator.value = true
return try {
appPrefs.encryptionStartedAt.set(Clock.System.now())
prefs.encryptionStartedAt.set(Clock.System.now())
if (!m.chatDbChanged.value) {
m.controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
}
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
appPrefs.encryptionStartedAt.set(null)
prefs.encryptionStartedAt.set(null)
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
when {
sqliteError is SQLiteError.ErrorNotADatabase -> {
@@ -457,8 +451,8 @@ suspend fun encryptDatabase(
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
if (useKeychain.value) {
DatabaseUtils.ksDatabasePassword.set(new)
} else {
removePassphraseFromKeyChain(useKeychain, storedKey, migration)
} else if (migration) {
removePassphraseFromKeyChain(useKeychain, prefs, storedKey, true)
}
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
@@ -529,6 +523,7 @@ fun PreviewDatabaseEncryptionLayout() {
SimpleXTheme {
DatabaseEncryptionLayout(
useKeychain = remember { mutableStateOf(true) },
prefs = AppPreferences(),
chatDbEncrypted = true,
currentKey = remember { mutableStateOf("") },
newKey = remember { mutableStateOf("") },
@@ -448,9 +448,6 @@ private fun stopChat(m: ChatModel, progressIndicator: MutableState<Boolean>? = n
progressIndicator?.value = true
stopChatAsync(m)
platform.androidChatStopped()
// close chat view for desktop
chatModel.chatId.value = null
ModalManager.end.closeModals()
onStop?.invoke()
} catch (e: Error) {
m.chatRunning.value = true
@@ -37,7 +37,7 @@ fun SwipeToDismissModifier(
return Modifier.swipeable(
state = state,
anchors = anchors,
thresholds = { _, _ -> FractionalThreshold(0.99f) },
thresholds = { _, _ -> FractionalThreshold(0.5f) },
orientation = Orientation.Horizontal,
reverseDirection = isRtl,
).offset { IntOffset(state.offset.value.roundToInt(), 0) }
@@ -39,7 +39,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) {
withBGApi {
val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile)
if (groupInfo != null) {
chatModel.updateGroup(rhId = rhId, groupInfo)
chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf()))
chatModel.chatItems.clear()
chatModel.chatItemStatuses.clear()
chatModel.chatId.value = groupInfo.id
@@ -31,6 +31,7 @@ import kotlinx.coroutines.delay
fun SetupDatabasePassphrase(m: ChatModel) {
val progressIndicator = remember { mutableStateOf(false) }
val prefs = m.controller.appPrefs
val saveInPreferences = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
// 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 "") }
@@ -57,16 +58,7 @@ fun SetupDatabasePassphrase(m: ChatModel) {
prefs.storeDBPassphrase.set(false)
val newKeyValue = newKey.value
val success = encryptDatabase(
currentKey = currentKey,
newKey = newKey,
confirmNewKey = confirmNewKey,
initialRandomDBPassphrase = mutableStateOf(true),
useKeychain = mutableStateOf(false),
storedKey = mutableStateOf(true),
progressIndicator = progressIndicator,
migration = false
)
val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator, false)
if (success) {
startChat(newKeyValue)
nextStep()
@@ -15,7 +15,6 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
@@ -167,24 +166,6 @@ private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
SectionView {
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
@@ -28,7 +28,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ClickableText
@@ -59,8 +58,6 @@ fun NetworkAndServersView() {
smpProxyFallback = smpProxyFallback,
proxyPort = proxyPort,
toggleSocksProxy = { enable ->
val def = NetCfg.defaults
val proxyDef = NetCfg.proxyDefaults
if (enable) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.network_enable_socks),
@@ -68,19 +65,7 @@ fun NetworkAndServersView() {
confirmText = generalGetString(MR.strings.confirm_verb),
onConfirm = {
withBGApi {
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)
}
val conf = NetCfg.proxyDefaults.withHostPort(chatModel.controller.appPrefs.networkProxyHostPort.get())
chatModel.controller.apiSetNetworkConfig(conf)
chatModel.controller.setNetCfg(conf)
networkUseSocksProxy.value = true
@@ -95,19 +80,7 @@ fun NetworkAndServersView() {
confirmText = generalGetString(MR.strings.confirm_verb),
onConfirm = {
withBGApi {
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)
}
val conf = NetCfg.defaults
chatModel.controller.apiSetNetworkConfig(conf)
chatModel.controller.setNetCfg(conf)
networkUseSocksProxy.value = false
@@ -271,11 +271,6 @@
<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>
<!-- 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) -->
<string name="reply_verb">Reply</string>
<string name="share_verb">Share</string>
@@ -358,8 +353,6 @@
<string name="share_image">Share media…</string>
<string name="share_file">Share file…</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 -->
<string name="attach">Attach</string>
@@ -415,8 +408,6 @@
<string name="error_saving_file">Error saving 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="file_error">File error</string>
<string name="temporary_file_error">Temporary file error</string>
<!-- Voice messages -->
<string name="voice_message">Voice message</string>
@@ -1412,8 +1403,6 @@
<string name="info_row_database_id">Database ID</string>
<string name="info_row_debug_delivery">Debug delivery</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_created_at">Created at</string>
<string name="info_row_received_at">Received at</string>
@@ -1422,8 +1411,6 @@
<string name="info_row_disappears_at">Disappears at</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_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_created_at">Created at: %s</string>
<string name="share_text_received_at">Received at: %s</string>
@@ -1923,9 +1910,6 @@
<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_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="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>
@@ -14,10 +14,8 @@ import com.russhwolf.settings.*
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.desc.desc
import kotlinx.coroutines.*
import java.io.File
import java.util.*
import java.util.concurrent.Executors
@Composable
actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font =
@@ -61,11 +59,8 @@ private val settingsThemesProps =
Properties()
.also { props -> try { settingsThemesFile.reader().use { props.load(it) } } catch (e: Exception) { /**/ } }
private val settingsWriterThread = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
actual val settings: Settings = PropertiesSettings(settingsProps) { CoroutineScope(settingsWriterThread).launch { settingsFile.writer().use { settingsProps.store(it, "") } } }
actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { CoroutineScope(settingsWriterThread).launch { settingsThemesFile.writer().use { settingsThemesProps.store(it, "") } } }
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, "") } } }
actual fun windowOrientation(): WindowOrientation =
if (simplexWindowState.windowState.size.width > simplexWindowState.windowState.size.height) {
+5 -7
View File
@@ -21,17 +21,15 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=5.8.1
android.version_code=221
android.version_name=5.8-beta.5
android.version_code=218
desktop.version_name=5.8.1
desktop.version_code=54
desktop.version_name=5.8-beta.5
desktop.version_code=52
kotlin.version=1.9.23
gradle.plugin.version=8.2.0
compose.version=1.6.1
compose.version=1.6.10
@@ -2,9 +2,10 @@
layout: layouts/article.html
title: "SimpleX network: private message routing, v5.8 released with IP address protection and chat themes"
date: 2024-06-04
previewBody: blog_previews/20240604.html
image: images/20240604-routing.png
imageBottom: true
# previewBody: blog_previews/20240426.html
draft: true
# image: images/20240426-profile.png
# imageBottom: true
permalink: "/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html"
---
@@ -12,159 +13,6 @@ permalink: "/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes
**Published:** June 4, 2024
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.
TODO
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
This is a permalink for release announcement.
-14
View File
@@ -1,19 +1,5 @@
# 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)
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.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 KiB

+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 8a3b72458f917e9867f4e3640dda0fa1827ff6cf
tag: 2e4f507919dd3a08e5c4106fde193fee4414de3c
source-repository-package
type: git
+1 -1
View File
@@ -7,7 +7,7 @@ revision: 11.02.2024
| Updated 23.03.2024 | Languages: EN |
# Download SimpleX apps
The latest stable version is v5.8.
The latest stable version is v5.6.
You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases).
+19 -220
View File
@@ -1,6 +1,6 @@
---
title: Hosting your own SMP Server
revision: 03.06.2024
revision: 28.05.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) |
@@ -21,7 +21,6 @@ revision: 03.06.2024
- [Tor: installation and configuration](#tor-installation-and-configuration)
- [Installation for onion address](#installation-for-onion-address)
- [SOCKS port for SMP PROXY](#socks-port-for-smp-proxy)
- [Server information page](#server-information-page)
- [Documentation](#documentation)
- [SMP server address](#smp-server-address)
- [Systemd commands](#systemd-commands)
@@ -228,44 +227,6 @@ 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:
```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]
# The server uses STM memory for persistence,
# that will be lost on restart (e.g., as with redis).
@@ -328,21 +289,6 @@ websockets: off
disconnect: off
# ttl: 43200
# 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
@@ -597,110 +543,6 @@ 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
All necessary files for `smp-server` are located in `/etc/opt/simplex/` folder.
@@ -793,69 +635,26 @@ 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:
```sh
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,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,monthMsgQueues
```
| Field number | Field name | Field Description |
| ------------- | ---------------------------- | -------------------------- |
| 1 | `fromTime` | Date of statistics |
| Messaging queue: |
| 2 | `qCreated` | Created |
| 3 | `qSecured` | Established |
| 4 | `qDeleted` | Deleted |
| Messages: |
| 5 | `msgSent` | Sent |
| 6 | `msgRecv` | Received |
| 7 | `dayMsgQueues` | Active queues in a day |
| 8 | `weekMsgQueues` | Active queues in a week |
| 9 | `monthMsgQueues` | Active queues in a month |
| Messages with "notification" flag |
| 10 | `msgSentNtf` | Sent |
| 11 | `msgRecvNtf` | Received |
| 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 |
- `fromTime` - timestamp; date and time of event
- `qCreated` - int; created queues
- `qSecured` - int; established queues
- `qDeleted` - int; deleted queues
- `msgSent` - int; sent messages
- `msgRecv` - int; received messages
- `dayMsgQueues` - int; active queues in a day
- `weekMsgQueues` - int; active queues in a week
- `monthMsgQueues` - int; active queues in a month
To import `csv` to `Grafana` one should:
+3 -42
View File
@@ -17,23 +17,18 @@ SimpleX Chat Protocol is a protocol used by SimpleX Chat clients to exchange mes
The scope of SimpleX Chat Protocol is application level messages, both for chat functionality, related to the conversations between the clients, and extensible for any other application functions. Currently supported chat functions:
- direct and group messages,
- message replies (quoting), message editing, forwarded messages and message deletions,
- message attachments: images, videos, voice messages and files,
- message replies (quoting), forwarded messages and message deletions,
- message attachments: images and files,
- creating and managing chat groups,
- invitation and signalling for audio/video WebRTC calls.
## General message format
SimpleX Chat protocol supports these message formats:
SimpleX Chat protocol supports two message formats:
- JSON-based format for chat and application messages.
- compressed format for adapting larger messages to reduced size of message envelope, caused by addition of PQ encryption keys to SMP agent message envelope.
- binary format for sending files or any other binary data.
JSON-based message format supports batching inside a single container message, by encoding list of messages as JSON array.
Current implementation of chat protocol in SimpleX Chat uses SimpleX File Transfer Protocol (XFTP) for file transfer, with passing file description as chat protocol messages, instead passing files in binary format via SMP connections.
### JSON format for chat and application messages
This document uses JTD schemas [RFC 8927](https://www.rfc-editor.org/rfc/rfc8927.html) to define the properties of chat messages, with some additional restrictions on message properties included in metadata member of JTD schemas. In case of any contradiction between JSON examples and JTD schema the latter MUST be considered correct.
@@ -82,22 +77,8 @@ For example, this message defines a simple text message `"hello!"`:
`params` property includes message data, depending on `event`, as defined below and in [JTD schema](./simplex-chat.schema.json).
### Compressed format
The syntax of compressed message is defined by the following ABNF notation:
```abnf
compressedMessage = %s"X" 1*15780 OCTET; compressed message data
```
Compressed message is required to fit into 13388 bytes, accounting for agent overhead (see Protocol's maxCompressedMsgLength).
The actual JSON message is required to fit into 15610 bytes, accounting for group message forwarding (x.grp.msg.forward) overhead (see Protocol's maxEncodedMsgLength).
### Binary format for sending files
> Note: Planned to be deprecated. No longer used for file transfer in SimpleX Chat implementation of chat protocol.
SimpleX Chat clients use separate connections to send files using a binary format. File chunk size send in each message MUST NOT be bigger than 15,780 bytes to fit into 16kb (16384 bytes) transport block.
The syntax of each message used to send files is defined by the following ABNF notation:
@@ -136,9 +117,7 @@ SimpleX Chat Protocol supports the following message types passed in `event` pro
- `x.contact` - contact profile and additional data sent as part of contact request to a long-term contact address.
- `x.info*` - messages to send, update and de-duplicate contact profiles.
- `x.msg.*` - messages to create, update and delete content chat items.
- `x.msg.file.descr` - message to transfer XFTP file description.
- `x.file.*` - messages to accept and cancel sending files (see files sub-protocol).
- `x.direct.del` - message to notify about contact deletion.
- `x.grp.*` - messages used to manage groups and group members (see group sub-protocol).
- `x.call.*` - messages to invite to WebRTC calls and send signalling messages.
- `x.ok` - message sent during connection handshake.
@@ -176,8 +155,6 @@ Message content can be one of four types:
- `text` - no file attachment is expected for this format, `text` property MUST be non-empty.
- `file` - attached file is required, `text` property MAY be empty.
- `image` - attached file is required, `text` property MAY be empty.
- `video` - attached file is required, `text` property MAY be empty.
- `voice` - attached file is required, `text` property MAY be empty.
- `link` - no file attachment is expected, `text` property MUST be non-empty. `preview` property contains information about link preview.
See `/definition/msgContent` in [JTD schema](./simplex-chat.schema.json) for message container format.
@@ -204,8 +181,6 @@ File attachment can optionally include connection address to receive the file -
`x.file.cancel` message is sent to notify the recipient that sending of the file was cancelled. It is sent in response to accepting the file with `x.file.acpt.inv` message. It is sent in the same connection where the file was offered.
`x.msg.file.descr` message is used to send XFTP file description. File descriptions that don't fit into a single chat protocol message are sent in parts, with messages including part number (`fileDescrPartNo`) and description completion marker (`fileDescrComplete`). Recipient client accumulates description parts and starts file download upon completing file description.
## Sub-protocol for chat groups
### Decentralized design for chat groups
@@ -222,8 +197,6 @@ The diagram below shows the sequence of messages sent between the users' clients
![Adding member to the group](./diagrams/group.svg)
While introduced members establish connection inside group, inviting member forwards messages between them by sending `x.grp.msg.forward` messages. When introduced members finalize connection, they notify inviting member to stop forwarding via `x.grp.mem.con` message.
### Member roles
Currently members can have one of three roles - `owner`, `admin` and `member`. The user that created the group is self-assigned owner role, the new members are assigned role by the member who adds them - only `owner` and `admin` members can add new members; only `owner` members can add members with `owner` role.
@@ -234,10 +207,6 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T
`x.grp.acpt` message is sent as part of group member connection handshake, only to the inviting user.
`x.grp.link.inv` message is sent as part of connection handshake to member joining via group link, and contains group profile and initial information about inviting and joining member.
`x.grp.link.mem` message is sent as part of connection handshake to member joining via group link, and contains remaining information about inviting member.
`x.grp.mem.new` message is sent by the inviting user to all connected members (and scheduled as pending to all announced but not yet connected members) to announce a new member to the existing members. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with `member` role.
`x.grp.mem.intro` messages are sent by the inviting user to the invited member, via their group member connection, one message for each existing member. When this message is sent by any other member than the one who invited the recipient it MUST be ignored.
@@ -250,10 +219,6 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T
`x.grp.mem.role` message is sent to update group member role - it is sent to all members by the member who updated the role of the member referenced in this message. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with role less than `admin`.
`x.grp.mem.restrict` message is sent to group members to communicate group member restrictions, such as member being blocked for sending messages.
`x.grp.mem.con` message is sent by members connecting inside group to inviting member, to notify the inviting member they have completed the connection and no longer require forwarding messages between them.
`x.grp.mem.del` message is sent to delete a member - it is sent to all members by the member who deletes the member referenced in this message. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with `member` role.
`x.grp.leave` message is sent to all members by the member leaving the group. If the only group `owner` leaves the group, it will not be possible to delete it with `x.grp.del` message - but all members can still leave the group with `x.grp.leave` message and then delete a local copy of the group.
@@ -262,10 +227,6 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T
`x.grp.info` message is sent to all members by the member who updated group profile. Only group owners can update group profiles. Clients MAY implement some conflict resolution strategy - it is currently not implemented by SimpleX Chat client. This message MUST only be sent by members with `owner` role. Receiving clients MUST ignore this message if it is received from member other than with `owner` role.
`x.grp.direct.inv` message is sent to a group member to propose establishing a direct connection between members, thus creating a contact with another member.
`x.grp.msg.forward` message is sent by inviting member to forward messages between introduced members, while they are connecting.
## Sub-protocol for WebRTC audio/video calls
This sub-protocol is used to send call invitations and to negotiate end-to-end encryption keys and pass WebRTC signalling information.
@@ -1,158 +0,0 @@
# 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,43 +0,0 @@
# Optimized subscription
## Problem
The `subscribeUserConnections` function has a few problems that affect UX on app start:
1. It loads entity data that isn't used until result processing. This produces a memory spike and takes CPU time to parse all the data.
2. Subscription results are processed synchronously after the agent finishes all the batches for user. The app wouldn't see connections as active until the slowest server responds or timeouts.
3. User subscriptions are processed sequentially. A currently active user is given a first round of subs, but the remaining are blocked. If a user profile is switched right away, the new user may start receiving updates with even more lag.
## Solution
Functions that fetch connections and entities are reduced to return only connection IDs. The filters should be moved from Haskell into specialized SQL queries that only return `[ConnId]`.
With the connection list on hands the agent subscriber thread forks off to do its thing and process batch results. This allows outer loop to start collecting connections for the remaining users.
Successful results are communicated with a new `UP srv conns` message emitted from agent when a batch finishes its processing. The `conns` payload would be a list of connections that actually have just switched subs from "pending" to "active". The UP handling machinery processes status updates just as it would in a server reconnect event. This would update connection state in chat apps as soon as the server responds, keeping app and core in tighter sync.
`reconnectSMPClient` should stop sending UPs to prevent double processing of the same result. The `okConns` membership test it currently uses is the same "did not belong to an active connection" that the batch result would use.
Sending results with UP allows to reduce summary responses to a bunch of counters so no entity data would be needed for CLI here:
```haskell
| CRContactSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRUserGroupLinksSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRMemberSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRPendingSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
```
Subscription errors are reported to API as `CRNetworkStatuses` as ususal, but the active subs are removed from the list as they are already handled by `UP`.
Subscription errors for CLI (when connection error reporting is enabled) are reported with the types reduced to a textual name:
```haskell
| CRContactSubError {user :: User, contactName :: ContactName, chatError :: ChatError}
| CRMemberSubError {user :: User, groupName :: GroupName, contactName :: ContactName, chatError :: ChatError}
| CRSndFileSubError {user :: User, sndFileTransfer :: Text, chatError :: ChatError}
| CRRcvFileSubError {user :: User, rcvFileTransfer :: Text, chatError :: ChatError}
```
> A generic constructor could be used instead, but then it would contain extra fields to form a message in View if matching the current output is desired.
The textual names for connections can be requested for the subset of all connections that needs them with the same procedure that's used in `UP` handling.
+1 -1
View File
@@ -1,5 +1,5 @@
name: simplex-chat
version: 5.8.1.0
version: 5.8.0.5
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
+1 -1
View File
@@ -47,7 +47,7 @@ for ORIG_NAME in "${ORIG_NAMES[@]}"; do
#(cd apk && 7z a -r -mx=0 -tzip ../$ORIG_NAME resources.arsc)
ALL_TOOLS=("$sdk_dir"/build-tools/*/)
BIN_DIR="${ALL_TOOLS[${#ALL_TOOLS[@]}-1]}"
BIN_DIR="${ALL_TOOLS[1]}"
"$BIN_DIR"/zipalign -p -f 4 "$ORIG_NAME" "$ORIG_NAME"-2
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."8a3b72458f917e9867f4e3640dda0fa1827ff6cf" = "1mmxdaj563kjmlkacxdnq62n6mzw9khampzaqghnk6iiwzdig0qy";
"https://github.com/simplex-chat/simplexmq.git"."2e4f507919dd3a08e5c4106fde193fee4414de3c" = "1s035v77rpk8xs8gcfqm2ddp1mf5n35zrkq68gxqr634r2r9vbaj";
"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/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+1 -2
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 5.8.1.0
version: 5.8.0.5
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
@@ -146,7 +146,6 @@ library
Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
Simplex.Chat.Migrations.M20240528_quota_err_counter
Simplex.Chat.Migrations.M20240530_user_contact_links_user_id
Simplex.Chat.Mobile
Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared
+203 -224
View File
@@ -93,9 +93,8 @@ import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescr
import qualified Simplex.FileTransfer.Description as FD
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
import qualified Simplex.FileTransfer.Transport as XFTP
import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId)
import Simplex.Messaging.Agent as Agent
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getNetworkConfig', ipAddressProtected, withLockMap)
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getNetworkConfig', ipAddressProtected, temporaryAgentError, withLockMap)
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
import Simplex.Messaging.Agent.Lock (withLock)
import Simplex.Messaging.Agent.Protocol
@@ -104,7 +103,7 @@ import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), Migrati
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.Migrations as Migrations
import Simplex.Messaging.Client (NetworkConfig (..), ProxyClientError (..), defaultNetworkConfig)
import Simplex.Messaging.Client (ProxyClientError (..), NetworkConfig (..), defaultNetworkConfig)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import qualified Simplex.Messaging.Crypto.File as CF
@@ -218,12 +217,11 @@ newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Boo
newChatController
ChatDatabase {chatStore, agentStore}
user
cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote, confirmMigrations}
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote}
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
backgroundMode = do
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
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'}
config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable}
firstTime = dbNew chatStore
currentUser <- newTVarIO user
currentRemoteHost <- newTVarIO Nothing
@@ -763,31 +761,28 @@ processChatCommand' vr = \case
_ -> throwChatError CEInvalidChatItemUpdate
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
CTGroup -> withGroupLock "updateChatItem" chatId $ do
Group gInfo@GroupInfo {groupId, membership} ms <- withStore $ \db -> getGroup db vr user chatId
Group gInfo@GroupInfo {groupId} ms <- withStore $ \db -> getGroup db vr user chatId
assertUserGroupRole gInfo GRAuthor
if prohibitedSimplexLinks gInfo membership mc
then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText GFSimplexLinks))
else do
cci <- withStore $ \db -> getGroupCIWithReactions db user gInfo itemId
case cci of
CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do
case (ciContent, itemSharedMsgId, editable) of
(CISndMsgContent oldMC, Just itemSharedMId, True) -> do
let changed = mc /= oldMC
if changed || fromMaybe False itemLive
then do
(SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive))
ci' <- withStore' $ \db -> do
currentTs <- liftIO getCurrentTime
when changed $
addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc)
let edited = itemLive /= Just True
updateGroupChatItem db user groupId ci (CISndMsgContent mc) edited live $ Just msgId
startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci'
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
cci <- withStore $ \db -> getGroupCIWithReactions db user gInfo itemId
case cci of
CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do
case (ciContent, itemSharedMsgId, editable) of
(CISndMsgContent oldMC, Just itemSharedMId, True) -> do
let changed = mc /= oldMC
if changed || fromMaybe False itemLive
then do
(SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive))
ci' <- withStore' $ \db -> do
currentTs <- liftIO getCurrentTime
when changed $
addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc)
let edited = itemLive /= Just True
updateGroupChatItem db user groupId ci (CISndMsgContent mc) edited live $ Just msgId
startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci'
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
(nf@NoteFolder {noteFolderId}, cci) <- withStore $ \db -> (,) <$> getNoteFolder db user chatId <*> getLocalChatItem db user chatId itemId
case cci of
@@ -1361,9 +1356,6 @@ processChatCommand' vr = \case
pure $ CRNetworkConfig cfg
APISetNetworkInfo info -> lift (withAgent' (`setUserNetworkInfo` info)) >> 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
CTDirect -> do
ct <- withStore $ \db -> do
@@ -2925,17 +2917,9 @@ prohibitedGroupContent :: GroupInfo -> GroupMember -> MsgContent -> Maybe f -> M
prohibitedGroupContent gInfo m mc file_
| isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice
| not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles
| prohibitedSimplexLinks gInfo m mc = Just GFSimplexLinks
| not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo) && containsFormat isSimplexLink (parseMarkdown $ msgContentText mc) = Just GFSimplexLinks
| 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 n
| n <= 0 = 4
@@ -3229,7 +3213,7 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete}
forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci
throwChatError $ CEFileNotApproved fileId unknownSrvs
getNetworkConfig :: CM' NetworkConfig
getNetworkConfig :: CM' NetworkConfig
getNetworkConfig = withAgent' $ liftIO . getNetworkConfig'
resetRcvCIFileStatus :: User -> FileTransferId -> CIFileStatus 'MDRcv -> CM (Maybe AChatItem)
@@ -3385,8 +3369,8 @@ agentSubscriber = do
toView' $ CRChatError Nothing $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) Nothing
E.throwIO e
where
process :: (ACorrId, EntityId, AEvt) -> CM' ()
process (corrId, entId, AEvt e msg) = run $ case e of
process :: (ACorrId, EntityId, APartyCmd 'Agent) -> CM' ()
process (corrId, entId, APC e msg) = run $ case e of
SAENone -> processAgentMessageNoConn msg
SAEConn -> processAgentMessage corrId entId msg
SAERcvFile -> processAgentMsgRcvFile corrId entId msg
@@ -3400,55 +3384,71 @@ subscribeUserConnections :: VersionRangeChat -> Bool -> AgentBatchSubscribe -> U
subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
-- get user connections
ce <- asks $ subscriptionEvents . config
(conns, ctConns, ucs, gs, mConns, sfts, rfts, pcConns) <-
(conns, cts, ucs, gs, ms, sfts, rfts, pcs) <-
if onlyNeeded
then do
(conns, entities) <- withStore' $ \db -> getConnectionsToSubscribe db vr user
let (cts, ucs, ms, sfts, rfts, pcs) = foldl' addEntity ([], [], [], M.empty, M.empty, []) entities
(conns, entities) <- withStore' (`getConnectionsToSubscribe` vr)
let (cts, ucs, ms, sfts, rfts, pcs) = foldl' addEntity (M.empty, M.empty, M.empty, M.empty, M.empty, M.empty) entities
pure (conns, cts, ucs, [], ms, sfts, rfts, pcs)
else do
withStore' (`unsetConnectionToSubscribe` user)
ctConns <- getContactConns
withStore' unsetConnectionToSubscribe
(ctConns, cts) <- getContactConns
(ucConns, ucs) <- getUserContactLinkConns
(gs, mConns) <- getGroupMemberConns
(gs, mConns, ms) <- getGroupMemberConns
(sftConns, sfts) <- getSndFileTransferConns
(rftConns, rfts) <- getRcvFileTransferConns
pcConns <- getPendingContactConns
(pcConns, pcs) <- getPendingContactConns
let conns = concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns]
pure (conns, ctConns, ucs, gs, mConns, sfts, rfts, pcConns)
-- detach subscription and result processing
void . lift . forkIO . runSubscriber $ do
-- subscribe using batched commands
rs <- withAgent $ \a -> agentBatchSubscribe a conns
let (errs, _oks) = M.mapEither id rs
refs <- if ce then withStore' $ \db -> getConnectionsContacts db (M.keys errs) else pure []
let connRefs = M.fromList $ map (\ContactRef {agentConnId = AgentConnId acId, localDisplayName} -> (acId, localDisplayName)) refs
contactSubsToView errs ctConns connRefs ce
contactLinkSubsToView errs ucs
groupSubsToView errs gs mConns connRefs ce
sndFileSubsToView errs sfts
rcvFileSubsToView errs rfts
pendingConnSubsToView errs pcConns
pure (conns, cts, ucs, gs, ms, sfts, rfts, pcs)
-- subscribe using batched commands
rs <- withAgent $ \a -> agentBatchSubscribe a conns
-- send connection events to view
contactSubsToView rs cts ce
-- TODO possibly, we could either disable these events or replace with less noisy for API
contactLinkSubsToView rs ucs
groupSubsToView rs gs ms ce
sndFileSubsToView rs sfts
rcvFileSubsToView rs rfts
pendingConnSubsToView rs pcs
where
runSubscriber :: CM () -> CM' ()
runSubscriber action = tryAllErrors' mkChatError action >>= either (logError . tshow) pure
addEntity (cts, ucs, ms, sfts, rfts, pcs) = \case
RcvDirectMsgConnection c (Just _ct) -> let cts' = aConnId c : cts in (cts', ucs, ms, sfts, rfts, pcs)
RcvDirectMsgConnection c Nothing -> let pcs' = aConnId c : pcs in (cts, ucs, ms, sfts, rfts, pcs')
RcvGroupMsgConnection c _g _m -> let ms' = aConnId c : ms in (cts, ucs, ms', sfts, rfts, pcs)
SndFileConnection c sft -> let sfts' = M.insert (aConnId c) sft sfts in (cts, ucs, ms, sfts', rfts, pcs)
RcvFileConnection c rft -> let rfts' = M.insert (aConnId c) rft rfts in (cts, ucs, ms, sfts, rfts', pcs)
UserContactConnection c uc -> let ucs' = (aConnId c, isNothing $ userContactGroupId uc) : ucs in (cts, ucs', ms, sfts, rfts, pcs)
getContactConns :: CM [ConnId]
getContactConns = withStore_ getUserContactConnIds
getUserContactLinkConns :: CM ([ConnId], [(ConnId, Bool)])
RcvDirectMsgConnection c (Just ct) -> let cts' = addConn c ct cts in (cts', ucs, ms, sfts, rfts, pcs)
RcvDirectMsgConnection c Nothing -> let pcs' = addConn c (toPCC c) pcs in (cts, ucs, ms, sfts, rfts, pcs')
RcvGroupMsgConnection c _g m -> let ms' = addConn c m ms in (cts, ucs, ms', sfts, rfts, pcs)
SndFileConnection c sft -> let sfts' = addConn c sft sfts in (cts, ucs, ms, sfts', rfts, pcs)
RcvFileConnection c rft -> let rfts' = addConn c rft rfts in (cts, ucs, ms, sfts, rfts', pcs)
UserContactConnection c uc -> let ucs' = addConn c uc ucs in (cts, ucs', ms, sfts, rfts, pcs)
addConn :: Connection -> a -> Map ConnId a -> Map ConnId a
addConn = M.insert . aConnId
toPCC Connection {connId, agentConnId, connStatus, viaUserContactLink, groupLinkId, customUserProfileId, localAlias, createdAt} =
PendingContactConnection
{ pccConnId = connId,
pccAgentConnId = agentConnId,
pccConnStatus = connStatus,
viaContactUri = False,
viaUserContactLink,
groupLinkId,
customUserProfileId,
connReqInv = Nothing,
localAlias,
createdAt,
updatedAt = createdAt
}
getContactConns :: CM ([ConnId], Map ConnId Contact)
getContactConns = do
cts <- withStore_ (`getUserContacts` vr)
let cts' = mapMaybe (\ct -> (,ct) <$> contactConnId ct) $ filter contactActive cts
pure (map fst cts', M.fromList cts')
getUserContactLinkConns :: CM ([ConnId], Map ConnId UserContact)
getUserContactLinkConns = do
ucs <- withStore_ getUserContactLinks
pure (map fst ucs, ucs)
getGroupMemberConns :: CM ([(GroupInfo, [ConnId])], [ConnId])
(cs, ucs) <- unzip <$> withStore_ (`getUserContactLinks` vr)
let connIds = map aConnId cs
pure (connIds, M.fromList $ zip connIds ucs)
getGroupMemberConns :: CM ([Group], [ConnId], Map ConnId GroupMember)
getGroupMemberConns = do
gs <- withStore_ (`getUserGroupMemberConnIds` vr)
pure (gs, concatMap snd gs)
gs <- withStore_ (`getUserGroups` vr)
let mPairs = concatMap (\(Group _ ms) -> mapMaybe (\m -> (,m) <$> memberConnId m) (filter (not . memberRemoved) ms)) gs
pure (gs, map fst mPairs, M.fromList mPairs)
getSndFileTransferConns :: CM ([ConnId], Map ConnId SndFileTransfer)
getSndFileTransferConns = do
sfts <- withStore_ getLiveSndFileTransfers
@@ -3459,81 +3459,92 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
rfts <- withStore_ getLiveRcvFileTransfers
let rftPairs = mapMaybe (\ft -> (,ft) <$> liveRcvFileTransferConnId ft) rfts
pure (map fst rftPairs, M.fromList rftPairs)
getPendingContactConns :: CM [ConnId]
getPendingContactConns = withStore_ getPendingContactConnections
contactSubsToView :: Map ConnId AgentErrorType -> [ConnId] -> Map ConnId ContactName -> Bool -> CM ()
contactSubsToView errs cts names ce = ifM (asks $ coreApi . config) notifyAPI notifyCLI
getPendingContactConns :: CM ([ConnId], Map ConnId PendingContactConnection)
getPendingContactConns = do
pcs <- withStore_ getPendingContactConnections
let connIds = map aConnId' pcs
pure (connIds, M.fromList $ zip connIds pcs)
contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> CM ()
contactSubsToView rs cts ce = do
chatModifyVar connNetworkStatuses $ M.union (M.fromList statuses)
ifM (asks $ coreApi . config) (notifyAPI statuses) notifyCLI
where
conns = S.fromList cts
errConns = M.restrictKeys errs conns
notifyCLI = do
toView CRContactSubSummary {user, okSubs = S.size conns - M.size errConns, errSubs = M.size errConns}
when ce $ forM_ (M.assocs errConns) $ \(acId, err) ->
forM_ (M.lookup acId names) $ \contactName ->
toView CRContactSubError {user, contactName, chatError = ChatErrorAgent err Nothing}
notifyAPI = unless (M.null errConns) $ toView $ CRNetworkStatuses (Just user) $ map status (M.assocs errConns)
let cRs = resultsFor rs cts
cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs
toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs
when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors
notifyAPI = toView . CRNetworkStatuses (Just user) . map (uncurry ConnNetworkStatus)
statuses = M.foldrWithKey' addStatus [] cts
where
status (connId, err) = ConnNetworkStatus (AgentConnId connId) $ NSError (errorNetworkStatus err)
errorNetworkStatus :: AgentErrorType -> String
addStatus :: ConnId -> Contact -> [(AgentConnId, NetworkStatus)] -> [(AgentConnId, NetworkStatus)]
addStatus _ Contact {activeConn = Nothing} nss = nss
addStatus connId Contact {activeConn = Just Connection {agentConnId}} nss =
let ns = (agentConnId, netStatus $ resultErr connId rs)
in ns : nss
netStatus :: Maybe ChatError -> NetworkStatus
netStatus = maybe NSConnected $ NSError . errorNetworkStatus
errorNetworkStatus :: ChatError -> String
errorNetworkStatus = \case
BROKER _ NETWORK -> "network"
SMP _ SMP.AUTH -> "contact deleted"
ChatErrorAgent (BROKER _ NETWORK) _ -> "network"
ChatErrorAgent (SMP _ SMP.AUTH) _ -> "contact deleted"
e -> show e
contactLinkSubsToView :: Map ConnId AgentErrorType -> [(ConnId, Bool)] -> CM ()
contactLinkSubsToView errs ucs = do
let (addresses, groupLinks) = partition snd ucs
forM_ addresses $ \(acId, _uc) -> toView $ CRUserAddrSubStatus {user, userContactError = (`ChatErrorAgent` Nothing) <$> M.lookup acId errs}
let groups = S.fromList $ map fst groupLinks
errGroups = M.restrictKeys errs groups
unless (S.null groups) $ toView CRUserGroupLinksSubSummary
{ user,
okSubs = S.size groups - M.size errGroups,
errSubs = M.size errGroups
}
groupSubsToView :: Map ConnId AgentErrorType -> [(GroupInfo, [ConnId])] -> [ConnId] -> Map ConnId ContactName -> Bool -> CM ()
groupSubsToView errs gs allMembers names ce = do
mapM_ (uncurry groupSub) gs
toView CRMemberSubSummary {user, okSubs = S.size conns - M.size errConns, errSubs = M.size errConns}
-- TODO possibly below could be replaced with less noisy events for API
contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> CM ()
contactLinkSubsToView rs = toView . CRUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs
groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [Group] -> Map ConnId GroupMember -> Bool -> CM ()
groupSubsToView rs gs ms ce = do
mapM_ groupSub $
sortOn (\(Group GroupInfo {localDisplayName = g} _) -> g) gs
toView . CRMemberSubSummary user $ map (uncurry MemberSubStatus) mRs
where
conns = S.fromList allMembers
errConns = M.restrictKeys errs conns
groupSub :: GroupInfo -> [ConnId] -> CM ()
groupSub g@GroupInfo {membership} groupMembers = do
when ce $ mapM_ (toView . uncurry (CRMemberSubError user g) ) mErrors
mRs = resultsFor rs ms
groupSub :: Group -> CM ()
groupSub (Group g@GroupInfo {membership, groupId = gId} members) = do
when ce $ mapM_ (toView . uncurry (CRMemberSubError user g)) mErrors
toView groupEvent
where
mErrors :: [(ContactName, ChatError)]
mErrors = sortOn fst $ mapMaybe mError groupMembers
mError :: ConnId -> Maybe (ContactName, ChatError)
mError mConnId = do
mErr <- M.lookup mConnId errConns
name <- M.lookup mConnId names
Just (name, ChatErrorAgent mErr Nothing)
mErrors :: [(GroupMember, ChatError)]
mErrors =
sortOn (\(GroupMember {localDisplayName = n}, _) -> n)
. filterErrors
$ filter (\(GroupMember {groupId}, _) -> groupId == gId) mRs
groupEvent :: ChatResponse
groupEvent
| memberStatus membership == GSMemInvited = CRGroupInvitation user g
| null groupMembers =
| all (\GroupMember {activeConn} -> isNothing activeConn) members =
if memberActive membership
then CRGroupEmpty user g
else CRGroupRemoved user g
| otherwise = CRGroupSubscribed user g
sndFileSubsToView :: Map ConnId AgentErrorType -> Map ConnId SndFileTransfer -> CM ()
sndFileSubsToView errs sfts =
forM_ (M.assocs sfts) $ \(acId, ft@SndFileTransfer {fileId, fileStatus}) -> do
forM_ (M.lookup acId errs) $ toView . CRSndFileSubError user ft . (`ChatErrorAgent` Nothing)
sndFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId SndFileTransfer -> CM ()
sndFileSubsToView rs sfts = do
let sftRs = resultsFor rs sfts
forM_ sftRs $ \(ft@SndFileTransfer {fileId, fileStatus}, err_) -> do
forM_ err_ $ toView . CRSndFileSubError user ft
void . forkIO $ do
threadDelay 1000000
when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withChatLock "subscribe sendFileChunk" $
sendFileChunk user ft
rcvFileSubsToView :: Map ConnId AgentErrorType -> Map ConnId RcvFileTransfer -> CM ()
rcvFileSubsToView errs = mapM_ (toView . uncurry (CRRcvFileSubError user)) . M.mapMaybeWithKey (\acId rft -> (\e -> (rft, ChatErrorAgent e Nothing)) <$> M.lookup acId errs)
pendingConnSubsToView :: Map ConnId AgentErrorType -> [ConnId] -> CM () -- XXX: ignored by View
pendingConnSubsToView errs pcs = toView CRPendingSubSummary {user, okSubs = S.size conns - M.size errConns, errSubs = M.size errConns}
where
conns = S.fromList pcs
errConns = M.restrictKeys errs conns
rcvFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId RcvFileTransfer -> CM ()
rcvFileSubsToView rs = mapM_ (toView . uncurry (CRRcvFileSubError user)) . filterErrors . resultsFor rs
pendingConnSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId PendingContactConnection -> CM ()
pendingConnSubsToView rs = toView . CRPendingSubSummary user . map (uncurry PendingSubStatus) . resultsFor rs
withStore_ :: (DB.Connection -> User -> IO [a]) -> CM [a]
withStore_ a = withStore' (`a` user) `catchChatError` \e -> toView (CRChatError (Just user) e) $> []
filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)]
filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_)
resultsFor :: Map ConnId (Either AgentErrorType ()) -> Map ConnId a -> [(a, Maybe ChatError)]
resultsFor rs = M.foldrWithKey' addResult []
where
addResult :: ConnId -> a -> [(a, Maybe ChatError)] -> [(a, Maybe ChatError)]
addResult connId = (:) . (,resultErr connId rs)
resultErr :: ConnId -> Map ConnId (Either AgentErrorType ()) -> Maybe ChatError
resultErr connId rs = case M.lookup connId rs of
Just (Left e) -> Just $ ChatErrorAgent e Nothing
Just _ -> Nothing
_ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId
cleanupManager :: CM ()
cleanupManager = do
interval <- asks (cleanupManagerInterval . config)
@@ -3673,7 +3684,7 @@ expireChatItems user@User {userId} ttl sync = do
membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db vr user gInfo
forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m
processAgentMessage :: ACorrId -> ConnId -> AEvent 'AEConn -> CM ()
processAgentMessage :: ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> CM ()
processAgentMessage _ connId (DEL_RCVQ srv qId err_) =
toView $ CRAgentRcvQueueDeleted (AgentConnId connId) srv (AgentQueueId qId) err_
processAgentMessage _ connId DEL_CONN =
@@ -3702,7 +3713,7 @@ critical a =
ChatErrorStore SEDBBusyError {message} -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing
e -> throwError e
processAgentMessageNoConn :: AEvent 'AENone -> CM ()
processAgentMessageNoConn :: ACommand 'Agent 'AENone -> CM ()
processAgentMessageNoConn = \case
CONNECT p h -> hostEvent $ CRHostConnected p h
DISCONNECT p h -> hostEvent $ CRHostDisconnected p h
@@ -3719,11 +3730,11 @@ processAgentMessageNoConn = \case
where
connIds = map AgentConnId conns
notifyAPI = toView . CRNetworkStatus nsStatus
notifyCLI = whenM (asks $ subscriptionEvents . config) $ do
notifyCLI = do
cs <- withStore' (`getConnectionsContacts` conns)
toView $ event srv cs
processAgentMsgSndFile :: ACorrId -> SndFileId -> AEvent 'AESndFile -> CM ()
processAgentMsgSndFile :: ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> CM ()
processAgentMsgSndFile _corrId aFileId msg = do
(cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId)
withEntityLock_ cRef_ . withFileLock "processAgentMsgSndFile" fileId $
@@ -3757,11 +3768,11 @@ processAgentMsgSndFile _corrId aFileId msg = do
lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId)
withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText rfds)
case rfds of
[] -> sendFileError (FileErrOther "no receiver descriptions") "no receiver descriptions" vr ft
[] -> sendFileError "no receiver descriptions" vr ft
rfd : _ -> case [fd | fd@(FD.ValidFileDescription FD.FileDescription {chunks = [_]}) <- rfds] of
[] -> case xftpRedirectFor of
Nothing -> xftpSndFileRedirect user fileId rfd >>= toView . CRSndFileRedirectStartXFTP user ft
Just _ -> sendFileError (FileErrOther "chaining redirects") "Prohibit chaining redirects" vr ft
Just _ -> sendFileError "Prohibit chaining redirects" vr ft
rfds' -> do
-- 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
@@ -3806,15 +3817,11 @@ processAgentMsgSndFile _corrId aFileId msg = do
pure (sndMsg, msgDeliveryId)
_ -> pure ()
_ -> pure () -- TODO error?
SFWARN e -> do
let err = tshow e
logWarn $ "Sent file warning: " <> err
ci <- withStore $ \db -> do
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
SFERR e
| temporaryAgentError e ->
throwChatError $ CEXFTPSndFile fileId (AgentSndFileId aFileId) e
| otherwise ->
sendFileError (tshow e) vr ft
where
fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text
fileDescrText = safeDecodeUtf8 . strEncode
@@ -3832,27 +3839,15 @@ processAgentMsgSndFile _corrId aFileId msg = do
case L.nonEmpty fds of
Just fds' -> loopSend fds'
Nothing -> pure msgDeliveryId
sendFileError :: FileError -> Text -> VersionRangeChat -> FileTransferMeta -> CM ()
sendFileError ferr err vr ft = do
sendFileError :: Text -> VersionRangeChat -> FileTransferMeta -> CM ()
sendFileError err vr ft = do
logError $ "Sent file error: " <> err
ci <- withStore $ \db -> do
liftIO $ updateFileCancelled db user fileId (CIFSSndError ferr)
liftIO $ updateFileCancelled db user fileId CIFSSndError
lookupChatItemByFileId db vr user fileId
lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId)
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 rfdText = do
partSize <- asks $ xftpDescrPartSize . config
@@ -3866,7 +3861,7 @@ splitFileDescr rfdText = do
then fileDescr :| []
else fileDescr <| splitParts (partNo + 1) partSize rest
processAgentMsgRcvFile :: ACorrId -> RcvFileId -> AEvent 'AERcvFile -> CM ()
processAgentMsgRcvFile :: ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> CM ()
processAgentMsgRcvFile _corrId aFileId msg = do
(cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId)
withEntityLock_ cRef_ . withFileLock "processAgentMsgRcvFile" fileId $
@@ -3905,24 +3900,21 @@ processAgentMsgRcvFile _corrId aFileId msg = do
lookupChatItemByFileId db vr user fileId
agentXFTPDeleteRcvFile aFileId fileId
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
| e == FILE NOT_APPROVED -> do
| temporaryAgentError e ->
throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e
| e == XFTP "" XFTP.NOT_APPROVED -> do
aci_ <- resetRcvCIFileStatus user fileId CIFSRcvAborted
agentXFTPDeleteRcvFile aFileId fileId
forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci
| otherwise -> do
ci <- withStore $ \db -> do
liftIO $ updateFileCancelled db user fileId (CIFSRcvError $ agentFileError e)
liftIO $ updateFileCancelled db user fileId CIFSRcvError
lookupChatItemByFileId db vr user fileId
agentXFTPDeleteRcvFile aFileId fileId
toView $ CRRcvFileError user ci e ft
processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> AEvent 'AEConn -> CM ()
processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> CM ()
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,
-- as in this case no need to ACK message - we can't process messages for this connection anyway.
@@ -3954,7 +3946,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
pure $ updateEntityConnStatus acEntity connStatus
Nothing -> pure acEntity
agentMsgConnStatus :: AEvent e -> Maybe ConnStatus
agentMsgConnStatus :: ACommand 'Agent e -> Maybe ConnStatus
agentMsgConnStatus = \case
CONF {} -> Just ConnRequested
INFO {} -> Just ConnSndReady
@@ -3976,7 +3968,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
processINFOpqSupport Connection {pqSupport = pq} pq' =
when (pq /= pq') $ messageWarning "processINFOpqSupport: unexpected pqSupport change"
processDirectMessage :: AEvent e -> ConnectionEntity -> Connection -> Maybe Contact -> CM ()
processDirectMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> Maybe Contact -> CM ()
processDirectMessage agentMsg connEntity conn@Connection {connId, connChatVersion, peerChatVRange, viaUserContactLink, customUserProfileId, connectionCode} = \case
Nothing -> case agentMsg of
CONF confId pqSupport _ connInfo -> do
@@ -4201,7 +4193,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- TODO add debugging output
_ -> pure ()
processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM ()
processGroupMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM ()
processGroupMessage agentMsg connEntity conn@Connection {connId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of
INV (ACR _ cReq) ->
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} ->
@@ -4629,7 +4621,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
r n'' = Just (ci, CIRcvDecryptionError mde n'')
mdeUpdatedCI _ _ = Nothing
processSndFileConn :: AEvent e -> ConnectionEntity -> Connection -> SndFileTransfer -> CM ()
processSndFileConn :: ACommand 'Agent e -> ConnectionEntity -> Connection -> SndFileTransfer -> CM ()
processSndFileConn agentMsg connEntity conn ft@SndFileTransfer {fileId, fileName, fileStatus} =
case agentMsg of
-- SMP CONF for SndFileConnection happens for direct file protocol
@@ -4677,7 +4669,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- TODO add debugging output
_ -> pure ()
processRcvFileConn :: AEvent e -> ConnectionEntity -> Connection -> RcvFileTransfer -> CM ()
processRcvFileConn :: ACommand 'Agent e -> ConnectionEntity -> Connection -> RcvFileTransfer -> CM ()
processRcvFileConn agentMsg connEntity conn ft@RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}, grpMemberId} =
case agentMsg of
INV (ACR _ cReq) ->
@@ -4760,7 +4752,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure ()
RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo
processUserContactRequest :: AEvent e -> ConnectionEntity -> Connection -> UserContact -> CM ()
processUserContactRequest :: ACommand 'Agent e -> ConnectionEntity -> Connection -> UserContact -> CM ()
processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of
REQ invId pqSupport _ connInfo -> do
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
@@ -4833,8 +4825,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
unless (connInactive conn) $ do
quotaErrCounter' <- withStore' $ \db -> incQuotaErrCounter db user conn
when (quotaErrCounter' >= quotaErrInactiveCount) $
toView $
CRConnectionInactive connEntity True
toView $ CRConnectionInactive connEntity True
_ -> pure ()
continueSending :: ConnectionEntity -> Connection -> CM Bool
@@ -4848,13 +4839,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- 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)
withCompletedCommand :: forall e. AEntityI e => Connection -> AEvent e -> (CommandData -> CM ()) -> CM ()
withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> CM ()) -> CM ()
withCompletedCommand Connection {connId} agentMsg action = do
let agentMsgTag = AEvtTag (sAEntity @e) $ aEventTag agentMsg
let agentMsgTag = APCT (sAEntity @e) $ aCommandTag agentMsg
cmdData_ <- withStore' $ \db -> getCommandDataByCorrId db user corrId
case cmdData_ of
Just cmdData@CommandData {cmdId, cmdConnId = Just cmdConnId', cmdFunction}
| connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == AEvtTag SAEConn ERR_) -> do
| connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == APCT SAEConn ERR_) -> do
withStore' $ \db -> deleteCommand db user cmdId
action cmdData
| otherwise -> err cmdId $ "not matching connection id or unexpected response, corrId = " <> show corrId
@@ -4918,14 +4909,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
BROKER _ e -> brokerError SndErrRelay e
SMP proxySrv (SMP.PROXY (SMP.BROKER e)) -> brokerError (SndErrProxy proxySrv) e
AP.PROXY proxySrv _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> brokerError (SndErrProxyRelay proxySrv) e
e -> SndErrOther $ tshow e
e -> SndErrOther . safeDecodeUtf8 $ strEncode e
where
brokerError srvErr = \case
NETWORK -> SndErrExpired
TIMEOUT -> SndErrExpired
HOST -> srvErr SrvErrHost
SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion
e -> srvErr . SrvErrOther $ tshow e
e -> srvErr . SrvErrOther . safeDecodeUtf8 $ strEncode e
badRcvFileChunk :: RcvFileTransfer -> String -> CM ()
badRcvFileChunk ft err =
@@ -5238,21 +5229,18 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
groupMsgToView gInfo ci' {reactions}
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_
| prohibitedSimplexLinks gInfo m mc =
messageWarning $ "x.msg.update ignored: feature not allowed " <> groupFeatureNameText GFSimplexLinks
| otherwise = do
updateRcvChatItem `catchCINotFound` \_ -> do
-- This patches initial sharedMsgId into chat item when locally deleted chat item
-- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete).
-- Chat item and update message which created it will have different sharedMsgId in this case...
let timed_ = rcvGroupCITimed gInfo ttl_
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) brokerTs content Nothing timed_ live
ci' <- withStore' $ \db -> do
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')
groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc msg@RcvMessage {msgId} brokerTs ttl_ live_ =
updateRcvChatItem `catchCINotFound` \_ -> do
-- This patches initial sharedMsgId into chat item when locally deleted chat item
-- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete).
-- Chat item and update message which created it will have different sharedMsgId in this case...
let timed_ = rcvGroupCITimed gInfo ttl_
ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) brokerTs content Nothing timed_ live
ci' <- withStore' $ \db -> do
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
content = CIRcvMsgContent mc
live = fromMaybe False live_
@@ -7060,30 +7048,22 @@ agentXFTPDeleteSndFilesRemote user sndFiles = do
let redirects' = mapMaybe mapRedirectMeta $ concat redirects
sndFilesAll = redirects' <> sndFiles
sndFilesAll' = filter (not . agentSndFileDeleted . fst) sndFilesAll
-- while file is being prepared and uploaded, it would not have description available;
-- this partitions files into those with and without descriptions -
-- files with description are deleted remotely, files without description are deleted internally
(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'
sndFilesAll'' <- catMaybes <$> mapM sndFileDescr sndFilesAll'
let sfs = map (\(XFTPSndFile {agentSndFileId = AgentSndFileId aFileId}, sfd, _) -> (aFileId, sfd)) sndFilesAll''
withAgent' $ \a -> xftpDeleteSndFilesRemote a (aUserId user) sfs
void . withStoreBatch' $ \db -> map (setSndFTAgentDeleted db user . (\(_, _, fId) -> fId)) sndFilesAll''
where
mapRedirectMeta :: FileTransferMeta -> Maybe (XFTPSndFile, FileTransferId)
mapRedirectMeta FileTransferMeta {fileId = fileId, xftpSndFile = Just sndFileRedirect} = Just (sndFileRedirect, fileId)
mapRedirectMeta _ = Nothing
partitionSndDescr ::
[(XFTPSndFile, FileTransferId)] ->
[SndFileId] ->
[(SndFileId, ValidFileDescription 'FSender)] ->
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 ->
sndFileDescr :: (XFTPSndFile, FileTransferId) -> CM' (Maybe (XFTPSndFile, ValidFileDescription 'FSender, FileTransferId))
sndFileDescr (xsf@XFTPSndFile {privateSndFileDescr}, fileId) =
join <$> forM privateSndFileDescr parseSndDescr
where
parseSndDescr sfdText =
tryChatError' (parseFileDescription sfdText) >>= \case
Left _ -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr
Right sfd -> partitionSndDescr xsfs filesWithoutDescr ((aFileId, sfd) : filesWithDescr)
Left _ -> pure Nothing
Right sd -> pure $ Just (xsf, sd, fileId)
userProfileToSend :: User -> Maybe Profile -> Maybe Contact -> Bool -> Profile
userProfileToSend user@User {profile = p} incognitoProfile ct inGroup = do
@@ -7401,7 +7381,6 @@ chatCommandP =
"/_network " *> (APISetNetworkConfig <$> jsonP),
("/network " <|> "/net ") *> (SetNetworkConfig <$> netCfgP),
("/network" <|> "/net") $> APIGetNetworkConfig,
"/reconnect " *> (ReconnectServer <$> A.decimal <* A.space <*> strP),
"/reconnect" $> ReconnectAllServers,
"/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP),
"/_member settings #" *> (APISetMemberSettings <$> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP),
+10 -10
View File
@@ -355,7 +355,6 @@ data ChatCommand
| SetNetworkConfig SimpleNetCfg
| APISetNetworkInfo UserNetworkInfo
| ReconnectAllServers
| ReconnectServer UserId SMPServer
| APISetChatSettings ChatRef ChatSettings
| APISetMemberSettings GroupId GroupMemberId GroupMemberSettings
| APIContactInfo ContactId
@@ -655,7 +654,6 @@ data ChatResponse
| CRRcvFileCancelled {user :: User, chatItem_ :: Maybe AChatItem, rcvFileTransfer :: RcvFileTransfer}
| CRRcvFileSndCancelled {user :: User, chatItem :: AChatItem, 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}
| CRSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer}
| CRSndFileRcvCancelled {user :: User, chatItem_ :: Maybe AChatItem, sndFileTransfer :: SndFileTransfer}
@@ -668,7 +666,6 @@ data ChatResponse
| CRSndStandaloneFileComplete {user :: User, fileTransferMeta :: FileTransferMeta, rcvURIs :: [Text]}
| CRSndFileCancelledXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta}
| 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}
| CRUserProfileImage {user :: User, profile :: Profile}
| CRContactAliasUpdated {user :: User, toContact :: Contact}
@@ -680,10 +677,9 @@ data ChatResponse
| CRSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity}
| CRContactsDisconnected {server :: SMPServer, contactRefs :: [ContactRef]}
| CRContactsSubscribed {server :: SMPServer, contactRefs :: [ContactRef]}
| CRContactSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRContactSubError {user :: User, contactName :: ContactName, chatError :: ChatError}
| CRUserAddrSubStatus {user :: User, userContactError :: Maybe ChatError}
| CRUserGroupLinksSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRContactSubError {user :: User, contact :: Contact, chatError :: ChatError}
| CRContactSubSummary {user :: User, contactSubscriptions :: [ContactSubStatus]}
| CRUserContactSubSummary {user :: User, userContactSubscriptions :: [UserContactSubStatus]}
| CRNetworkStatus {networkStatus :: NetworkStatus, connections :: [AgentConnId]}
| CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]}
| CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
@@ -720,10 +716,10 @@ data ChatResponse
| CRNewMemberContactSentInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
| CRNewMemberContactReceivedInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
| CRContactAndMemberAssociated {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember, updatedContact :: Contact}
| CRMemberSubError {user :: User, groupInfo :: GroupInfo, contactName :: ContactName, chatError :: ChatError}
| CRMemberSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRMemberSubError {user :: User, groupInfo :: GroupInfo, member :: GroupMember, chatError :: ChatError}
| CRMemberSubSummary {user :: User, memberSubscriptions :: [MemberSubStatus]}
| CRGroupSubscribed {user :: User, groupInfo :: GroupInfo}
| CRPendingSubSummary {user :: User, okSubs :: Int, errSubs :: Int}
| CRPendingSubSummary {user :: User, pendingSubscriptions :: [PendingSubStatus]}
| CRSndFileSubError {user :: User, sndFileTransfer :: SndFileTransfer, chatError :: ChatError}
| CRRcvFileSubError {user :: User, rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError}
| CRCallInvitation {callInvitation :: RcvCallInvitation}
@@ -732,6 +728,8 @@ data ChatResponse
| CRCallExtraInfo {user :: User, contact :: Contact, extraInfo :: WebRTCExtraInfo}
| CRCallEnded {user :: User, contact :: Contact}
| CRCallInvitations {callInvitations :: [RcvCallInvitation]}
| CRUserContactLinkSubscribed -- TODO delete
| CRUserContactLinkSubError {chatError :: ChatError} -- TODO delete
| CRNtfTokenStatus {status :: NtfTknStatus}
| CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode, ntfServer :: NtfServer}
| CRNtfMessages {user_ :: Maybe User, connEntity_ :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]}
@@ -1159,6 +1157,8 @@ data ChatErrorType
| CEFileImageSize {filePath :: FilePath}
| CEFileNotReceived {fileId :: FileTransferId}
| CEFileNotApproved {fileId :: FileTransferId, unknownServers :: [XFTPServer]}
| CEXFTPRcvFile {fileId :: FileTransferId, agentRcvFileId :: AgentRcvFileId, agentError :: AgentErrorType}
| CEXFTPSndFile {fileId :: FileTransferId, agentSndFileId :: AgentSndFileId, agentError :: AgentErrorType}
| CEFallbackToSMPProhibited {fileId :: FileTransferId}
| CEInlineFileProhibited {fileId :: FileTransferId}
| CEInvalidQuote
+4
View File
@@ -144,6 +144,10 @@ markdownToList (m1 :|: m2) = markdownToList m1 <> markdownToList m2
parseMarkdown :: Text -> Markdown
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 = \case
SimplexLink {} -> True;
+16 -55
View File
@@ -536,16 +536,14 @@ data CIFileStatus (d :: MsgDirection) where
CIFSSndTransfer :: {sndProgress :: Int64, sndTotal :: Int64} -> CIFileStatus 'MDSnd
CIFSSndCancelled :: CIFileStatus 'MDSnd
CIFSSndComplete :: CIFileStatus 'MDSnd
CIFSSndError :: {sndFileError :: FileError} -> CIFileStatus 'MDSnd
CIFSSndWarning :: {sndFileError :: FileError} -> CIFileStatus 'MDSnd
CIFSSndError :: CIFileStatus 'MDSnd
CIFSRcvInvitation :: CIFileStatus 'MDRcv
CIFSRcvAccepted :: CIFileStatus 'MDRcv
CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv
CIFSRcvAborted :: CIFileStatus 'MDRcv
CIFSRcvComplete :: CIFileStatus 'MDRcv
CIFSRcvCancelled :: CIFileStatus 'MDRcv
CIFSRcvError :: {rcvFileError :: FileError} -> CIFileStatus 'MDRcv
CIFSRcvWarning :: {rcvFileError :: FileError} -> CIFileStatus 'MDRcv
CIFSRcvError :: CIFileStatus 'MDRcv
CIFSInvalid :: {text :: Text} -> CIFileStatus 'MDSnd
deriving instance Eq (CIFileStatus d)
@@ -558,16 +556,14 @@ ciFileEnded = \case
CIFSSndTransfer {} -> False
CIFSSndCancelled -> True
CIFSSndComplete -> True
CIFSSndError {} -> True
CIFSSndWarning {} -> False
CIFSSndError -> True
CIFSRcvInvitation -> False
CIFSRcvAccepted -> False
CIFSRcvTransfer {} -> False
CIFSRcvAborted -> True
CIFSRcvCancelled -> True
CIFSRcvComplete -> True
CIFSRcvError {} -> True
CIFSRcvWarning {} -> False
CIFSRcvError -> True
CIFSInvalid {} -> True
ciFileLoaded :: CIFileStatus d -> Bool
@@ -576,16 +572,14 @@ ciFileLoaded = \case
CIFSSndTransfer {} -> True
CIFSSndComplete -> True
CIFSSndCancelled -> True
CIFSSndError {} -> True
CIFSSndWarning {} -> True
CIFSSndError -> True
CIFSRcvInvitation -> False
CIFSRcvAccepted -> False
CIFSRcvTransfer {} -> False
CIFSRcvAborted -> False
CIFSRcvCancelled -> False
CIFSRcvComplete -> True
CIFSRcvError {} -> False
CIFSRcvWarning {} -> False
CIFSRcvError -> False
CIFSInvalid {} -> False
data ACIFileStatus = forall d. MsgDirectionI d => AFS (SMsgDirection d) (CIFileStatus d)
@@ -598,16 +592,14 @@ instance MsgDirectionI d => StrEncoding (CIFileStatus d) where
CIFSSndTransfer sent total -> strEncode (Str "snd_transfer", sent, total)
CIFSSndCancelled -> "snd_cancelled"
CIFSSndComplete -> "snd_complete"
CIFSSndError sndFileErr -> "snd_error " <> strEncode sndFileErr
CIFSSndWarning sndFileErr -> "snd_warning " <> strEncode sndFileErr
CIFSSndError -> "snd_error"
CIFSRcvInvitation -> "rcv_invitation"
CIFSRcvAccepted -> "rcv_accepted"
CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total)
CIFSRcvAborted -> "rcv_aborted"
CIFSRcvComplete -> "rcv_complete"
CIFSRcvCancelled -> "rcv_cancelled"
CIFSRcvError rcvFileErr -> "rcv_error " <> strEncode rcvFileErr
CIFSRcvWarning rcvFileErr -> "rcv_warning " <> strEncode rcvFileErr
CIFSRcvError -> "rcv_error"
CIFSInvalid {} -> "invalid"
strP = (\(AFS _ st) -> checkDirection st) <$?> strP
@@ -623,16 +615,14 @@ instance StrEncoding ACIFileStatus where
"snd_transfer" -> AFS SMDSnd <$> progress CIFSSndTransfer
"snd_cancelled" -> pure $ AFS SMDSnd CIFSSndCancelled
"snd_complete" -> pure $ AFS SMDSnd CIFSSndComplete
"snd_error" -> AFS SMDSnd . CIFSSndError <$> ((A.space *> strP) <|> pure (FileErrOther "")) -- alternative for backwards compatibility
"snd_warning" -> AFS SMDSnd . CIFSSndWarning <$> (A.space *> strP)
"snd_error" -> pure $ AFS SMDSnd CIFSSndError
"rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation
"rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted
"rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer
"rcv_aborted" -> pure $ AFS SMDRcv CIFSRcvAborted
"rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete
"rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled
"rcv_error" -> AFS SMDRcv . CIFSRcvError <$> ((A.space *> strP) <|> pure (FileErrOther "")) -- alternative for backwards compatibility
"rcv_warning" -> AFS SMDRcv . CIFSRcvWarning <$> (A.space *> strP)
"rcv_error" -> pure $ AFS SMDRcv CIFSRcvError
_ -> fail "bad file status"
progress :: (Int64 -> Int64 -> a) -> A.Parser a
progress f = f <$> num <*> num <|> pure (f 0 1)
@@ -643,16 +633,14 @@ data JSONCIFileStatus
| JCIFSSndTransfer {sndProgress :: Int64, sndTotal :: Int64}
| JCIFSSndCancelled
| JCIFSSndComplete
| JCIFSSndError {sndFileError :: FileError}
| JCIFSSndWarning {sndFileError :: FileError}
| JCIFSSndError
| JCIFSRcvInvitation
| JCIFSRcvAccepted
| JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64}
| JCIFSRcvAborted
| JCIFSRcvComplete
| JCIFSRcvCancelled
| JCIFSRcvError {rcvFileError :: FileError}
| JCIFSRcvWarning {rcvFileError :: FileError}
| JCIFSRcvError
| JCIFSInvalid {text :: Text}
jsonCIFileStatus :: CIFileStatus d -> JSONCIFileStatus
@@ -661,16 +649,14 @@ jsonCIFileStatus = \case
CIFSSndTransfer sent total -> JCIFSSndTransfer sent total
CIFSSndCancelled -> JCIFSSndCancelled
CIFSSndComplete -> JCIFSSndComplete
CIFSSndError sndFileErr -> JCIFSSndError sndFileErr
CIFSSndWarning sndFileErr -> JCIFSSndWarning sndFileErr
CIFSSndError -> JCIFSSndError
CIFSRcvInvitation -> JCIFSRcvInvitation
CIFSRcvAccepted -> JCIFSRcvAccepted
CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total
CIFSRcvAborted -> JCIFSRcvAborted
CIFSRcvComplete -> JCIFSRcvComplete
CIFSRcvCancelled -> JCIFSRcvCancelled
CIFSRcvError rcvFileErr -> JCIFSRcvError rcvFileErr
CIFSRcvWarning rcvFileErr -> JCIFSRcvWarning rcvFileErr
CIFSRcvError -> JCIFSRcvError
CIFSInvalid text -> JCIFSInvalid text
aciFileStatusJSON :: JSONCIFileStatus -> ACIFileStatus
@@ -679,39 +665,16 @@ aciFileStatusJSON = \case
JCIFSSndTransfer sent total -> AFS SMDSnd $ CIFSSndTransfer sent total
JCIFSSndCancelled -> AFS SMDSnd CIFSSndCancelled
JCIFSSndComplete -> AFS SMDSnd CIFSSndComplete
JCIFSSndError sndFileErr -> AFS SMDSnd (CIFSSndError sndFileErr)
JCIFSSndWarning sndFileErr -> AFS SMDSnd (CIFSSndWarning sndFileErr)
JCIFSSndError -> AFS SMDSnd CIFSSndError
JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation
JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted
JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total
JCIFSRcvAborted -> AFS SMDRcv CIFSRcvAborted
JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete
JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled
JCIFSRcvError rcvFileErr -> AFS SMDRcv (CIFSRcvError rcvFileErr)
JCIFSRcvWarning rcvFileErr -> AFS SMDRcv (CIFSRcvWarning rcvFileErr)
JCIFSRcvError -> AFS SMDRcv CIFSRcvError
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
data CIFileInfo = CIFileInfo
{ fileId :: Int64,
@@ -1245,8 +1208,6 @@ instance ChatTypeI c => ToJSON (CIMeta c d) where
toJSON = $(JQ.mkToJSON defaultJSON ''CIMeta)
toEncoding = $(JQ.mkToEncoding defaultJSON ''CIMeta)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FileErr") ''FileError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCIFS") ''JSONCIFileStatus)
instance MsgDirectionI d => FromJSON (CIFileStatus d) where
@@ -1,18 +0,0 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20240530_user_contact_links_user_id where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240530_user_contact_links_user_id :: Query
m20240530_user_contact_links_user_id =
[sql|
CREATE INDEX idx_user_contact_links_user_id ON user_contact_links(user_id);
|]
down_m20240530_user_contact_links_user_id :: Query
down_m20240530_user_contact_links_user_id =
[sql|
DROP INDEX idx_user_contact_links_user_id;
|]
@@ -882,4 +882,3 @@ CREATE INDEX idx_chat_items_fwd_from_group_id ON chat_items(fwd_from_group_id);
CREATE INDEX idx_chat_items_fwd_from_chat_item_id ON chat_items(
fwd_from_chat_item_id
);
CREATE INDEX idx_user_contact_links_user_id ON user_contact_links(user_id);
+1 -2
View File
@@ -198,8 +198,7 @@ mobileChatOpts dbFilePrefix =
logAgent = Nothing,
logFile = Nothing,
tbqSize = 1024,
highlyAvailable = False,
yesToUpMigrations = False
highlyAvailable = False
},
deviceName = Nothing,
chatCmd = "",
+3 -10
View File
@@ -27,6 +27,7 @@ import Numeric.Natural (Natural)
import Options.Applicative
import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString)
import Simplex.FileTransfer.Description (mb)
import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback (..))
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll)
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth)
@@ -62,8 +63,7 @@ data CoreChatOpts = CoreChatOpts
logAgent :: Maybe LogLevel,
logFile :: Maybe FilePath,
tbqSize :: Natural,
highlyAvailable :: Bool,
yesToUpMigrations :: Bool
highlyAvailable :: Bool
}
data ChatCmdLog = CCLAll | CCLMessages | CCLNone
@@ -205,12 +205,6 @@ coreChatOptsP appDir defaultDbFileName = do
( long "ha"
<> 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
CoreChatOpts
{ dbFilePrefix,
@@ -224,8 +218,7 @@ coreChatOptsP appDir defaultDbFileName = do
logAgent = if logAgent || logLevel == CLLDebug then Just $ agentLogLevel logLevel else Nothing,
logFile,
tbqSize,
highlyAvailable,
yesToUpMigrations
highlyAvailable
}
where
useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 7 (const 15) p
+12 -9
View File
@@ -19,7 +19,7 @@ module Simplex.Chat.Store.Connections
where
import Control.Applicative ((<|>))
import Control.Monad (forM)
import Control.Monad
import Control.Monad.Except
import Data.Int (Int64)
import Data.Maybe (catMaybes, fromMaybe)
@@ -28,6 +28,7 @@ import Database.SQLite.Simple.QQ (sql)
import Simplex.Chat.Protocol
import Simplex.Chat.Store.Files
import Simplex.Chat.Store.Groups
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Store.Shared
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Protocol (ConnId)
@@ -212,17 +213,19 @@ getContactConnEntityByConnReqHash db vr user@User {userId} (cReqHash1, cReqHash2
(userId, cReqHash1, cReqHash2, ConnDeleted)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_
getConnectionsToSubscribe :: DB.Connection -> VersionRangeChat -> User -> IO ([ConnId], [ConnectionEntity])
getConnectionsToSubscribe db vr user@User {userId} = do
aConnIds <- map fromOnly <$> DB.query db "SELECT agent_conn_id FROM connections WHERE to_subscribe = 1 AND user_id = ?" (Only userId)
entities <- forM aConnIds $ \acId ->
eitherToMaybe <$> runExceptT (getConnectionEntity db vr user acId)
unsetConnectionToSubscribe db user
getConnectionsToSubscribe :: DB.Connection -> VersionRangeChat -> IO ([ConnId], [ConnectionEntity])
getConnectionsToSubscribe db vr = do
aConnIds <- map fromOnly <$> DB.query_ db "SELECT agent_conn_id FROM connections where to_subscribe = 1"
entities <- forM aConnIds $ \acId -> do
getUserByAConnId db acId >>= \case
Just user -> eitherToMaybe <$> runExceptT (getConnectionEntity db vr user acId)
Nothing -> pure Nothing
unsetConnectionToSubscribe db
let connIds = map (\(AgentConnId connId) -> connId) aConnIds
pure (connIds, catMaybes entities)
unsetConnectionToSubscribe :: DB.Connection -> User -> IO ()
unsetConnectionToSubscribe db User {userId} = DB.execute db "UPDATE connections SET to_subscribe = 0 WHERE user_id = ? AND to_subscribe = 1" (Only userId)
unsetConnectionToSubscribe :: DB.Connection -> IO ()
unsetConnectionToSubscribe db = DB.execute_ db "UPDATE connections SET to_subscribe = 0 WHERE to_subscribe = 1"
deleteConnectionRecord :: DB.Connection -> User -> Int64 -> IO ()
deleteConnectionRecord db User {userId} cId = do
+13 -31
View File
@@ -56,7 +56,6 @@ module Simplex.Chat.Store.Direct
incQuotaErrCounter,
setQuotaErrCounter,
getUserContacts,
getUserContactConnIds,
createOrUpdateContactRequest,
getContactRequest',
getContactRequest,
@@ -840,9 +839,19 @@ getUserByContactRequestId db contactRequestId =
ExceptT . firstRow toUser (SEUserNotFoundByContactRequestId contactRequestId) $
DB.query db (userQuery <> " JOIN contact_requests cr ON cr.user_id = u.user_id WHERE cr.contact_request_id = ?") (Only contactRequestId)
getPendingContactConnections :: DB.Connection -> User -> IO [ConnId]
getPendingContactConnections db User {userId} =
map fromOnly <$> DB.query db "SELECT agent_conn_id FROM connections WHERE user_id = ? AND conn_type = ? AND contact_id IS NULL" (userId, ConnContact)
getPendingContactConnections :: DB.Connection -> User -> IO [PendingContactConnection]
getPendingContactConnections db User {userId} = do
map toPendingContactConnection
<$> DB.queryNamed
db
[sql|
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at
FROM connections
WHERE user_id = :user_id
AND conn_type = :conn_type
AND contact_id IS NULL
|]
[":user_id" := userId, ":conn_type" := ConnContact]
getContactConnections :: DB.Connection -> VersionRangeChat -> UserId -> Contact -> IO [Connection]
getContactConnections db vr userId Contact {contactId} =
@@ -864,33 +873,6 @@ getContactConnections db vr userId Contact {contactId} =
connections [] = pure []
connections rows = pure $ map (toConnection vr) rows
getUserContactConnIds :: DB.Connection -> User -> IO [ConnId]
getUserContactConnIds db User {userId} =
map fromOnly
<$> DB.query
db
[sql|
SELECT c.agent_conn_id
FROM contacts ct
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ?
AND ct.contact_status = ?
AND ct.deleted = 0
AND
c.connection_id = (
SELECT cc_connection_id FROM (
SELECT
cc.connection_id AS cc_connection_id,
cc.created_at AS cc_created_at
FROM connections cc
WHERE cc.user_id = ct.user_id AND cc.contact_id = ct.contact_id
ORDER BY cc_created_at DESC
LIMIT 1
)
)
|]
(userId, CSActive)
getConnectionById :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO Connection
getConnectionById db vr User {userId} connId = ExceptT $ do
firstRow (toConnection vr) (SEConnectionNotFoundById connId) $
-29
View File
@@ -52,7 +52,6 @@ module Simplex.Chat.Store.Groups
deleteGroupItemsAndMembers,
deleteGroup,
getUserGroups,
getUserGroupMemberConnIds,
getUserGroupDetails,
getUserGroupsWithSummary,
getGroupSummary,
@@ -629,14 +628,6 @@ getUserGroups db vr user@User {userId} = do
groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId)
rights <$> mapM (runExceptT . getGroup db vr user) groupIds
getUserGroupMemberConnIds :: DB.Connection -> VersionRangeChat -> User -> IO [(GroupInfo, [ConnId])]
getUserGroupMemberConnIds db vr user@User {userId} = do
groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ? ORDER BY local_display_name" (Only userId)
fmap rights . forM groupIds $ \groupId -> runExceptT $ do
gInfo <- getGroupInfo db vr user groupId
members <- liftIO $ getGroupMemberConnIds db user gInfo
pure (gInfo, members)
getUserGroupDetails :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfo]
getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ =
map (toGroupInfo vr userContactId)
@@ -757,26 +748,6 @@ getGroupMembers db vr user@User {userId, userContactId} GroupInfo {groupId} = do
(groupMemberQuery <> " WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)")
(userId, groupId, userId, userContactId)
getGroupMemberConnIds :: DB.Connection -> User -> GroupInfo -> IO [ConnId]
getGroupMemberConnIds db User {userId, userContactId} GroupInfo {groupId} = do
map fromOnly
<$> DB.query
db
[sql|
SELECT c.agent_conn_id
FROM group_members m
JOIN connections c ON c.connection_id = (
SELECT max(cc.connection_id)
FROM connections cc
WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id
)
WHERE m.group_id = ?
AND m.user_id = ?
AND (m.contact_id IS NULL OR m.contact_id != ?)
AND m.member_status NOT IN (?, ?, ?, ?)
|]
(userId, groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown)
getGroupMembersForExpiration :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember]
getGroupMembersForExpiration db vr user@User {userId, userContactId} GroupInfo {groupId} = do
map (toContactMember vr user)
+1 -3
View File
@@ -110,7 +110,6 @@ import Simplex.Chat.Migrations.M20240501_chat_deleted
import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
import Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
import Simplex.Chat.Migrations.M20240528_quota_err_counter
import Simplex.Chat.Migrations.M20240530_user_contact_links_user_id
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -220,8 +219,7 @@ schemaMigrations =
("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted),
("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy),
("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays),
("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter),
("20240530_user_contact_links_user_id", m20240530_user_contact_links_user_id, Just down_m20240530_user_contact_links_user_id)
("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter)
]
-- | The list of migrations in ascending order by date
+19 -11
View File
@@ -350,17 +350,25 @@ getUserAddressConnections db vr User {userId} = do
|]
(userId, userId)
getUserContactLinks :: DB.Connection -> User -> IO [(ConnId, Bool)]
getUserContactLinks db User {userId} =
DB.query
db
[sql|
SELECT c.agent_conn_id, uc.group_id IS NULL
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
WHERE c.user_id = ? AND uc.user_id = ?
|]
(userId, userId)
getUserContactLinks :: DB.Connection -> VersionRangeChat -> User -> IO [(Connection, UserContact)]
getUserContactLinks db vr User {userId} =
map toUserContactConnection
<$> DB.query
db
[sql|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id,
c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version,
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
FROM connections c
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
WHERE c.user_id = ? AND uc.user_id = ?
|]
(userId, userId)
where
toUserContactConnection :: (ConnectionRow :. (Int64, ConnReqContact, Maybe GroupId)) -> (Connection, UserContact)
toUserContactConnection (connRow :. (userContactLinkId, connReqContact, groupId)) = (toConnection vr connRow, UserContact {userContactLinkId, connReqContact, groupId})
deleteUserAddress :: DB.Connection -> User -> IO ()
deleteUserAddress db user@User {userId} = do
+3 -4
View File
@@ -47,8 +47,7 @@ import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Chat.Types.Util
import Simplex.FileTransfer.Description (FileDigest)
import Simplex.FileTransfer.Types (RcvFileId, SndFileId)
import Simplex.Messaging.Agent.Protocol (ACorrId, AEventTag (..), AEvtTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId)
import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, RcvFileId, SAEntity (..), SndFileId, UserId)
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff)
import Simplex.Messaging.Encoding.String
@@ -1583,7 +1582,7 @@ instance TextEncoding CommandFunction where
CFAckMessage -> "ack_message"
CFDeleteConn -> "delete_conn"
commandExpectedResponse :: CommandFunction -> AEvtTag
commandExpectedResponse :: CommandFunction -> APartyCmdTag 'Agent
commandExpectedResponse = \case
CFCreateConnGrpMemInv -> t INV_
CFCreateConnGrpInv -> t INV_
@@ -1595,7 +1594,7 @@ commandExpectedResponse = \case
CFAckMessage -> t OK_
CFDeleteConn -> t OK_
where
t = AEvtTag SAEConn
t = APCT SAEConn
data CommandData = CommandData
{ cmdId :: CommandId,
+31 -34
View File
@@ -18,7 +18,7 @@ import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char (isSpace, toUpper)
import Data.Function (on)
import Data.Int (Int64)
import Data.List (groupBy, intercalate, intersperse, sortOn)
import Data.List (groupBy, intercalate, intersperse, partition, sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
@@ -54,9 +54,8 @@ import qualified Simplex.FileTransfer.Transport as XFTP
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..))
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Protocol (AgentErrorType (RCP))
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..))
import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback)
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import qualified Simplex.Messaging.Crypto.Ratchet as CR
@@ -66,9 +65,9 @@ import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtoServerWithAuth, ProtocolServer (..), ProtocolTypeI, SProtocolType (..))
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Util (safeDecodeUtf8, tshow)
import Simplex.Messaging.Util (bshow, safeDecodeUtf8, tshow)
import Simplex.Messaging.Version hiding (version)
import Simplex.RemoteControl.Types (RCCtrlAddress (..), RCErrorType (..))
import Simplex.RemoteControl.Types (RCCtrlAddress (..))
import System.Console.ANSI.Types
type CurrentTime = UTCTime
@@ -212,8 +211,6 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft
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]
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
CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft
CRSndStandaloneFileCreated u ft -> ttyUser u $ uploadingFileStandalone "started" ft
@@ -225,8 +222,6 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRSndFileCancelledXFTP {} -> []
CRSndFileError u Nothing ft e -> ttyUser u $ uploadingFileStandalone "error" ft <> [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} ->
ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft]
CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [viewJSON j]) info_
@@ -238,13 +233,19 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
in ttyUser u [sShow connId <> ": END"]
CRContactsDisconnected srv cs -> [plain $ "server disconnected " <> showSMPServer srv <> " (" <> contactList cs <> ")"]
CRContactsSubscribed srv cs -> [plain $ "server connected " <> showSMPServer srv <> " (" <> contactList cs <> ")"]
CRContactSubError u c e -> ttyUser u [ttyContact c <> ": contact error " <> sShow e]
CRContactSubSummary {user, okSubs, errSubs} -> ttyUser user $ [sShow okSubs <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | okSubs > 0] <> viewErrorsSummary errSubs " contact errors"
CRUserAddrSubStatus {user, userContactError} -> ttyUser user [maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError]
CRUserGroupLinksSubSummary {user, okSubs, errSubs} ->
ttyUser user $
[sShow okSubs <> " group links active" | okSubs > 0]
<> viewErrorsSummary errSubs " group link errors"
CRContactSubError u c e -> ttyUser u [ttyContact' c <> ": contact error " <> sShow e]
CRContactSubSummary u summary ->
ttyUser u $ [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)" | not (null subscribed)] <> viewErrorsSummary errors " contact errors"
where
(errors, subscribed) = partition (isJust . contactError) summary
CRUserContactSubSummary u summary ->
ttyUser u $
map addressSS addresses
<> ([sShow (length groupLinksSubscribed) <> " group links active" | not (null groupLinksSubscribed)] <> viewErrorsSummary groupLinkErrors " group link errors")
where
(addresses, groupLinks) = partition (\UserContactSubStatus {userContact} -> isNothing . userContactGroupId $ userContact) summary
addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError
(groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks
CRNetworkStatus status conns -> if testView then [plain $ show (length conns) <> " connections " <> netStatusStr status] else []
CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else []
CRGroupInvitation u g -> ttyUser u [groupInvitation' g]
@@ -278,10 +279,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRNewMemberContactSentInv u _ct g m -> ttyUser u ["sent invitation to connect directly to member " <> ttyGroup' g <> " " <> ttyMember m]
CRNewMemberContactReceivedInv u ct g m -> ttyUser u [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"]
CRContactAndMemberAssociated u ct g m ct' -> ttyUser u $ viewContactAndMemberAssociated ct g m ct'
CRMemberSubError u g m e -> ttyUser u [ttyGroup' g <> " member " <> ttyContact m <> " error: " <> sShow e]
CRMemberSubSummary {user, errSubs} -> ttyUser user $ viewErrorsSummary errSubs " group member errors"
CRMemberSubError u g m e -> ttyUser u [ttyGroup' g <> " member " <> ttyMember m <> " error: " <> sShow e]
CRMemberSubSummary u summary -> ttyUser u $ viewErrorsSummary (filter (isJust . memberError) summary) " group member errors"
CRGroupSubscribed u g -> ttyUser u $ viewGroupSubscribed g
CRPendingSubSummary {user} -> ttyUser user [] -- XXX: ???
CRPendingSubSummary u _ -> ttyUser u []
CRSndFileSubError u SndFileTransfer {fileId, fileName} e ->
ttyUser u ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e]
CRRcvFileSubError u RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e ->
@@ -292,6 +293,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRCallExtraInfo {user = u, contact} -> ttyUser u ["call extra info from " <> ttyContact' contact]
CRCallEnded {user = u, contact} -> ttyUser u ["call with " <> ttyContact' contact <> " ended"]
CRCallInvitations _ -> []
CRUserContactLinkSubscribed -> ["Your address is active! To show: " <> highlight' "/sa"]
CRUserContactLinkSubError e -> ["user address error: " <> sShow e, "to delete your address: " <> highlight' "/da"]
CRContactConnectionDeleted u PendingContactConnection {pccConnId} -> ttyUser u ["connection :" <> sShow pccConnId <> " deleted"]
CRNtfTokenStatus status -> ["device token status: " <> plain (smpEncode status)]
CRNtfToken _ status mode srv -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode) <> ", server: " <> sShow srv]
@@ -343,7 +346,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
]
CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, ctrlDeviceName} ->
["remote controller " <> sShow rcId <> " session started with " <> plain ctrlDeviceName]
CRRemoteCtrlStopped {rcStopReason} -> viewRemoteCtrlStopped rcStopReason
CRRemoteCtrlStopped {} -> ["remote controller stopped"]
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
CRSlowSQLQueries {chatQueries, agentQueries} ->
@@ -450,8 +453,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
testViewItem (CChatItem _ ci@ChatItem {meta = CIMeta {itemText}}) membership_ =
let deleted_ = maybe "" (\t -> " [" <> t <> "]") (chatItemDeletedText ci membership_)
in itemText <> deleted_
viewErrorsSummary :: Int -> StyledString -> [StyledString]
viewErrorsSummary numErrors s = [ttyError (tshow numErrors) <> s <> " (run with -c option to show each error)" | numErrors > 0]
viewErrorsSummary :: [a] -> StyledString -> [StyledString]
viewErrorsSummary summary s = [ttyError (T.pack . show $ length summary) <> s <> " (run with -c option to show each error)" | not (null summary)]
contactList :: [ContactRef] -> String
contactList cs = T.unpack . T.intercalate ", " $ map (\ContactRef {localDisplayName = n} -> "@" <> n) cs
unmuted :: User -> ChatInfo c -> ChatItem c d -> [StyledString] -> [StyledString]
@@ -1191,7 +1194,7 @@ viewServerTestResult (AProtoServerWithAuth p _) = \case
<> [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]
where
result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> sShow testError]
result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> plain (strEncode testError)]
brokerErr = case testError of
BROKER _ NETWORK -> True
_ -> False
@@ -1773,16 +1776,14 @@ viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId
CIFSSndTransfer progress total -> ["sending " <> fstr <> " in progress " <> fileProgressXFTP progress total fileSize]
CIFSSndCancelled -> ["sending " <> fstr <> " cancelled"]
CIFSSndComplete -> ["sending " <> fstr <> " complete"]
CIFSSndError sndFileErr -> ["sending " <> fstr <> " error: " <> plain (show sndFileErr)]
CIFSSndWarning sndFileErr -> ["sending " <> fstr <> " warning: " <> plain (show sndFileErr)]
CIFSSndError -> ["sending " <> fstr <> " error"]
CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"]
CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"]
CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize]
CIFSRcvAborted -> ["receiving " <> fstr <> " aborted, use " <> highlight ("/fr " <> show fileId) <> " to receive file"]
CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource]
CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"]
CIFSRcvError rcvFileErr -> ["receiving " <> fstr <> " error: " <> plain (show rcvFileErr)]
CIFSRcvWarning rcvFileErr -> ["receiving " <> fstr <> " warning: " <> plain (show rcvFileErr)]
CIFSRcvError -> ["receiving " <> fstr <> " error"]
CIFSInvalid text -> [fstr <> " invalid status: " <> plain text]
where
fstr = fileTransferStr fileId fileName
@@ -1836,7 +1837,7 @@ viewCallAnswer ct WebRTCSession {rtcSession = answer, rtcIceCandidates = iceCand
[ ttyContact' ct <> " continued the WebRTC call",
"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
@@ -1907,12 +1908,6 @@ viewRemoteCtrl CtrlAppInfo {deviceName, appVersionRange = AppVersionRange _ (App
| otherwise = ""
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 isCmd logLevel testView = \case
ChatError err -> case err of
@@ -1989,6 +1984,8 @@ viewChatError isCmd logLevel testView = \case
CEFileImageSize _ -> ["max image size: " <> sShow maxImageSize <> " bytes, resize it or send as a file using " <> highlight' "/f"]
CEFileNotReceived fileId -> ["file " <> sShow fileId <> " not received"]
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"]
CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."]
CEInvalidQuote -> ["cannot reply to this message"]
+3 -3
View File
@@ -101,8 +101,7 @@ testCoreOpts =
logAgent = Nothing,
logFile = Nothing,
tbqSize = 16,
highlyAvailable = False,
yesToUpMigrations = False
highlyAvailable = False
}
getTestOpts :: Bool -> ScrubbedBytes -> ChatOpts
@@ -132,7 +131,8 @@ aCfg = (agentConfig defaultChatConfig) {tbqSize = 16}
testAgentCfg :: AgentConfig
testAgentCfg =
aCfg
{ reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}
{ reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000},
xftpNotifyErrsOnRetry = False
}
testCfg :: ChatConfig
+12 -11
View File
@@ -808,7 +808,7 @@ testTestSMPServerConnection =
alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"
alice <## "SMP server test passed"
alice ##> "/smp test smp://LcJU@localhost:7001"
alice <## "SMP server test failed at Connect, error: BROKER {brokerAddress = \"smp://LcJU@localhost:7001\", brokerErr = NETWORK}"
alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:7001 NETWORK"
alice <## "Possibly, certificate fingerprint in SMP server address is incorrect"
testGetSetXFTPServers :: HasCallStack => FilePath -> IO ()
@@ -839,7 +839,7 @@ testTestXFTPServer =
alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"
alice <## "XFTP server test passed"
alice ##> "/xftp test xftp://LcJU@localhost:7002"
alice <## "XFTP server test failed at Connect, error: BROKER {brokerAddress = \"xftp://LcJU@localhost:7002\", brokerErr = NETWORK}"
alice <## "XFTP server test failed at Connect, error: BROKER xftp://LcJU@localhost:7002 NETWORK"
alice <## "Possibly, certificate fingerprint in XFTP server address is incorrect"
testAsyncInitiatingOffline :: HasCallStack => FilePath -> IO ()
@@ -1448,7 +1448,6 @@ testUsersSubscribeAfterRestart :: HasCallStack => FilePath -> IO ()
testUsersSubscribeAfterRestart tmp = do
withNewTestChat tmp "bob" bobProfile $ \bob -> do
withNewTestChat tmp "alice" aliceProfile $ \alice -> do
threadDelay 100000
connectUsers alice bob
alice <##> bob
@@ -1459,7 +1458,8 @@ testUsersSubscribeAfterRestart tmp = do
withTestChat tmp "alice" $ \alice -> do
-- second user is active
alice <### ["1 contacts connected (use /cs for the list)", "[user: alice] 1 contacts connected (use /cs for the list)"]
alice <## "1 contacts connected (use /cs for the list)"
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
-- second user receives message
alice <##> bob
@@ -1793,7 +1793,8 @@ testUsersRestartCIExpiration tmp = do
showActiveUser alice "alice (Alice)"
withTestChatCfg tmp cfg "alice" $ \alice -> do
alice <### ["1 contacts connected (use /cs for the list)", "[user: alisa] 1 contacts connected (use /cs for the list)"]
alice <## "1 contacts connected (use /cs for the list)"
alice <## "[user: alisa] 1 contacts connected (use /cs for the list)"
-- first user messages
alice ##> "/user alice"
@@ -1891,7 +1892,8 @@ testEnableCIExpirationOnlyForOneUser tmp = do
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
withTestChatCfg tmp cfg "alice" $ \alice -> do
alice <### ["1 contacts connected (use /cs for the list)", "[user: alice] 1 contacts connected (use /cs for the list)"]
alice <## "1 contacts connected (use /cs for the list)"
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
-- messages are not deleted for second user after restart
alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")])
@@ -1946,7 +1948,8 @@ testDisableCIExpirationOnlyForOneUser tmp = do
alice #$> ("/_get chat @4 count=100", chat, [])
withTestChatCfg tmp cfg "alice" $ \alice -> do
alice <### ["1 contacts connected (use /cs for the list)", "[user: alice] 1 contacts connected (use /cs for the list)"]
alice <## "1 contacts connected (use /cs for the list)"
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
-- second user still has ttl configured after restart
alice #$> ("/ttl", id, "old messages are set to be deleted after: 1 second(s)")
@@ -2052,10 +2055,8 @@ testUsersTimedMessages tmp = do
alice <# "bob> alisa 4"
withTestChat tmp "alice" $ \alice -> do
alice
<### [ "1 contacts connected (use /cs for the list)",
"[user: alice] 1 contacts connected (use /cs for the list)"
]
alice <## "1 contacts connected (use /cs for the list)"
alice <## "[user: alice] 1 contacts connected (use /cs for the list)"
alice ##> "/user alice"
showActiveUser alice "alice (Alice)"
+1 -1
View File
@@ -738,7 +738,7 @@ testXFTPRcvError tmp = do
_ <- getTermLine bob
bob ##> "/fs 1"
bob <## "receiving file 1 (test.pdf) error: FileErrAuth"
bob <## "receiving file 1 (test.pdf) error"
testXFTPCancelRcvRepeat :: HasCallStack => FilePath -> IO ()
testXFTPCancelRcvRepeat =
-2
View File
@@ -2590,7 +2590,6 @@ testPlanGroupLinkConnecting :: HasCallStack => FilePath -> IO ()
testPlanGroupLinkConnecting tmp = do
-- gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
@@ -3201,7 +3200,6 @@ testPlanGroupLinkNoContactKnown =
testPlanGroupLinkNoContactConnecting :: HasCallStack => FilePath -> IO ()
testPlanGroupLinkNoContactConnecting tmp = do
gLink <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do
threadDelay 100000
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
+1 -10
View File
@@ -2032,19 +2032,10 @@ testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfil
threadDelay 1000000
bob ##> "/c"
inv <- getInvitation bob
bob ##> ("#team \"" <> inv <> "\\ntest\"")
bob <## "bad chat command: feature not allowed SimpleX links"
bob ##> ("/_send #1 json {\"msgContent\": {\"type\": \"text\", \"text\": \"" <> inv <> "\\ntest\"}}")
bob ##> ("#team " <> inv)
bob <## "bad chat command: feature not allowed SimpleX links"
(alice </)
(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)
bob <# ("#team alice> " <> inv)
cath <# ("#team alice> " <> inv)
-7
View File
@@ -210,10 +210,3 @@ 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"]
it "no markdown" do
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"
]
+4 -3
View File
@@ -136,10 +136,10 @@ networkStatuses =
#endif
networkStatusesSwift :: LB.ByteString
networkStatusesSwift = "{\"resp\":{\"_owsf\":true,\"networkStatuses\":{\"networkStatuses\":[]}}}"
networkStatusesSwift = "{\"resp\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}"
networkStatusesTagged :: LB.ByteString
networkStatusesTagged = "{\"resp\":{\"type\":\"networkStatuses\",\"networkStatuses\":[]}}"
networkStatusesTagged = "{\"resp\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}"
memberSubSummary :: LB.ByteString
memberSubSummary =
@@ -222,7 +222,8 @@ testChatApi tmp = do
chatSendCmd cc "/u" `shouldReturn` activeUser
chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists
chatSendCmd cc "/_start" `shouldReturn` chatStarted
chatSendCmd cc "/_network_statuses" `shouldReturn` networkStatuses
chatRecvMsg cc `shouldReturn` networkStatuses
chatRecvMsg cc `shouldReturn` userContactSubSummary
chatRecvMsgWait cc 10000 `shouldReturn` ""
chatParseMarkdown "hello" `shouldBe` "{}"
chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown
+1 -1
View File
@@ -119,7 +119,7 @@ remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfil
inv <- getTermLine desktop
mobileBob ##> ("/connect remote ctrl " <> inv)
mobileBob <## ("connecting new remote controller: My desktop, v" <> versionNumber)
mobileBob <## "remote controller stopped: this link was used with another controller, please create a new link on the host"
mobileBob <## "remote controller stopped"
-- the server remains active after rejecting invalid client
mobile ##> ("/connect remote ctrl " <> inv)
+1 -1
View File
@@ -31,7 +31,7 @@
"simplex-explained-tab-2-p-1": "لكل اتصال، تستخدم قائمتي انتظار منفصلتين للمُراسلة لإرسال واستلام الرسائل عبر خوادم مختلفة.",
"simplex-explained-tab-2-p-2": "تقوم الخوادم بتمرير الرسائل في اتجاه واحد فقط، دون الحصول على الصورة الكاملة لمُحادثات المستخدم أو اتصالاته.",
"simplex-explained-tab-3-p-1": "تحتوي الخوادم على بيانات اعتماد مجهولة منفصلة لكل قائمة انتظار، ولا تعرف المستخدمين الذين ينتمون إليهم.",
"copyright-label": "مشروع مفتوح المصدر © SimpleX 2020-2024",
"copyright-label": "مشروع مفتوح المصدر © SimpleX 2020-2023",
"simplex-chat-protocol": "بروتوكول دردشة SimpleX",
"developers": "المطورين",
"hero-subheader": "أول نظام مُراسلة<br> دون معرّفات مُستخدم",
+1 -1
View File
@@ -21,7 +21,7 @@
"smp-protocol": "СМП Протокол",
"chat-protocol": "Чат протокол",
"donate": "Дарете",
"copyright-label": "© 2020-2024 SimpleX | Проект с отворен код",
"copyright-label": "© 2020-2023 SimpleX | Проект с отворен код",
"simplex-chat-protocol": "SimpleX Чат протокол",
"terminal-cli": "Системна конзола",
"terms-and-privacy-policy": "Условия и политика за поверителност",
+1 -1
View File
@@ -25,7 +25,7 @@
"smp-protocol": "SMP protokol",
"chat-protocol": "Chat protokol",
"donate": "Darovat",
"copyright-label": "© 2020-2024 SimpleX | Projekt s otevřeným zdrojovým kódem",
"copyright-label": "© 2020-2023 SimpleX | Projekt s otevřeným zdrojovým kódem",
"simplex-chat-protocol": "SimpleX Chat protokol",
"terminal-cli": "Terminálové rozhraní příkazového řádku",
"terms-and-privacy-policy": "Podmínky a zásady ochrany osobních údajů",
+1 -1
View File
@@ -21,7 +21,7 @@
"smp-protocol": "SMP Protokoll",
"chat-bot-example": "Beispiel für einen Chatbot",
"donate": "Spenden",
"copyright-label": "© 2020-2024 SimpleX | Open-Source Projekt",
"copyright-label": "© 2020-2023 SimpleX | Open-Source Projekt",
"chat-protocol": "Chat Protokoll",
"simplex-chat-protocol": "SimpleX Chat Protokoll",
"terminal-cli": "Terminal Kommandozeilen-Schnittstelle",
+1 -1
View File
@@ -21,7 +21,7 @@
"smp-protocol": "SMP protocol",
"chat-protocol": "Chat protocol",
"donate": "Donate",
"copyright-label": "© 2020-2024 SimpleX | Open-Source Project",
"copyright-label": "© 2020-2023 SimpleX | Open-Source Project",
"simplex-chat-protocol": "SimpleX Chat protocol",
"terminal-cli": "Terminal CLI",
"terms-and-privacy-policy": "Privacy Policy",
+1 -1
View File
@@ -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.",
"smp-protocol": "Protocolo SMP",
"donate": "Donación",
"copyright-label": "© 2020-2024 SimpleX | Proyecto de Código Abierto",
"copyright-label": "© 2020-2023 SimpleX | Proyecto de Código Abierto",
"simplex-chat-protocol": "Protocolo de SimpleX Chat",
"terms-and-privacy-policy": "Términos y Política de Privacidad",
"hero-header": "Privacidad redefinida",
+1 -1
View File
@@ -21,7 +21,7 @@
"smp-protocol": "Protocole SMP",
"chat-protocol": "Protocole de chat",
"donate": "Faire un don",
"copyright-label": "© 2020-2024 SimpleX | Projet Open-Source",
"copyright-label": "© 2020-2023 SimpleX | Projet Open-Source",
"simplex-chat-protocol": "Protocole SimpleX Chat",
"terminal-cli": "Terminal CLI",
"terms-and-privacy-policy": "Politique de confidentialité",
+2 -2
View File
@@ -20,7 +20,7 @@
"smp-protocol": "SMP protokoll",
"chat-protocol": "Csevegés protokoll",
"donate": "Támogatás",
"copyright-label": "© 2020-2024 SimpleX | Nyílt forráskódú projekt",
"copyright-label": "© 2020-2023 SimpleX | Nyílt forráskódú projekt",
"simplex-chat-protocol": "SimpleX Chat protokoll",
"terminal-cli": "Terminál CLI",
"terms-and-privacy-policy": "Adatvédelmi irányelvek",
@@ -256,4 +256,4 @@
"simplex-chat-via-f-droid": "SimpleX Chat az F-Droidon keresztül",
"simplex-chat-repo": "SimpleX Chat tároló",
"stable-and-beta-versions-built-by-developers": "A fejlesztők által készített stabil és béta verziók"
}
}
+1 -1
View File
@@ -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.",
"chat-protocol": "Protocollo di chat",
"donate": "Dona",
"copyright-label": "© 2020-2024 SimpleX | Progetto Open-Source",
"copyright-label": "© 2020-2023 SimpleX | Progetto Open-Source",
"simplex-chat-protocol": "Protocollo di SimpleX Chat",
"terminal-cli": "Terminale CLI",
"terms-and-privacy-policy": "Informativa sulla privacy",
+1 -1
View File
@@ -52,7 +52,7 @@
"chat-protocol": "チャットプロトコル",
"chat-bot-example": "チャットボットの例",
"donate": "寄付",
"copyright-label": "© 2020-2024 SimpleX | Open-Source Project",
"copyright-label": "© 2020-2023 SimpleX | Open-Source Project",
"hero-p-1": "他のアプリにはユーザー ID があります: Signal、Matrix、Session、Briar、Jami、Cwtch など。<br> SimpleX にはありません。<strong>乱数さえもありません</strong>。<br> これにより、プライバシーが大幅に向上します。",
"copy-the-command-below-text": "以下のコマンドをコピーしてチャットで使用します:",
"simplex-private-card-9-point-1": "各メッセージ キューは、異なる送信アドレスと受信アドレスを使用してメッセージを一方向に渡します。",
+1 -1
View File
@@ -17,7 +17,7 @@
"chat-bot-example": "Chatbot voorbeeld",
"smp-protocol": "SMP protocol",
"donate": "Doneer",
"copyright-label": "© 2020-2024 SimpleX | Open-sourceproject",
"copyright-label": "© 2020-2023 SimpleX | Open-sourceproject",
"simplex-chat-protocol": "SimpleX Chat protocol",
"terminal-cli": "Terminal CLI",
"terms-and-privacy-policy": "Privacybeleid",
+1 -1
View File
@@ -15,7 +15,7 @@
"smp-protocol": "Protokół SMP",
"chat-protocol": "Protokół czatu",
"donate": "Darowizna",
"copyright-label": "© 2020-2024 SimpleX | Projekt Open-Source",
"copyright-label": "© 2020-2023 SimpleX | Projekt Open-Source",
"simplex-chat-protocol": "Protokół SimpleX Chat",
"terminal-cli": "Terminal CLI",
"terms-and-privacy-policy": "Polityka prywatności",
+1 -1
View File
@@ -25,7 +25,7 @@
"smp-protocol": "Protocolo SMP",
"chat-protocol": "Protocolo de bate-papo",
"donate": "Doar",
"copyright-label": "© 2020-2024 SimpleX | Projeto de Código Livre",
"copyright-label": "© 2020-2023 SimpleX | Projeto de Código Livre",
"simplex-chat-protocol": "Protocolo Chat SimpleX",
"terminal-cli": "CLI Terminal",
"hero-header": "Privacidade redefinida",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"copy-the-command-below-text": "скопируйте приведенную ниже команду и используйте ее в чате:",
"copyright-label": "© 2020-2024 SimpleX | Проект с открытым исходным кодом",
"copyright-label": "© 2020-2023 SimpleX | Проект с открытым исходным кодом",
"chat-bot-example": "Пример Чат бота",
"simplex-private-card-9-point-1": "Каждая очередь сообщений передает сообщения в одном направлении с разными адресами отправки и получения.",
"simplex-private-card-1-point-2": "Криптобокс NaCL в каждой очереди для предотвращения корреляции трафика между очередями сообщений, в случае компрометации TLS.",
-4
View File
@@ -59,10 +59,6 @@
"term": "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",
"definition": "Merkle directed acyclic graph"
@@ -1,14 +0,0 @@
<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>
+1 -1
View File
@@ -58,7 +58,7 @@ active_blog: true
</div>
<div class="p-6 md:py-8 flex-[2.5] flex flex-col">
<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>
</h1>
<p class="text-sm text-[#A8B0B4] font-medium mt-2 mb-4 tracking-[0.03em]">
+11 -14
View File
@@ -11,31 +11,28 @@ metadata:
email: chat@simplex.chat
---
<?xml version="1.0" encoding="utf-8"?>
<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) }}"/>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="{{ metadata.url }}">
<title>{{ metadata.title }}</title>
<subtitle>{{ metadata.subtitle }}</subtitle>
<link href="{{ permalink | absoluteUrl(metadata.url) }}" rel="self"/>
<link href="{{ metadata.url }}"/>
<updated>{{ collections.blogs | getNewestCollectionItemDate | dateToRfc3339 }}</updated>
<id>{{ metadata.url }}</id>
<author>
<name>{{ metadata.author.name }}</name>
<email>{{ metadata.author.email }}</email>
</author>
{%- for blog in collections.blogs | reverse %}
{%- if not blog.data.draft %}
{%- set absolutePostUrl = blog.data.permalink | absoluteUrl(metadata.url) %}
{%- set absolutePostUrl = blog.url | absoluteUrl(metadata.url) %}
<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>
<content type="html">{{ blog.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}</content>
<author>
<name>{{ metadata.author.name }}</name>
<email>{{ metadata.author.email }}</email>
</author>
<link href="{{ absolutePostUrl }}"/>
{# <updated>{{ blog.date | dateToRfc3339 }}</updated> #}
<updated>{{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }}</updated>
<id>{{ absolutePostUrl }}</id>
<content xml:lang="{{ metadata.language }}" type="html">{{ blog.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}</content>
{# <content xml:lang="{{ metadata.language }}" type="html">{{ blog.templateContent | striptags | truncate(200) }}</content> #}
</entry>
{%- endif %}
{%- endfor %}
+2 -2
View File
@@ -26,8 +26,8 @@ metadata:
<link>{{ absolutePostUrl }}</link>
<description>{{ blog.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}</description>
{# <description>{{ blog.templateContent | striptags | truncate(200) }}</description> #}
<pubDate>{{ blog.data.date | dateToRfc822 }}</pubDate>
{# <pubDate>{{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }}</pubDate> #}
{# <pubDate>{{ blog.data.date | dateToRfc822 }}</pubDate> #}
<pubDate>{{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }}</pubDate>
<dc:creator>{{ metadata.author.name }}</dc:creator>
<guid>{{ absolutePostUrl }}</guid>
</item>
-4
View File
@@ -46,10 +46,6 @@ img{
-ms-user-select: none; /* For Internet Explorer and Edge */
}
a{
word-wrap: break-word;
}
/* #comparison::before {
display: block;
content: " ";
+2 -2
View File
@@ -2,7 +2,7 @@
layout: layouts/group_link.html
title: "SimpleX Chat - MoneroKon group"
description: "Join the group of attendees of Monero Konferenco 3 - Praha 2023"
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"
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"
groupLinkText: Open MoneroKon group link
templateEngineOverride: njk
---
---