Merge branch 'stable' into angerman/both-compilers

This commit is contained in:
Evgeny Poberezkin 2024-02-17 18:02:35 +00:00
commit 855004840e
No known key found for this signature in database
GPG Key ID: 494BDDD9A28B577D
74 changed files with 508 additions and 287 deletions

View File

@ -749,7 +749,9 @@ struct ChatView: View {
if ci.meta.editable && !mc.isVoice && !live { if ci.meta.editable && !mc.isVoice && !live {
menu.append(editAction(ci)) menu.append(editAction(ci))
} }
menu.append(viewInfoUIAction(ci)) if !ci.isLiveDummy {
menu.append(viewInfoUIAction(ci))
}
if revealed { if revealed {
menu.append(hideUIAction()) menu.append(hideUIAction())
} }

View File

@ -978,6 +978,9 @@ struct ComposeView: View {
} }
private func cancelLinkPreview() { private func cancelLinkPreview() {
if let pendingLink = pendingLinkUrl?.absoluteString {
cancelledLinks.insert(pendingLink)
}
if let uri = composeState.linkPreview?.uri.absoluteString { if let uri = composeState.linkPreview?.uri.absoluteString {
cancelledLinks.insert(uri) cancelledLinks.insert(uri)
} }

View File

@ -370,7 +370,11 @@ struct GroupChatInfoView: View {
private func addOrEditWelcomeMessage() -> some View { private func addOrEditWelcomeMessage() -> some View {
NavigationLink { NavigationLink {
GroupWelcomeView(groupId: groupInfo.groupId, groupInfo: $groupInfo) GroupWelcomeView(
groupInfo: $groupInfo,
groupProfile: groupInfo.groupProfile,
welcomeText: groupInfo.groupProfile.description ?? ""
)
.navigationTitle("Welcome message") .navigationTitle("Welcome message")
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.large)
} label: { } label: {

View File

@ -11,29 +11,32 @@ import SimpleXChat
struct GroupWelcomeView: View { struct GroupWelcomeView: View {
@Environment(\.dismiss) var dismiss: DismissAction @Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject private var m: ChatModel
var groupId: Int64
@Binding var groupInfo: GroupInfo @Binding var groupInfo: GroupInfo
@State private var welcomeText: String = "" @State var groupProfile: GroupProfile
@State var welcomeText: String
@State private var editMode = true @State private var editMode = true
@FocusState private var keyboardVisible: Bool @FocusState private var keyboardVisible: Bool
@State private var showSaveDialog = false @State private var showSaveDialog = false
let maxByteCount = 1200
var body: some View { var body: some View {
VStack { VStack {
if groupInfo.canEdit { if groupInfo.canEdit {
editorView() editorView()
.modifier(BackButton { .modifier(BackButton {
if welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) { if welcomeTextUnchanged() {
dismiss() dismiss()
} else { } else {
showSaveDialog = true showSaveDialog = true
} }
}) })
.confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) { .confirmationDialog(
Button("Save and update group profile") { welcomeTextFitsLimit() ? "Save welcome message?" : "Welcome message is too long",
save() isPresented: $showSaveDialog
dismiss() ) {
if welcomeTextFitsLimit() {
Button("Save and update group profile") { save() }
} }
Button("Exit without saving") { dismiss() } Button("Exit without saving") { dismiss() }
} }
@ -47,14 +50,15 @@ struct GroupWelcomeView: View {
} }
} }
.onAppear { .onAppear {
welcomeText = groupInfo.groupProfile.description ?? "" DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
keyboardVisible = true keyboardVisible = true
}
} }
} }
private func textPreview() -> some View { private func textPreview() -> some View {
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false) messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false)
.frame(minHeight: 140, alignment: .topLeading) .frame(minHeight: 130, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
@ -74,7 +78,7 @@ struct GroupWelcomeView: View {
} }
.padding(.horizontal, -5) .padding(.horizontal, -5)
.padding(.top, -8) .padding(.top, -8)
.frame(height: 140, alignment: .topLeading) .frame(height: 130, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
} else { } else {
@ -93,6 +97,9 @@ struct GroupWelcomeView: View {
} }
.disabled(welcomeText.isEmpty) .disabled(welcomeText.isEmpty)
copyButton() copyButton()
} footer: {
Text(!welcomeTextFitsLimit() ? "Message too large" : "")
.foregroundColor(.red)
} }
Section { Section {
@ -113,7 +120,15 @@ struct GroupWelcomeView: View {
Button("Save and update group profile") { Button("Save and update group profile") {
save() save()
} }
.disabled(welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil)) .disabled(welcomeTextUnchanged() || !welcomeTextFitsLimit())
}
private func welcomeTextUnchanged() -> Bool {
welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil)
}
private func welcomeTextFitsLimit() -> Bool {
chatJsonLength(welcomeText) <= maxByteCount
} }
private func save() { private func save() {
@ -123,11 +138,13 @@ struct GroupWelcomeView: View {
if welcome?.count == 0 { if welcome?.count == 0 {
welcome = nil welcome = nil
} }
var groupProfileUpdated = groupInfo.groupProfile groupProfile.description = welcome
groupProfileUpdated.description = welcome let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile)
groupInfo = try await apiUpdateGroup(groupId, groupProfileUpdated) await MainActor.run {
m.updateGroup(groupInfo) groupInfo = gInfo
welcomeText = welcome ?? "" ChatModel.shared.updateGroup(gInfo)
dismiss()
}
} catch let error { } catch let error {
logger.error("apiUpdateGroup error: \(responseError(error))") logger.error("apiUpdateGroup error: \(responseError(error))")
} }
@ -137,6 +154,6 @@ struct GroupWelcomeView: View {
struct GroupWelcomeView_Previews: PreviewProvider { struct GroupWelcomeView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
GroupWelcomeView(groupId: 1, groupInfo: Binding.constant(GroupInfo.sampleData)) GroupProfileView(groupInfo: Binding.constant(GroupInfo.sampleData), groupProfile: GroupProfile.sampleData)
} }
} }

View File

@ -29,6 +29,11 @@
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; }; 5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; }; 5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; };
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; }; 5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
5C29C3AF2B783F82003DF84C /* libHSsimplex-chat-5.5.3.0-AUrnxTuqxo1yzY63w39Bk-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3AA2B783F82003DF84C /* libHSsimplex-chat-5.5.3.0-AUrnxTuqxo1yzY63w39Bk-ghc9.6.3.a */; };
5C29C3B02B783F82003DF84C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3AB2B783F82003DF84C /* libgmpxx.a */; };
5C29C3B12B783F82003DF84C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3AC2B783F82003DF84C /* libffi.a */; };
5C29C3B22B783F82003DF84C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3AD2B783F82003DF84C /* libgmp.a */; };
5C29C3B32B783F82003DF84C /* libHSsimplex-chat-5.5.3.0-AUrnxTuqxo1yzY63w39Bk.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C29C3AE2B783F82003DF84C /* libHSsimplex-chat-5.5.3.0-AUrnxTuqxo1yzY63w39Bk.a */; };
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; }; 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */; };
5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; }; 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260A27A30CFA00F70299 /* ChatListView.swift */; };
5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; }; 5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C2E260E27A30FDC00F70299 /* ChatView.swift */; };
@ -61,11 +66,6 @@
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; }; 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; }; 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; }; 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; };
5C83A1AD2B5EF67D00AE0A4A /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C83A1A82B5EF67D00AE0A4A /* libgmp.a */; };
5C83A1AE2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C83A1A92B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3-ghc9.6.3.a */; };
5C83A1AF2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C83A1AA2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3.a */; };
5C83A1B02B5EF67D00AE0A4A /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C83A1AB2B5EF67D00AE0A4A /* libffi.a */; };
5C83A1B12B5EF67D00AE0A4A /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C83A1AC2B5EF67D00AE0A4A /* libgmpxx.a */; };
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; }; 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; };
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; }; 5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; };
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */; }; 5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */; };
@ -278,6 +278,11 @@
5C245F3C2B501E98001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; }; 5C245F3C2B501E98001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
5C245F3D2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = "tr.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5C245F3D2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = "tr.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5C245F3E2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5C245F3E2B501F13001CC39F /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
5C29C3AA2B783F82003DF84C /* libHSsimplex-chat-5.5.3.0-AUrnxTuqxo1yzY63w39Bk-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.3.0-AUrnxTuqxo1yzY63w39Bk-ghc9.6.3.a"; sourceTree = "<group>"; };
5C29C3AB2B783F82003DF84C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C29C3AC2B783F82003DF84C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C29C3AD2B783F82003DF84C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C29C3AE2B783F82003DF84C /* libHSsimplex-chat-5.5.3.0-AUrnxTuqxo1yzY63w39Bk.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.3.0-AUrnxTuqxo1yzY63w39Bk.a"; sourceTree = "<group>"; };
5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; }; 5C2E260627A2941F00F70299 /* SimpleXAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXAPI.swift; sourceTree = "<group>"; };
5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; }; 5C2E260A27A30CFA00F70299 /* ChatListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = "<group>"; };
5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; }; 5C2E260E27A30FDC00F70299 /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
@ -325,11 +330,6 @@
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; }; 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; }; 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; }; 5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
5C83A1A82B5EF67D00AE0A4A /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C83A1A92B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3-ghc9.6.3.a"; sourceTree = "<group>"; };
5C83A1AA2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3.a"; sourceTree = "<group>"; };
5C83A1AB2B5EF67D00AE0A4A /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C83A1AC2B5EF67D00AE0A4A /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C84FE9129A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; }; 5C84FE9129A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
5C84FE9329A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = "nl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5C84FE9329A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = "nl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5C84FE9429A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5C84FE9429A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@ -514,13 +514,13 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5C29C3B02B783F82003DF84C /* libgmpxx.a in Frameworks */,
5C29C3B32B783F82003DF84C /* libHSsimplex-chat-5.5.3.0-AUrnxTuqxo1yzY63w39Bk.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C83A1B02B5EF67D00AE0A4A /* libffi.a in Frameworks */,
5C83A1AD2B5EF67D00AE0A4A /* libgmp.a in Frameworks */,
5C83A1AE2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3-ghc9.6.3.a in Frameworks */,
5C83A1AF2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C83A1B12B5EF67D00AE0A4A /* libgmpxx.a in Frameworks */, 5C29C3B12B783F82003DF84C /* libffi.a in Frameworks */,
5C29C3B22B783F82003DF84C /* libgmp.a in Frameworks */,
5C29C3AF2B783F82003DF84C /* libHSsimplex-chat-5.5.3.0-AUrnxTuqxo1yzY63w39Bk-ghc9.6.3.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -582,11 +582,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5C83A1AB2B5EF67D00AE0A4A /* libffi.a */, 5C29C3AC2B783F82003DF84C /* libffi.a */,
5C83A1A82B5EF67D00AE0A4A /* libgmp.a */, 5C29C3AD2B783F82003DF84C /* libgmp.a */,
5C83A1AC2B5EF67D00AE0A4A /* libgmpxx.a */, 5C29C3AB2B783F82003DF84C /* libgmpxx.a */,
5C83A1A92B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3-ghc9.6.3.a */, 5C29C3AA2B783F82003DF84C /* libHSsimplex-chat-5.5.3.0-AUrnxTuqxo1yzY63w39Bk-ghc9.6.3.a */,
5C83A1AA2B5EF67D00AE0A4A /* libHSsimplex-chat-5.5.0.4-HTW6wkBBAjO2GDtnvnI9O3.a */, 5C29C3AE2B783F82003DF84C /* libHSsimplex-chat-5.5.3.0-AUrnxTuqxo1yzY63w39Bk.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1509,7 +1509,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 194; CURRENT_PROJECT_VERSION = 198;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@ -1531,7 +1531,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 5.5; MARKETING_VERSION = 5.5.3;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -1552,7 +1552,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 194; CURRENT_PROJECT_VERSION = 198;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@ -1574,7 +1574,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 5.5; MARKETING_VERSION = 5.5.3;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -1633,7 +1633,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 194; CURRENT_PROJECT_VERSION = 198;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -1646,7 +1646,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 5.5; MARKETING_VERSION = 5.5.3;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -1665,7 +1665,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 194; CURRENT_PROJECT_VERSION = 198;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@ -1678,7 +1678,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 5.5; MARKETING_VERSION = 5.5.3;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -1697,7 +1697,7 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 194; CURRENT_PROJECT_VERSION = 198;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@ -1721,7 +1721,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
MARKETING_VERSION = 5.5; MARKETING_VERSION = 5.5.3;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@ -1743,7 +1743,7 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 194; CURRENT_PROJECT_VERSION = 198;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@ -1767,7 +1767,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
MARKETING_VERSION = 5.5; MARKETING_VERSION = 5.5.3;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;

View File

@ -105,6 +105,11 @@ public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? {
return nil return nil
} }
public func chatJsonLength(_ s: String) -> Int {
var c = s.cString(using: .utf8)!
return Int(chat_json_length(&c))
}
struct ParsedMarkdown: Decodable { struct ParsedMarkdown: Decodable {
var formattedText: [FormattedText]? var formattedText: [FormattedText]?
} }

View File

@ -25,6 +25,7 @@ extern char *chat_parse_markdown(char *str);
extern char *chat_parse_server(char *str); extern char *chat_parse_server(char *str);
extern char *chat_password_hash(char *pwd, char *salt); extern char *chat_password_hash(char *pwd, char *salt);
extern char *chat_valid_name(char *name); extern char *chat_valid_name(char *name);
extern int chat_json_length(char *str);
extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len); extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len);
extern char *chat_decrypt_media(char *key, char *frame, int len); extern char *chat_decrypt_media(char *key, char *frame, int len);

View File

@ -1,11 +1,12 @@
package chat.simplex.app package chat.simplex.app
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.* import android.os.*
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatDelegate import androidx.compose.ui.platform.ClipboardManager
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import chat.simplex.app.model.NtfManager import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.getUserIdFromIntent import chat.simplex.app.model.NtfManager.getUserIdFromIntent
@ -58,6 +59,17 @@ class MainActivity: FragmentActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
AppLock.recheckAuthState() AppLock.recheckAuthState()
withApi {
delay(1000)
if (!isAppOnForeground) return@withApi
/**
* When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies
* access to clipboard because the app considered in background.
* This will ensure that the app will get the event on resume
* */
val service = getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
chatModel.clipboardHasText.value = service.hasPrimaryClip()
}
} }
override fun onPause() { override fun onPause() {

View File

@ -71,7 +71,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
Log.d(TAG, "onStateChanged: $event") Log.d(TAG, "onStateChanged: $event")
withBGApi { withLongRunningApi {
when (event) { when (event) {
Lifecycle.Event.ON_START -> { Lifecycle.Event.ON_START -> {
isAppOnForeground = true isAppOnForeground = true
@ -97,13 +97,6 @@ class SimplexApp: Application(), LifecycleEventObserver {
} }
Lifecycle.Event.ON_RESUME -> { Lifecycle.Event.ON_RESUME -> {
isAppOnForeground = true isAppOnForeground = true
/**
* When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies
* access to clipboard because the app considered in background.
* This will ensure that the app will get the event on resume
* */
val service = androidAppContext.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
chatModel.clipboardHasText.value = service.hasPrimaryClip()
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) { if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) {
SimplexService.showBackgroundServiceNoticeIfNeeded() SimplexService.showBackgroundServiceNoticeIfNeeded()
} }

View File

@ -104,7 +104,7 @@ class SimplexService: Service() {
if (wakeLock != null || isStartingService) return if (wakeLock != null || isStartingService) return
val self = this val self = this
isStartingService = true isStartingService = true
withLongRunningApi(slow = 30_000, deadlock = 60_000) { withLongRunningApi {
val chatController = ChatController val chatController = ChatController
waitDbMigrationEnds(chatController) waitDbMigrationEnds(chatController)
try { try {

View File

@ -14,17 +14,27 @@ import chat.simplex.common.views.helpers.*
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlin.math.min
actual fun ClipboardManager.shareText(text: String) { actual fun ClipboardManager.shareText(text: String) {
val sendIntent: Intent = Intent().apply { var text = text
action = Intent.ACTION_SEND for (i in 10 downTo 1) {
putExtra(Intent.EXTRA_TEXT, text) try {
type = "text/plain" val sendIntent: Intent = Intent().apply {
flags = FLAG_ACTIVITY_NEW_TASK action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
flags = FLAG_ACTIVITY_NEW_TASK
}
val shareIntent = Intent.createChooser(sendIntent, null)
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(shareIntent)
break
} catch (e: Exception) {
Log.e(TAG, "Failed to share text: ${e.stackTraceToString()}")
text = text.substring(0, min(i * 1000, text.length))
}
} }
val shareIntent = Intent.createChooser(sendIntent, null)
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(shareIntent)
} }
actual fun shareFile(text: String, fileSource: CryptoFile) { actual fun shareFile(text: String, fileSource: CryptoFile) {

View File

@ -12,6 +12,8 @@ import androidx.activity.compose.setContent
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import chat.simplex.common.AppScreen import chat.simplex.common.AppScreen
import chat.simplex.common.model.clear
import chat.simplex.common.ui.theme.SimpleXTheme
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import androidx.compose.ui.platform.LocalContext as LocalContext1 import androidx.compose.ui.platform.LocalContext as LocalContext1
import chat.simplex.res.MR import chat.simplex.res.MR
@ -112,7 +114,8 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.app_was_crashed), title = generalGetString(MR.strings.app_was_crashed),
text = e.stackTraceToString() text = e.stackTraceToString(),
shareText = true
) )
} }
} }

View File

@ -66,6 +66,7 @@ extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str); extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name); extern char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str);
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length); extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path); extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
@ -163,6 +164,14 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
return res; return res;
} }
JNIEXPORT int JNICALL
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
int res = chat_json_length(_str);
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) { Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE); const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);

View File

@ -39,6 +39,7 @@ extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str); extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name); extern char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str);
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length); extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path); extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
@ -173,6 +174,14 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
return res; return res;
} }
JNIEXPORT int JNICALL
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
const char *_str = encode_to_utf8_chars(env, str);
int res = chat_json_length(_str);
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) { Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
const char *_path = encode_to_utf8_chars(env, path); const char *_path = encode_to_utf8_chars(env, path);

View File

@ -108,6 +108,7 @@ fun MainScreen() {
val localUserCreated = chatModel.localUserCreated.value val localUserCreated = chatModel.localUserCreated.value
var showInitializationView by remember { mutableStateOf(false) } var showInitializationView by remember { mutableStateOf(false) }
when { when {
chatModel.dbMigrationInProgress.value -> DefaultProgressView(stringResource(MR.strings.database_migration_in_progress))
chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database)) chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database))
showChatDatabaseError -> { showChatDatabaseError -> {
// Prevent showing keyboard on Android when: passcode enabled and database password not saved // Prevent showing keyboard on Android when: passcode enabled and database password not saved

View File

@ -2,6 +2,7 @@ package chat.simplex.common.model
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
@ -48,6 +49,7 @@ object ChatModel {
val chatDbEncrypted = mutableStateOf<Boolean?>(false) val chatDbEncrypted = mutableStateOf<Boolean?>(false)
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null) val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
val ctrlInitInProgress = mutableStateOf(false) val ctrlInitInProgress = mutableStateOf(false)
val dbMigrationInProgress = mutableStateOf(false)
val chats = mutableStateListOf<Chat>() val chats = mutableStateListOf<Chat>()
// map of connections network statuses, key is agent connection id // map of connections network statuses, key is agent connection id
val networkStatuses = mutableStateMapOf<String, NetworkStatus>() val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
@ -55,7 +57,7 @@ object ChatModel {
// current chat // current chat
val chatId = mutableStateOf<String?>(null) val chatId = mutableStateOf<String?>(null)
val chatItems = mutableStateListOf<ChatItem>() val chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
// rhId, chatId // rhId, chatId
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList()) val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
val chatItemStatuses = mutableMapOf<Long, CIStatus>() val chatItemStatuses = mutableMapOf<Long, CIStatus>()
@ -63,8 +65,6 @@ object ChatModel {
val terminalItems = mutableStateOf<List<TerminalItem>>(listOf()) val terminalItems = mutableStateOf<List<TerminalItem>>(listOf())
val userAddress = mutableStateOf<UserContactLinkRec?>(null) val userAddress = mutableStateOf<UserContactLinkRec?>(null)
// Allows to temporary save servers that are being edited on multiple screens
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null)
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None) val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
// set when app opened from external intent // set when app opened from external intent
@ -269,18 +269,15 @@ object ChatModel {
} else { } else {
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
} }
Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
// add to current chat // add to current chat
if (chatId.value == cInfo.id) { if (chatId.value == cInfo.id) {
Log.d(TAG, "TODOCHAT: addChatItem: chatIds are equal, size ${chatItems.size}")
// Prevent situation when chat item already in the list received from backend // Prevent situation when chat item already in the list received from backend
if (chatItems.none { it.id == cItem.id }) { if (chatItems.value.none { it.id == cItem.id }) {
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem) chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem)
} else { } else {
chatItems.add(cItem) chatItems.add(cItem)
Log.d(TAG, "TODOCHAT: addChatItem: added to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
} }
} }
} }
@ -307,14 +304,13 @@ object ChatModel {
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
res = true res = true
} }
Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
return withContext(Dispatchers.Main) { return withContext(Dispatchers.Main) {
// update current chat // update current chat
if (chatId.value == cInfo.id) { if (chatId.value == cInfo.id) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id } val items = chatItems.value
val itemIndex = items.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) { if (itemIndex >= 0) {
chatItems[itemIndex] = cItem items[itemIndex] = cItem
Log.d(TAG, "TODOCHAT: upsertChatItem: updated in chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
false false
} else { } else {
val status = chatItemStatuses.remove(cItem.id) val status = chatItemStatuses.remove(cItem.id)
@ -324,7 +320,6 @@ object ChatModel {
cItem cItem
} }
chatItems.add(ci) chatItems.add(ci)
Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
true true
} }
} else { } else {
@ -336,9 +331,10 @@ object ChatModel {
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) { suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (chatId.value == cInfo.id) { if (chatId.value == cInfo.id) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id } val items = chatItems.value
val itemIndex = items.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) { if (itemIndex >= 0) {
chatItems[itemIndex] = cItem items[itemIndex] = cItem
} }
} else if (status != null) { } else if (status != null) {
chatItemStatuses[cItem.id] = status chatItemStatuses[cItem.id] = status
@ -362,10 +358,10 @@ object ChatModel {
} }
// remove from current chat // remove from current chat
if (chatId.value == cInfo.id) { if (chatId.value == cInfo.id) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id } chatItems.removeAll {
if (itemIndex >= 0) { val remove = it.id == cItem.id
AudioPlayer.stop(chatItems[itemIndex]) if (remove) { AudioPlayer.stop(it) }
chatItems.removeAt(itemIndex) remove
} }
} }
} }
@ -406,7 +402,7 @@ object ChatModel {
} }
fun removeLiveDummy() { fun removeLiveDummy() {
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
chatItems.removeLast() chatItems.removeLast()
} }
} }
@ -438,14 +434,14 @@ object ChatModel {
var markedRead = 0 var markedRead = 0
if (chatId.value == cInfo.id) { if (chatId.value == cInfo.id) {
var i = 0 var i = 0
Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marking read ${cInfo.id}, current chatId ${chatId.value}, size was ${chatItems.size}") val items = chatItems.value
while (i < chatItems.count()) { while (i < items.size) {
val item = chatItems[i] val item = items[i]
if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) { if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) {
val newItem = item.withStatus(CIStatus.RcvRead()) val newItem = item.withStatus(CIStatus.RcvRead())
chatItems[i] = newItem items[i] = newItem
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) { if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
chatItems[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy( items[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS))) deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS)))
) )
} }
@ -453,7 +449,6 @@ object ChatModel {
} }
i += 1 i += 1
} }
Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marked read ${cInfo.id}, current chatId ${chatId.value}, size now ${chatItems.size}")
} }
return markedRead return markedRead
} }
@ -644,7 +639,8 @@ object ChatModel {
} }
fun addTerminalItem(item: TerminalItem) { fun addTerminalItem(item: TerminalItem) {
if (terminalItems.value.size >= 500) { val maxItems = if (appPreferences.developerTools.get()) 500 else 200
if (terminalItems.value.size >= maxItems) {
terminalItems.value = terminalItems.value.subList(1, terminalItems.value.size) terminalItems.value = terminalItems.value.subList(1, terminalItems.value.size)
} }
terminalItems.value += item terminalItems.value += item
@ -2006,6 +2002,46 @@ data class ChatItem (
} }
} }
fun MutableState<SnapshotStateList<ChatItem>>.add(index: Int, chatItem: ChatItem) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, chatItem) }
}
fun MutableState<SnapshotStateList<ChatItem>>.add(chatItem: ChatItem) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(chatItem) }
}
fun MutableState<SnapshotStateList<ChatItem>>.addAll(index: Int, chatItems: List<ChatItem>) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(index, chatItems) }
}
fun MutableState<SnapshotStateList<ChatItem>>.addAll(chatItems: List<ChatItem>) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(chatItems) }
}
fun MutableState<SnapshotStateList<ChatItem>>.removeAll(block: (ChatItem) -> Boolean) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAll(block) }
}
fun MutableState<SnapshotStateList<ChatItem>>.removeAt(index: Int) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAt(index) }
}
fun MutableState<SnapshotStateList<ChatItem>>.removeLast() {
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeLast() }
}
fun MutableState<SnapshotStateList<ChatItem>>.replaceAll(chatItems: List<ChatItem>) {
value = SnapshotStateList<ChatItem>().apply { addAll(chatItems) }
}
fun MutableState<SnapshotStateList<ChatItem>>.clear() {
value = SnapshotStateList<ChatItem>()
}
fun State<SnapshotStateList<ChatItem>>.asReversed(): MutableList<ChatItem> = value.asReversed()
val State<List<ChatItem>>.size: Int get() = value.size
enum class CIMergeCategory { enum class CIMergeCategory {
MemberConnected, MemberConnected,
RcvGroupEvent, RcvGroupEvent,

View File

@ -451,7 +451,21 @@ object ChatController {
} }
try { try {
val msg = recvMsg(ctrl) val msg = recvMsg(ctrl)
if (msg != null) processReceivedMsg(msg) if (msg != null) {
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
processReceivedMsg(msg)
}
if (finishedWithoutTimeout == null) {
Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType)
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_slow_function_title),
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.resp.responseType + "\n" + Exception().stackTraceToString()),
shareText = true
)
}
}
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "ChatController recvMsg/processReceivedMsg exception: " + e.stackTraceToString()); Log.e(TAG, "ChatController recvMsg/processReceivedMsg exception: " + e.stackTraceToString());
} catch (e: Throwable) { } catch (e: Throwable) {
@ -1685,7 +1699,7 @@ object ChatController {
chatModel.networkStatuses[s.agentConnId] = s.networkStatus chatModel.networkStatuses[s.agentConnId] = s.networkStatus
} }
} }
is CR.NewChatItem -> { is CR.NewChatItem -> withBGApi {
val cInfo = r.chatItem.chatInfo val cInfo = r.chatItem.chatInfo
val cItem = r.chatItem.chatItem val cItem = r.chatItem.chatItem
if (active(r.user)) { if (active(r.user)) {
@ -1700,7 +1714,7 @@ object ChatController {
((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) ((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV)
|| (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV) || (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) { || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
withBGApi { receiveFile(rhId, r.user, file.fileId, auto = true) } receiveFile(rhId, r.user, file.fileId, auto = true)
} }
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) { if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) {
ntfManager.notifyMessageReceived(r.user, cInfo, cItem) ntfManager.notifyMessageReceived(r.user, cInfo, cItem)

View File

@ -28,6 +28,7 @@ external fun chatParseMarkdown(str: String): String
external fun chatParseServer(str: String): String external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String external fun chatPasswordHash(pwd: String, salt: String): String
external fun chatValidName(name: String): String external fun chatValidName(name: String): String
external fun chatJsonLength(str: String): Int
external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any> external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String
@ -42,7 +43,7 @@ val appPreferences: AppPreferences
val chatController: ChatController = ChatController val chatController: ChatController = ChatController
fun initChatControllerAndRunMigrations() { fun initChatControllerAndRunMigrations() {
withLongRunningApi(slow = 30_000, deadlock = 60_000) { withLongRunningApi {
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) { if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
initChatController(startChat = ::showStartChatAfterRestartAlert) initChatController(startChat = ::showStartChatAfterRestartAlert)
} else { } else {
@ -58,10 +59,23 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
chatModel.ctrlInitInProgress.value = true chatModel.ctrlInitInProgress.value = true
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value) var migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value)
val res: DBMigrationResult = kotlin.runCatching { var res: DBMigrationResult = runCatching {
json.decodeFromString<DBMigrationResult>(migrated[0] as String) json.decodeFromString<DBMigrationResult>(migrated[0] as String)
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
val rerunMigration = res is DBMigrationResult.ErrorMigration && when (res.migrationError) {
// we don't allow to run down migrations without confirmation in UI, so currently it won't be YesUpDown
is MigrationError.Upgrade -> confirm == MigrationConfirmation.YesUp || confirm == MigrationConfirmation.YesUpDown
is MigrationError.Downgrade -> confirm == MigrationConfirmation.YesUpDown
is MigrationError.Error -> false
}
if (rerunMigration) {
chatModel.dbMigrationInProgress.value = true
migrated = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value)
res = runCatching {
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
}
val ctrl = if (res is DBMigrationResult.OK) { val ctrl = if (res is DBMigrationResult.OK) {
migrated[1] as Long migrated[1] as Long
} else null } else null
@ -119,6 +133,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
} }
} finally { } finally {
chatModel.ctrlInitInProgress.value = false chatModel.ctrlInitInProgress.value = false
chatModel.dbMigrationInProgress.value = false
} }
} }

View File

@ -55,7 +55,7 @@ abstract class NtfManager {
} }
fun openChatAction(userId: Long?, chatId: ChatId) { fun openChatAction(userId: Long?, chatId: ChatId) {
withLongRunningApi(slow = 30_000, deadlock = 60_000) { withLongRunningApi {
awaitChatStartedIfNeeded(chatModel) awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
// TODO include remote host ID in desktop notifications? // TODO include remote host ID in desktop notifications?
@ -70,7 +70,7 @@ abstract class NtfManager {
} }
fun showChatsAction(userId: Long?) { fun showChatsAction(userId: Long?) {
withLongRunningApi(slow = 30_000, deadlock = 60_000) { withLongRunningApi {
awaitChatStartedIfNeeded(chatModel) awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
// TODO include remote host ID in desktop notifications? // TODO include remote host ID in desktop notifications?

View File

@ -324,7 +324,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
.fillMaxHeight(), .fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween verticalArrangement = Arrangement.SpaceBetween
) { ) {
LaunchedEffect(Unit) { LaunchedEffect(ciInfo) {
if (ciInfo.memberDeliveryStatuses != null) { if (ciInfo.memberDeliveryStatuses != null) {
selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses) selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses)
} }

View File

@ -67,14 +67,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
launch { launch {
snapshotFlow { chatModel.chatId.value } snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged() .distinctUntilChanged()
.onEach { Log.d(TAG, "TODOCHAT: chatId: activeChatId ${activeChat.value?.id} == new chatId $it ${activeChat.value?.id == it} ") }
.filterNotNull() .filterNotNull()
.collect { chatId -> .collect { chatId ->
if (activeChat.value?.id != chatId) { if (activeChat.value?.id != chatId) {
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
// Also for situation when chatId changes after clicking in notification, etc // Also for situation when chatId changes after clicking in notification, etc
activeChat.value = chatModel.getChat(chatId) activeChat.value = chatModel.getChat(chatId)
Log.d(TAG, "TODOCHAT: chatId: activeChatId became ${activeChat.value?.id}")
} }
markUnreadChatAsRead(activeChat, chatModel) markUnreadChatAsRead(activeChat, chatModel)
} }
@ -94,12 +92,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
} }
} }
.distinctUntilChanged() .distinctUntilChanged()
.onEach { Log.d(TAG, "TODOCHAT: chats: activeChatId ${activeChat.value?.id} == new chatId ${it?.id} ${activeChat.value?.id == it?.id} ") }
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions // Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
.filter { it != null && it?.chatInfo != activeChat.value?.chatInfo } .filter { it != null && it.chatInfo != activeChat.value?.chatInfo }
.collect { .collect {
activeChat.value = it activeChat.value = it
Log.d(TAG, "TODOCHAT: chats: activeChatId became ${activeChat.value?.id}")
} }
} }
} }
@ -150,7 +146,6 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}, },
attachmentOption, attachmentOption,
attachmentBottomSheetState, attachmentBottomSheetState,
chatModel.chatItems,
searchText, searchText,
useLinkPreviews = useLinkPreviews, useLinkPreviews = useLinkPreviews,
linkMode = chatModel.simplexLinkMode.value, linkMode = chatModel.simplexLinkMode.value,
@ -228,19 +223,17 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
loadPrevMessages = { loadPrevMessages = {
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
val firstId = chatModel.chatItems.firstOrNull()?.id val firstId = chatModel.chatItems.value.firstOrNull()?.id
if (c != null && firstId != null) { if (c != null && firstId != null) {
withBGApi { withBGApi {
Log.d(TAG, "TODOCHAT: loadPrevMessages: loading for ${c.id}, current chatId ${ChatModel.chatId.value}, size was ${ChatModel.chatItems.size}")
apiLoadPrevMessages(c, chatModel, firstId, searchText.value) apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
Log.d(TAG, "TODOCHAT: loadPrevMessages: loaded for ${c.id}, current chatId ${ChatModel.chatId.value}, size now ${ChatModel.chatItems.size}")
} }
} }
}, },
deleteMessage = { itemId, mode -> deleteMessage = { itemId, mode ->
withBGApi { withBGApi {
val cInfo = chat.chatInfo val cInfo = chat.chatInfo
val toDeleteItem = chatModel.chatItems.firstOrNull { it.id == itemId } val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo) val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
val groupInfo = toModerate?.first val groupInfo = toModerate?.first
val groupMember = toModerate?.second val groupMember = toModerate?.second
@ -406,12 +399,15 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel) setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
} }
ModalManager.end.closeModals() ModalManager.end.closeModals()
ModalManager.end.showModal(endButtons = { ModalManager.end.showModalCloseable(endButtons = {
ShareButton { ShareButton {
clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get())) clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
} }
}) { }) { close ->
ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get()) ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
KeyChangeEffect(chatModel.chatId.value) {
close()
}
} }
} }
} }
@ -497,7 +493,6 @@ fun ChatLayout(
composeView: (@Composable () -> Unit), composeView: (@Composable () -> Unit),
attachmentOption: MutableState<AttachmentOption?>, attachmentOption: MutableState<AttachmentOption?>,
attachmentBottomSheetState: ModalBottomSheetState, attachmentBottomSheetState: ModalBottomSheetState,
chatItems: List<ChatItem>,
searchValue: State<String>, searchValue: State<String>,
useLinkPreviews: Boolean, useLinkPreviews: Boolean,
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
@ -584,7 +579,7 @@ fun ChatLayout(
.padding(contentPadding) .padding(contentPadding)
) { ) {
ChatItemsList( ChatItemsList(
chat, unreadCount, composeState, chatItems, searchValue, chat, unreadCount, composeState, searchValue,
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
@ -842,7 +837,6 @@ fun BoxWithConstraintsScope.ChatItemsList(
chat: Chat, chat: Chat,
unreadCount: State<Int>, unreadCount: State<Int>,
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
chatItems: List<ChatItem>,
searchValue: State<String>, searchValue: State<String>,
useLinkPreviews: Boolean, useLinkPreviews: Boolean,
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
@ -871,7 +865,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
ScrollToBottom(chat.id, listState, chatItems) ScrollToBottom(chat.id, listState, chatModel.chatItems)
var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) } var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) }
// Scroll to bottom when search value changes from something to nothing and back // Scroll to bottom when search value changes from something to nothing and back
LaunchedEffect(searchValue.value.isEmpty()) { LaunchedEffect(searchValue.value.isEmpty()) {
@ -888,7 +882,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages) PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
Spacer(Modifier.size(8.dp)) Spacer(Modifier.size(8.dp))
val reversedChatItems by remember { derivedStateOf { chatItems.reversed().toList() } } val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } }
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() } val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
val scrollToItem: (Long) -> Unit = { itemId: Long -> val scrollToItem: (Long) -> Unit = { itemId: Long ->
val index = reversedChatItems.indexOfFirst { it.id == itemId } val index = reversedChatItems.indexOfFirst { it.id == itemId }
@ -941,7 +935,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
} }
} }
val provider = { val provider = {
providerForGallery(i, chatItems, cItem.id) { indexInReversed -> providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed ->
scope.launch { scope.launch {
listState.scrollToItem( listState.scrollToItem(
kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1), kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1),
@ -1064,11 +1058,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
} }
} }
} }
FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState) FloatingButtons(chatModel.chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
} }
@Composable @Composable
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: List<ChatItem>) { private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: State<List<ChatItem>>) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
// Helps to scroll to bottom after moving from Group to Direct chat // Helps to scroll to bottom after moving from Group to Direct chat
// and prevents scrolling to bottom on orientation change // and prevents scrolling to bottom on orientation change
@ -1086,7 +1080,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
* When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise * When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise
* */ * */
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
snapshotFlow { chatItems.lastOrNull()?.id } snapshotFlow { chatItems.value.lastOrNull()?.id }
.distinctUntilChanged() .distinctUntilChanged()
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it } .filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
.collect { .collect {
@ -1109,7 +1103,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
@Composable @Composable
fun BoxWithConstraintsScope.FloatingButtons( fun BoxWithConstraintsScope.FloatingButtons(
chatItems: List<ChatItem>, chatItems: State<List<ChatItem>>,
unreadCount: State<Int>, unreadCount: State<Int>,
minUnreadItemId: Long, minUnreadItemId: Long,
searchValue: State<String>, searchValue: State<String>,
@ -1143,10 +1137,11 @@ fun BoxWithConstraintsScope.FloatingButtons(
val bottomUnreadCount by remember { val bottomUnreadCount by remember {
derivedStateOf { derivedStateOf {
if (unreadCount.value == 0) return@derivedStateOf 0 if (unreadCount.value == 0) return@derivedStateOf 0
val from = chatItems.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems val items = chatItems.value
if (chatItems.size <= from || from < 0) return@derivedStateOf 0 val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
if (items.size <= from || from < 0) return@derivedStateOf 0
chatItems.subList(from, chatItems.size).count { it.isRcvNew } items.subList(from, items.size).count { it.isRcvNew }
} }
} }
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt() val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
@ -1192,7 +1187,7 @@ fun BoxWithConstraintsScope.FloatingButtons(
painterResource(MR.images.ic_check), painterResource(MR.images.ic_check),
onClick = { onClick = {
markRead( markRead(
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
bottomUnreadCount bottomUnreadCount
) )
showDropDown.value = false showDropDown.value = false
@ -1497,7 +1492,6 @@ fun PreviewChatLayout() {
composeView = {}, composeView = {},
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) }, attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
chatItems = chatItems,
searchValue, searchValue,
useLinkPreviews = true, useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION, linkMode = SimplexLinkMode.DESCRIPTION,
@ -1570,7 +1564,6 @@ fun PreviewGroupChatLayout() {
composeView = {}, composeView = {},
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) }, attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
chatItems = chatItems,
searchValue, searchValue,
useLinkPreviews = true, useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION, linkMode = SimplexLinkMode.DESCRIPTION,

View File

@ -267,7 +267,7 @@ fun ComposeView(
fun loadLinkPreview(url: String, wait: Long? = null) { fun loadLinkPreview(url: String, wait: Long? = null) {
if (pendingLinkUrl.value == url) { if (pendingLinkUrl.value == url) {
composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null)) composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null))
withLongRunningApi(slow = 30_000, deadlock = 60_000) { withLongRunningApi(slow = 60_000) {
if (wait != null) delay(wait) if (wait != null) delay(wait)
val lp = getLinkPreview(url) val lp = getLinkPreview(url)
if (lp != null && pendingLinkUrl.value == url) { if (lp != null && pendingLinkUrl.value == url) {
@ -551,7 +551,7 @@ fun ComposeView(
} }
fun sendMessage(ttl: Int?) { fun sendMessage(ttl: Int?) {
withLongRunningApi(slow = 30_000, deadlock = 60_000) { withLongRunningApi(slow = 120_000) {
sendMessageAsync(null, false, ttl) sendMessageAsync(null, false, ttl)
} }
} }
@ -583,6 +583,10 @@ fun ComposeView(
} }
fun cancelLinkPreview() { fun cancelLinkPreview() {
val pendingLink = pendingLinkUrl.value
if (pendingLink != null) {
cancelledLinks.add(pendingLink)
}
val uri = composeState.value.linkPreview?.uri val uri = composeState.value.linkPreview?.uri
if (uri != null) { if (uri != null) {
cancelledLinks.add(uri) cancelledLinks.add(uri)
@ -661,7 +665,7 @@ fun ComposeView(
fun editPrevMessage() { fun editPrevMessage() {
if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return
val lastEditable = chatModel.chatItems.findLast { it.meta.editable } val lastEditable = chatModel.chatItems.value.findLast { it.meta.editable }
if (lastEditable != null) { if (lastEditable != null) {
composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews) composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews)
} }

View File

@ -54,7 +54,7 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
}, },
inviteMembers = { inviteMembers = {
allowModifyMembers = false allowModifyMembers = false
withLongRunningApi(slow = 30_000, deadlock = 60_000) { withLongRunningApi(slow = 120_000) {
for (contactId in selectedContacts) { for (contactId in selectedContacts) {
val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value) val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value)
if (member != null) { if (member != null) {

View File

@ -152,7 +152,7 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved), text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved),
confirmText = generalGetString(MR.strings.leave_group_button), confirmText = generalGetString(MR.strings.leave_group_button),
onConfirm = { onConfirm = {
withBGApi { withLongRunningApi(60_000) {
chatModel.controller.leaveGroup(rhId, groupInfo.groupId) chatModel.controller.leaveGroup(rhId, groupInfo.groupId)
close?.invoke() close?.invoke()
} }

View File

@ -3,11 +3,9 @@ package chat.simplex.common.views.chat.group
import InfoRow import InfoRow
import SectionBottomSpacer import SectionBottomSpacer
import SectionDividerSpaced import SectionDividerSpaced
import SectionItemView
import SectionSpacer import SectionSpacer
import SectionTextFooter import SectionTextFooter
import SectionView import SectionView
import TextIconSpaced
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import java.net.URI import java.net.URI
import androidx.compose.foundation.* import androidx.compose.foundation.*
@ -74,9 +72,8 @@ fun GroupMemberInfoView(
if (chatModel.getContactChat(it) == null) { if (chatModel.getContactChat(it) == null) {
chatModel.addChat(c) chatModel.addChat(c)
} }
chatModel.chatItems.clear()
chatModel.chatItemStatuses.clear() chatModel.chatItemStatuses.clear()
chatModel.chatItems.addAll(c.chatItems) chatModel.chatItems.replaceAll(c.chatItems)
chatModel.chatId.value = c.id chatModel.chatId.value = c.id
closeAll() closeAll()
} }

View File

@ -3,6 +3,7 @@ package chat.simplex.common.views.chat.group
import SectionBottomSpacer import SectionBottomSpacer
import SectionDividerSpaced import SectionDividerSpaced
import SectionItemView import SectionItemView
import SectionTextFooter
import SectionView import SectionView
import TextIconSpaced import TextIconSpaced
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -14,6 +15,7 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
@ -27,9 +29,13 @@ import chat.simplex.common.views.chat.item.MarkdownText
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.GroupInfo import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.chatJsonLength
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
private const val maxByteCount = 1200
@Composable @Composable
fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) { fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) {
var gInfo by remember { mutableStateOf(groupInfo) } var gInfo by remember { mutableStateOf(groupInfo) }
@ -54,8 +60,11 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
ModalView( ModalView(
close = { close = {
if (welcomeText.value == gInfo.groupProfile.description || (welcomeText.value == "" && gInfo.groupProfile.description == null)) close() when {
else showUnsavedChangesAlert({ save(close) }, close) welcomeTextUnchanged(welcomeText, gInfo) -> close()
!welcomeTextFitsLimit(welcomeText) -> showUnsavedChangesTooLongAlert(close)
else -> showUnsavedChangesAlert({ save(close) }, close)
}
}, },
) { ) {
GroupWelcomeLayout( GroupWelcomeLayout(
@ -67,6 +76,14 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
} }
} }
private fun welcomeTextUnchanged(welcomeText: MutableState<String>, groupInfo: GroupInfo): Boolean {
return welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)
}
private fun welcomeTextFitsLimit(welcomeText: MutableState<String>): Boolean {
return chatJsonLength(welcomeText.value) <= maxByteCount
}
@Composable @Composable
private fun GroupWelcomeLayout( private fun GroupWelcomeLayout(
welcomeText: MutableState<String>, welcomeText: MutableState<String>,
@ -95,6 +112,13 @@ private fun GroupWelcomeLayout(
} else { } else {
TextPreview(wt.value, linkMode) TextPreview(wt.value, linkMode)
} }
SectionTextFooter(
if (!welcomeTextFitsLimit(wt)) { generalGetString(MR.strings.message_too_large) } else "",
color = if (welcomeTextFitsLimit(wt)) MaterialTheme.colors.secondary else Color.Red
)
Spacer(Modifier.size(8.dp))
ChangeModeButton( ChangeModeButton(
editMode.value, editMode.value,
click = { click = {
@ -104,10 +128,18 @@ private fun GroupWelcomeLayout(
) )
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) } CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) }
SectionDividerSpaced(maxBottomPadding = false)
Divider(
Modifier.padding(
start = DEFAULT_PADDING_HALF,
top = 8.dp,
end = DEFAULT_PADDING_HALF,
bottom = 8.dp)
)
SaveButton( SaveButton(
save = save, save = save,
disabled = wt.value == groupInfo.groupProfile.description || (wt.value == "" && groupInfo.groupProfile.description == null) disabled = welcomeTextUnchanged(wt, groupInfo) || !welcomeTextFitsLimit(wt)
) )
} else { } else {
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
@ -182,3 +214,11 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
onDismiss = revert, onDismiss = revert,
) )
} }
private fun showUnsavedChangesTooLongAlert(revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.welcome_message_is_too_long),
confirmText = generalGetString(MR.strings.exit_without_saving),
onConfirm = revert,
)
}

View File

@ -94,7 +94,7 @@ fun CIFileView(
FileProtocol.LOCAL -> {} FileProtocol.LOCAL -> {}
} }
file.fileStatus is CIFileStatus.RcvComplete || (file.fileStatus is CIFileStatus.SndStored && file.fileProtocol == FileProtocol.LOCAL) -> { file.fileStatus is CIFileStatus.RcvComplete || (file.fileStatus is CIFileStatus.SndStored && file.fileProtocol == FileProtocol.LOCAL) -> {
withLongRunningApi(slow = 60_000, deadlock = 600_000) { withLongRunningApi(slow = 600_000) {
var filePath = getLoadedFilePath(file) var filePath = getLoadedFilePath(file)
if (chatModel.connectedToRemote() && filePath == null) { if (chatModel.connectedToRemote() && filePath == null) {
file.loadRemoteFile(true) file.loadRemoteFile(true)

View File

@ -41,7 +41,7 @@ fun CIVideoView(
val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) } val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) }
if (chatModel.connectedToRemote()) { if (chatModel.connectedToRemote()) {
LaunchedEffect(file) { LaunchedEffect(file) {
withLongRunningApi(slow = 60_000, deadlock = 600_000) { withLongRunningApi(slow = 600_000) {
if (file != null && file.loaded && getLoadedFilePath(file) == null) { if (file != null && file.loaded && getLoadedFilePath(file) == null) {
file.loadRemoteFile(false) file.loadRemoteFile(false)
filePath.value = getLoadedFilePath(file) filePath.value = getLoadedFilePath(file)

View File

@ -177,7 +177,8 @@ fun ChatItemView(
fun MsgContentItemDropdownMenu() { fun MsgContentItemDropdownMenu() {
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file) val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
when { when {
cItem.content.msgContent != null -> { // cItem.id check is a special case for live message chat item which has negative ID while not sent yet
cItem.content.msgContent != null && cItem.id >= 0 -> {
DefaultDropdownMenu(showMenu) { DefaultDropdownMenu(showMenu) {
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) { if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
MsgReactionsMenu() MsgReactionsMenu()
@ -212,7 +213,7 @@ fun ChatItemView(
showMenu.value = false showMenu.value = false
} }
if (chatModel.connectedToRemote() && fileSource == null) { if (chatModel.connectedToRemote() && fileSource == null) {
withLongRunningApi(slow = 60_000, deadlock = 600_000) { withLongRunningApi(slow = 600_000) {
cItem.file?.loadRemoteFile(true) cItem.file?.loadRemoteFile(true)
fileSource = getLoadedFileSource(cItem.file) fileSource = getLoadedFileSource(cItem.file)
shareIfExists() shareIfExists()
@ -526,8 +527,9 @@ fun DeleteItemAction(
val range = chatViewItemsRange(currIndex, prevHidden) val range = chatViewItemsRange(currIndex, prevHidden)
if (range != null) { if (range != null) {
val itemIds: ArrayList<Long> = arrayListOf() val itemIds: ArrayList<Long> = arrayListOf()
val reversedChatItems = chatModel.chatItems.asReversed()
for (i in range) { for (i in range) {
itemIds.add(chatModel.chatItems.asReversed()[i].id) itemIds.add(reversedChatItems[i].id)
} }
deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages) deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages)
} else { } else {

View File

@ -212,18 +212,15 @@ suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) {
} }
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) { suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) {
Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId) val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId)
if (chat != null) { if (chat != null) {
openLoadedChat(chat, chatModel) openLoadedChat(chat, chatModel)
Log.d(TAG, "TODOCHAT: openChat: opened ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
} }
} }
fun openLoadedChat(chat: Chat, chatModel: ChatModel) { fun openLoadedChat(chat: Chat, chatModel: ChatModel) {
chatModel.chatItems.clear()
chatModel.chatItemStatuses.clear() chatModel.chatItemStatuses.clear()
chatModel.chatItems.addAll(chat.chatItems) chatModel.chatItems.replaceAll(chat.chatItems)
chatModel.chatId.value = chat.chatInfo.id chatModel.chatId.value = chat.chatInfo.id
} }
@ -239,8 +236,7 @@ suspend fun apiFindMessages(ch: Chat, chatModel: ChatModel, search: String) {
val chatInfo = ch.chatInfo val chatInfo = ch.chatInfo
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return
if (chatModel.chatId.value != chat.id) return if (chatModel.chatId.value != chat.id) return
chatModel.chatItems.clear() chatModel.chatItems.replaceAll(chat.chatItems)
chatModel.chatItems.addAll(0, chat.chatItems)
} }
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {

View File

@ -62,7 +62,7 @@ fun DatabaseEncryptionView(m: ChatModel) {
initialRandomDBPassphrase, initialRandomDBPassphrase,
progressIndicator, progressIndicator,
onConfirmEncrypt = { onConfirmEncrypt = {
withLongRunningApi(slow = 30_000, deadlock = 60_000) { withLongRunningApi {
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator) encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator)
} }
} }

View File

@ -368,7 +368,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
} }
fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>, progressIndicator: MutableState<Boolean>? = null) { fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>, progressIndicator: MutableState<Boolean>? = null) {
withLongRunningApi(slow = 30_000, deadlock = 60_000) { withLongRunningApi {
try { try {
progressIndicator?.value = true progressIndicator?.value = true
if (chatDbChanged.value) { if (chatDbChanged.value) {
@ -581,7 +581,7 @@ private fun importArchive(
progressIndicator.value = true progressIndicator.value = true
val archivePath = saveArchiveFromURI(importedArchiveURI) val archivePath = saveArchiveFromURI(importedArchiveURI)
if (archivePath != null) { if (archivePath != null) {
withLongRunningApi(slow = 60_000, deadlock = 180_000) { withLongRunningApi {
try { try {
m.controller.apiDeleteStorage() m.controller.apiDeleteStorage()
try { try {

View File

@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.* import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -22,6 +23,8 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
class AlertManager { class AlertManager {
@ -128,6 +131,8 @@ class AlertManager {
) { ) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
delay(200)
focusRequester.requestFocus() focusRequester.requestFocus()
} }
TextButton(onClick = { TextButton(onClick = {
@ -186,6 +191,7 @@ class AlertManager {
title: String, text: String? = null, title: String, text: String? = null,
confirmText: String = generalGetString(MR.strings.ok), confirmText: String = generalGetString(MR.strings.ok),
hostDevice: Pair<Long?, String>? = null, hostDevice: Pair<Long?, String>? = null,
shareText: Boolean? = null
) { ) {
showAlert { showAlert {
AlertDialog( AlertDialog(
@ -195,12 +201,23 @@ class AlertManager {
AlertContent(text, hostDevice, extraPadding = true) { AlertContent(text, hostDevice, extraPadding = true) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
delay(200)
focusRequester.requestFocus() focusRequester.requestFocus()
} }
// Can pass shareText = false to prevent showing Share button if it's needed in a specific case
val showShareButton = text != null && (shareText == true || (shareText == null && text.length > 500))
Row( Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.Center horizontalArrangement = if (showShareButton) Arrangement.SpaceBetween else Arrangement.Center
) { ) {
val clipboard = LocalClipboardManager.current
if (showShareButton && text != null) {
TextButton(onClick = {
clipboard.shareText(text)
hideAlert()
}) { Text(stringResource(MR.strings.share_verb)) }
}
TextButton( TextButton(
onClick = { onClick = {
hideAlert() hideAlert()

View File

@ -5,6 +5,7 @@ import androidx.compose.material.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.ui.theme.DEFAULT_PADDING
@ -20,7 +21,7 @@ fun DefaultProgressView(description: String?) {
strokeWidth = 2.5.dp strokeWidth = 2.5.dp
) )
if (description != null) { if (description != null) {
Text(description) Text(description, textAlign = TextAlign.Center)
} }
} }
} }

View File

@ -61,10 +61,10 @@ class ModalManager(private val placement: ModalPlacement? = null) {
} }
} }
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable ModalData.(close: () -> Unit) -> Unit) { fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
val data = ModalData() val data = ModalData()
showCustomModal { close -> showCustomModal { close ->
ModalView(close, showClose = showClose, content = { data.content(close) }) ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content(close) })
} }
} }

View File

@ -16,7 +16,7 @@ class ProcessedErrors <T: AgentErrorType>(val interval: Long) {
fun newError(error: T, offerRestart: Boolean) { fun newError(error: T, offerRestart: Boolean) {
timer.cancel() timer.cancel()
timer = withLongRunningApi(slow = 70_000, deadlock = 130_000) { timer = withLongRunningApi(slow = 130_000) {
val delayBeforeNext = (lastShownTimestamp + interval) - System.currentTimeMillis() val delayBeforeNext = (lastShownTimestamp + interval) - System.currentTimeMillis()
if ((lastShownOfferRestart || !offerRestart) && delayBeforeNext >= 0) { if ((lastShownOfferRestart || !offerRestart) && delayBeforeNext >= 0) {
delay(delayBeforeNext) delay(delayBeforeNext)

View File

@ -198,16 +198,16 @@ fun <T> SectionItemWithValue(
} }
@Composable @Composable
fun SectionTextFooter(text: String) { fun SectionTextFooter(text: String, color: Color = MaterialTheme.colors.secondary) {
SectionTextFooter(AnnotatedString(text)) SectionTextFooter(AnnotatedString(text), color = color)
} }
@Composable @Composable
fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start) { fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start, color: Color = MaterialTheme.colors.secondary) {
Text( Text(
text, text,
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F), Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
color = MaterialTheme.colors.secondary, color = color,
lineHeight = 18.sp, lineHeight = 18.sp,
fontSize = 14.sp, fontSize = 14.sp,
textAlign = textAlign textAlign = textAlign

View File

@ -37,30 +37,22 @@ fun withBGApi(action: suspend CoroutineScope.() -> Unit): Job =
CoroutineScope(singleThreadDispatcher).launch(block = { wrapWithLogging(action, it) }) CoroutineScope(singleThreadDispatcher).launch(block = { wrapWithLogging(action, it) })
} }
fun withLongRunningApi(slow: Long = Long.MAX_VALUE, deadlock: Long = Long.MAX_VALUE, action: suspend CoroutineScope.() -> Unit): Job = fun withLongRunningApi(slow: Long = Long.MAX_VALUE, action: suspend CoroutineScope.() -> Unit): Job =
Exception().let { Exception().let {
CoroutineScope(Dispatchers.Default).launch(block = { wrapWithLogging(action, it, slow = slow, deadlock = deadlock) }) CoroutineScope(Dispatchers.Default).launch(block = { wrapWithLogging(action, it, slow = slow) })
} }
private suspend fun wrapWithLogging(action: suspend CoroutineScope.() -> Unit, exception: java.lang.Exception, slow: Long = 10_000, deadlock: Long = 60_000) = coroutineScope { private suspend fun wrapWithLogging(action: suspend CoroutineScope.() -> Unit, exception: java.lang.Exception, slow: Long = 20_000) = coroutineScope {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val job = launch {
delay(deadlock)
Log.e(TAG, "Possible deadlock of the thread, not finished after ${deadlock / 1000}s:\n${exception.stackTraceToString()}")
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_deadlock_title),
text = generalGetString(MR.strings.possible_deadlock_desc).format(deadlock / 1000, exception.stackTraceToString()),
)
}
action() action()
job.cancel() val end = System.currentTimeMillis()
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) { if (end - start > slow) {
val end = System.currentTimeMillis() Log.e(TAG, "Possible problem with execution of the thread, took ${(end - start) / 1000}s:\n${exception.stackTraceToString()}")
if (end - start > slow) { if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
Log.e(TAG, "Possible problem with execution of the thread, took ${(end - start) / 1000}s:\n${exception.stackTraceToString()}")
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_slow_function_title), title = generalGetString(MR.strings.possible_slow_function_title),
text = generalGetString(MR.strings.possible_slow_function_desc).format((end - start) / 1000, exception.stackTraceToString()), text = generalGetString(MR.strings.possible_slow_function_desc).format((end - start) / 1000, exception.stackTraceToString()),
shareText = true
) )
} }
} }

View File

@ -49,7 +49,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
} }
private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) { private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) {
withLongRunningApi(slow = 30_000, deadlock = 60_000) { withLongRunningApi {
try { try {
/** Waiting until [initChatController] finishes */ /** Waiting until [initChatController] finishes */
while (m.ctrlInitInProgress.value) { while (m.ctrlInitInProgress.value) {

View File

@ -50,7 +50,7 @@ fun SetupDatabasePassphrase(m: ChatModel) {
confirmNewKey, confirmNewKey,
progressIndicator, progressIndicator,
onConfirmEncrypt = { onConfirmEncrypt = {
withLongRunningApi(slow = 30_000, deadlock = 60_000) { withLongRunningApi {
if (m.chatRunning.value == true) { if (m.chatRunning.value == true) {
// Stop chat if it's started before doing anything // Stop chat if it's started before doing anything
stopChatAsync(m) stopChatAsync(m)

View File

@ -47,10 +47,6 @@ fun NetworkAndServersView(
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) } val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) } val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
LaunchedEffect(Unit) {
chatModel.userSMPServersUnsaved.value = null
}
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } } val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
NetworkAndServersLayout( NetworkAndServersLayout(
currentRemoteHost = currentRemoteHost, currentRemoteHost = currentRemoteHost,

View File

@ -96,7 +96,7 @@ fun PrivacySettingsView(
val currentUser = chatModel.currentUser.value val currentUser = chatModel.currentUser.value
if (currentUser != null) { if (currentUser != null) {
fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) { fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) {
withLongRunningApi(slow = 30_000, deadlock = 60_000) { withLongRunningApi(slow = 60_000) {
val mrs = UserMsgReceiptSettings(enable, clearOverrides) val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserContactReceipts(currentUser, mrs) chatModel.controller.apiSetUserContactReceipts(currentUser, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
@ -119,7 +119,7 @@ fun PrivacySettingsView(
} }
fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) { fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) {
withLongRunningApi(slow = 30_000, deadlock = 60_000) { withLongRunningApi(slow = 60_000) {
val mrs = UserMsgReceiptSettings(enable, clearOverrides) val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserGroupReceipts(currentUser, mrs) chatModel.controller.apiSetUserGroupReceipts(currentUser, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)

View File

@ -28,19 +28,18 @@ import chat.simplex.res.MR
@Composable @Composable
fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) { fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) {
var presetServers by remember(rhId) { mutableStateOf(emptyList<String>()) } var presetServers by remember(rhId) { mutableStateOf(emptyList<String>()) }
var servers by remember(rhId) { var servers by remember { stateGetOrPut("servers") { emptyList<ServerCfg>() } }
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList()) var serversAlreadyLoaded by remember { stateGetOrPut("serversAlreadyLoaded") { false } }
}
val currServers = remember(rhId) { mutableStateOf(servers) } val currServers = remember(rhId) { mutableStateOf(servers) }
val testing = rememberSaveable(rhId) { mutableStateOf(false) } val testing = rememberSaveable(rhId) { mutableStateOf(false) }
val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } } val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } } val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } }
val saveDisabled = remember { val saveDisabled = remember(servers) {
derivedStateOf { derivedStateOf {
servers.isEmpty() || servers.isEmpty() ||
servers == currServers.value || servers == currServers.value ||
testing.value || testing.value ||
!servers.all { srv -> servers.none { srv ->
val address = parseServerAddress(srv.server) val address = parseServerAddress(srv.server)
address != null && uniqueAddress(srv, address, servers) address != null && uniqueAddress(srv, address, servers)
} || } ||
@ -49,8 +48,8 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
} }
KeyChangeEffect(rhId) { KeyChangeEffect(rhId) {
m.userSMPServersUnsaved.value = null
servers = emptyList() servers = emptyList()
serversAlreadyLoaded = false
} }
LaunchedEffect(rhId) { LaunchedEffect(rhId) {
@ -59,8 +58,9 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
if (res != null) { if (res != null) {
currServers.value = res.protoServers currServers.value = res.protoServers
presetServers = res.presetServers presetServers = res.presetServers
if (servers.isEmpty()) { if (servers.isEmpty() && !serversAlreadyLoaded) {
servers = currServers.value servers = currServers.value
serversAlreadyLoaded = true
} }
} }
} }
@ -80,13 +80,11 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
newServers.add(index, updated) newServers.add(index, updated)
old = updated old = updated
servers = newServers servers = newServers
m.userSMPServersUnsaved.value = servers
}, },
onDelete = { onDelete = {
val newServers = ArrayList(servers) val newServers = ArrayList(servers)
newServers.removeAt(index) newServers.removeAt(index)
servers = newServers servers = newServers
m.userSMPServersUnsaved.value = servers
close() close()
}) })
} }
@ -125,7 +123,6 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
ScanProtocolServer(rhId) { ScanProtocolServer(rhId) {
close() close()
servers = servers + it servers = servers + it
m.userSMPServersUnsaved.value = servers
} }
} }
} }
@ -150,13 +147,11 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
testServersJob.value = withLongRunningApi { testServersJob.value = withLongRunningApi {
testServers(testing, servers, m) { testServers(testing, servers, m) {
servers = it servers = it
m.userSMPServersUnsaved.value = servers
} }
} }
}, },
resetServers = { resetServers = {
servers = currServers.value ?: emptyList() servers = currServers.value
m.userSMPServersUnsaved.value = null
}, },
saveSMPServers = { saveSMPServers = {
saveServers(rhId, serverProtocol, currServers, servers, m) saveServers(rhId, serverProtocol, currServers, servers, m)
@ -355,7 +350,6 @@ private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: Muta
withBGApi { withBGApi {
if (m.controller.setUserProtoServers(rhId, protocol, servers)) { if (m.controller.setUserProtoServers(rhId, protocol, servers)) {
currServers.value = servers currServers.value = servers
m.userSMPServersUnsaved.value = null
} }
afterSave() afterSave()
} }

View File

@ -1588,8 +1588,6 @@
<string name="remote_ctrl_error_busy">سطح المكتب مشغول</string> <string name="remote_ctrl_error_busy">سطح المكتب مشغول</string>
<string name="remote_ctrl_error_bad_version">يحتوي سطح المكتب على إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين</string> <string name="remote_ctrl_error_bad_version">يحتوي سطح المكتب على إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين</string>
<string name="past_member_vName">العضو السابق %1$s</string> <string name="past_member_vName">العضو السابق %1$s</string>
<string name="possible_deadlock_title">مأزق</string>
<string name="possible_deadlock_desc">يستغرق تنفيذ التعليمات البرمجية وقتًا طويلاً جدًا: %1$d ثانية. من المحتمل أن التطبيق مجمّد: %2$s</string>
<string name="possible_slow_function_title">وظيفة بطيئة</string> <string name="possible_slow_function_title">وظيفة بطيئة</string>
<string name="developer_options_section">خيارات المطور</string> <string name="developer_options_section">خيارات المطور</string>
<string name="profile_update_event_member_name_changed">تغيّر العضو %1$s إلى %2$s</string> <string name="profile_update_event_member_name_changed">تغيّر العضو %1$s إلى %2$s</string>

View File

@ -16,6 +16,7 @@
<!-- MainActivity.kt --> <!-- MainActivity.kt -->
<string name="opening_database">Opening database…</string> <string name="opening_database">Opening database…</string>
<string name="database_migration_in_progress">Database migration is in progress.\nIt may take a few minutes.</string>
<string name="non_content_uri_alert_title">Invalid file path</string> <string name="non_content_uri_alert_title">Invalid file path</string>
<string name="non_content_uri_alert_text">You shared an invalid file path. Report the issue to the app developers.</string> <string name="non_content_uri_alert_text">You shared an invalid file path. Report the issue to the app developers.</string>
<string name="app_was_crashed">View crashed</string> <string name="app_was_crashed">View crashed</string>
@ -146,8 +147,6 @@
<string name="smp_server_test_delete_file">Delete file</string> <string name="smp_server_test_delete_file">Delete file</string>
<string name="error_deleting_user">Error deleting user profile</string> <string name="error_deleting_user">Error deleting user profile</string>
<string name="error_updating_user_privacy">Error updating user privacy</string> <string name="error_updating_user_privacy">Error updating user privacy</string>
<string name="possible_deadlock_title">Deadlock</string>
<string name="possible_deadlock_desc">Execution of code takes too long time: %1$d seconds. Probably, the app is frozen: %2$s</string>
<string name="possible_slow_function_title">Slow function</string> <string name="possible_slow_function_title">Slow function</string>
<string name="possible_slow_function_desc">Execution of function takes too long time: %1$d seconds: %2$s</string> <string name="possible_slow_function_desc">Execution of function takes too long time: %1$d seconds: %2$s</string>
@ -1377,9 +1376,11 @@
<!-- GroupWelcomeView.kt --> <!-- GroupWelcomeView.kt -->
<string name="group_welcome_title">Welcome message</string> <string name="group_welcome_title">Welcome message</string>
<string name="save_welcome_message_question">Save welcome message?</string> <string name="save_welcome_message_question">Save welcome message?</string>
<string name="welcome_message_is_too_long">Welcome message is too long</string>
<string name="save_and_update_group_profile">Save and update group profile</string> <string name="save_and_update_group_profile">Save and update group profile</string>
<string name="group_welcome_preview">Preview</string> <string name="group_welcome_preview">Preview</string>
<string name="enter_welcome_message">Enter welcome message…</string> <string name="enter_welcome_message">Enter welcome message…</string>
<string name="message_too_large">Message too large</string>
<!-- ConnectionStats --> <!-- ConnectionStats -->
<string name="conn_stats_section_title_servers">SERVERS</string> <string name="conn_stats_section_title_servers">SERVERS</string>

View File

@ -1555,7 +1555,6 @@
<string name="chat_is_stopped_you_should_transfer_database">Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново.</string> <string name="chat_is_stopped_you_should_transfer_database">Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново.</string>
<string name="remote_ctrl_error_bad_invitation">Настолното устройство има грешен код за връзка</string> <string name="remote_ctrl_error_bad_invitation">Настолното устройство има грешен код за връзка</string>
<string name="remote_ctrl_error_bad_version">Настолното устройство е с неподдържана версия. Моля, уверете се, че използвате една и съща версия и на двете устройства</string> <string name="remote_ctrl_error_bad_version">Настолното устройство е с неподдържана версия. Моля, уверете се, че използвате една и съща версия и на двете устройства</string>
<string name="possible_deadlock_desc">Изпълнението на кода отнема твърде много време: %1$d секунди. Вероятно приложението е замразено: %2$s</string>
<string name="possible_slow_function_title">Бавна функция</string> <string name="possible_slow_function_title">Бавна функция</string>
<string name="possible_slow_function_desc">Изпълнението на функцията отнема твърде много време: %1$d секунди: %2$s</string> <string name="possible_slow_function_desc">Изпълнението на функцията отнема твърде много време: %1$d секунди: %2$s</string>
<string name="show_internal_errors">Покажи вътрешните грешки</string> <string name="show_internal_errors">Покажи вътрешните грешки</string>
@ -1591,5 +1590,4 @@
\nПрепоръчително е да рестартирате приложението.</string> \nПрепоръчително е да рестартирате приложението.</string>
<string name="developer_options_section">Опции за разработчици</string> <string name="developer_options_section">Опции за разработчици</string>
<string name="show_slow_api_calls">Показване на бавни API заявки</string> <string name="show_slow_api_calls">Показване на бавни API заявки</string>
<string name="possible_deadlock_title">Грешка в заключено положение</string>
</resources> </resources>

View File

@ -1672,9 +1672,7 @@
<string name="possible_slow_function_title">Langsame Funktion</string> <string name="possible_slow_function_title">Langsame Funktion</string>
<string name="show_slow_api_calls">Zeige langsame API-Aufrufe an</string> <string name="show_slow_api_calls">Zeige langsame API-Aufrufe an</string>
<string name="group_member_status_unknown_short">unbekannt</string> <string name="group_member_status_unknown_short">unbekannt</string>
<string name="possible_deadlock_title">Blockade</string>
<string name="developer_options_section">Optionen für Entwickler</string> <string name="developer_options_section">Optionen für Entwickler</string>
<string name="possible_deadlock_desc">Die Code-Ausführung dauert zu lange: %1$d Sekunden. Wahrscheinlich ist die App eingefroren: %2$s</string>
<string name="group_member_status_unknown">unbekannter Gruppenmitglieds-Status</string> <string name="group_member_status_unknown">unbekannter Gruppenmitglieds-Status</string>
<string name="v5_5_private_notes_descr">Mit verschlüsselten Dateien und Medien.</string> <string name="v5_5_private_notes_descr">Mit verschlüsselten Dateien und Medien.</string>
<string name="v5_5_private_notes">Private Notizen</string> <string name="v5_5_private_notes">Private Notizen</string>

View File

@ -1559,11 +1559,9 @@
<string name="remote_host_error_bad_state"><![CDATA[État médiocre de la connexion au mobile <b>%s</b>.]]></string> <string name="remote_host_error_bad_state"><![CDATA[État médiocre de la connexion au mobile <b>%s</b>.]]></string>
<string name="remote_ctrl_was_disconnected_title">Connexion interrompue</string> <string name="remote_ctrl_was_disconnected_title">Connexion interrompue</string>
<string name="remote_ctrl_error_bad_state">État médiocre de la connexion avec le bureau</string> <string name="remote_ctrl_error_bad_state">État médiocre de la connexion avec le bureau</string>
<string name="possible_deadlock_title">Impasse</string>
<string name="remote_ctrl_error_bad_version">La version de l\'ordinateur de bureau n\'est pas prise en charge. Veillez à utiliser la même version sur les deux appareils.</string> <string name="remote_ctrl_error_bad_version">La version de l\'ordinateur de bureau n\'est pas prise en charge. Veillez à utiliser la même version sur les deux appareils.</string>
<string name="remote_ctrl_error_disconnected">Le bureau a été déconnecté</string> <string name="remote_ctrl_error_disconnected">Le bureau a été déconnecté</string>
<string name="developer_options_section">Options pour les développeurs</string> <string name="developer_options_section">Options pour les développeurs</string>
<string name="possible_deadlock_desc">Le code prend trop de temps à s\'exécuter: %1$d secondes. Il est probable que l\'application soit figée: %2$s</string>
<string name="agent_internal_error_title">Erreur interne</string> <string name="agent_internal_error_title">Erreur interne</string>
<string name="remote_host_error_bad_version"><![CDATA[La version du mobile <b>%s</b> n\'est pas prise en charge. Veillez à utiliser la même version sur les deux appareils.]]></string> <string name="remote_host_error_bad_version"><![CDATA[La version du mobile <b>%s</b> n\'est pas prise en charge. Veillez à utiliser la même version sur les deux appareils.]]></string>
<string name="show_internal_errors">Afficher les erreurs internes</string> <string name="show_internal_errors">Afficher les erreurs internes</string>

View File

@ -1583,9 +1583,7 @@
<string name="possible_slow_function_title">Lassú funkció</string> <string name="possible_slow_function_title">Lassú funkció</string>
<string name="show_slow_api_calls">Lassú API-hívások megjelenítése</string> <string name="show_slow_api_calls">Lassú API-hívások megjelenítése</string>
<string name="remote_host_error_inactive"><![CDATA[A(z) <b>%s</b> mobil eszköz inaktív]]></string> <string name="remote_host_error_inactive"><![CDATA[A(z) <b>%s</b> mobil eszköz inaktív]]></string>
<string name="possible_deadlock_title">Elakadt</string>
<string name="developer_options_section">Fejlesztői beállítások</string> <string name="developer_options_section">Fejlesztői beállítások</string>
<string name="possible_deadlock_desc">A kód végrehajtása túl sokáig tart: %1$d másodperc. Valószínűleg az alkalmazás lefagyott: %2$s</string>
<string name="possible_slow_function_desc">A funkció végrehajtása túl sokáig tart: %1$d másodperc: %2$s</string> <string name="possible_slow_function_desc">A funkció végrehajtása túl sokáig tart: %1$d másodperc: %2$s</string>
<string name="remote_host_error_busy"><![CDATA[A(z) <b>%s</b> mobil eszköz elfoglalt]]></string> <string name="remote_host_error_busy"><![CDATA[A(z) <b>%s</b> mobil eszköz elfoglalt]]></string>
<string name="past_member_vName">Legutóbbi tag %1$s</string> <string name="past_member_vName">Legutóbbi tag %1$s</string>

View File

@ -1591,9 +1591,7 @@
<string name="possible_slow_function_title">Funzione lenta</string> <string name="possible_slow_function_title">Funzione lenta</string>
<string name="show_slow_api_calls">Mostra chiamate API lente</string> <string name="show_slow_api_calls">Mostra chiamate API lente</string>
<string name="group_member_status_unknown_short">sconosciuto</string> <string name="group_member_status_unknown_short">sconosciuto</string>
<string name="possible_deadlock_desc">L\'esecuzione del codice impiega troppo tempo: %1$d secondi. Probabilmente l\'app è congelata: %2$s</string>
<string name="group_member_status_unknown">stato sconosciuto</string> <string name="group_member_status_unknown">stato sconosciuto</string>
<string name="possible_deadlock_title">Stallo</string>
<string name="developer_options_section">Opzioni sviluppatore</string> <string name="developer_options_section">Opzioni sviluppatore</string>
<string name="v5_5_private_notes">Note private</string> <string name="v5_5_private_notes">Note private</string>
<string name="v5_5_new_interface_languages">Interfaccia in ungherese e turco</string> <string name="v5_5_new_interface_languages">Interfaccia in ungherese e turco</string>

View File

@ -1571,9 +1571,7 @@
<string name="remote_ctrl_error_busy">PC版が処理中</string> <string name="remote_ctrl_error_busy">PC版が処理中</string>
<string name="remote_ctrl_error_disconnected">PC版が切断されました</string> <string name="remote_ctrl_error_disconnected">PC版が切断されました</string>
<string name="remote_ctrl_error_bad_version">ご利用のPC版のバージョンがサポートされてません。両端末が同じバージョンかどうか、ご確認ください。</string> <string name="remote_ctrl_error_bad_version">ご利用のPC版のバージョンがサポートされてません。両端末が同じバージョンかどうか、ご確認ください。</string>
<string name="possible_deadlock_title">デッドロック状態</string>
<string name="developer_options_section">開発者向けの設定</string> <string name="developer_options_section">開発者向けの設定</string>
<string name="possible_deadlock_desc">処理時間が異常にかかるようです: %1$d 秒。アプリが固まった恐れがあります: %2$s</string>
<string name="remote_host_error_busy"><![CDATA[携帯版 <b>%s</b> がただいま処理中]]></string> <string name="remote_host_error_busy"><![CDATA[携帯版 <b>%s</b> がただいま処理中]]></string>
<string name="possible_slow_function_desc">機能の処理時間が以上にかかってます: %1$d 秒: %2$s</string> <string name="possible_slow_function_desc">機能の処理時間が以上にかかってます: %1$d 秒: %2$s</string>
<string name="show_internal_errors">内部エラーを表示</string> <string name="show_internal_errors">内部エラーを表示</string>

View File

@ -1574,7 +1574,6 @@
<string name="remote_host_error_missing"><![CDATA[Mobiel <b>%s</b> ontbreekt]]></string> <string name="remote_host_error_missing"><![CDATA[Mobiel <b>%s</b> ontbreekt]]></string>
<string name="remote_host_error_bad_state"><![CDATA[De verbinding met de mobiel <b>%s</b> is in slechte staat]]></string> <string name="remote_host_error_bad_state"><![CDATA[De verbinding met de mobiel <b>%s</b> is in slechte staat]]></string>
<string name="remote_ctrl_error_disconnected">De verbinding met desktop is verbroken</string> <string name="remote_ctrl_error_disconnected">De verbinding met desktop is verbroken</string>
<string name="possible_deadlock_title">Impasse</string>
<string name="possible_slow_function_desc">Uitvoering van functie duurt te lang: %1$d seconden: %2$s</string> <string name="possible_slow_function_desc">Uitvoering van functie duurt te lang: %1$d seconden: %2$s</string>
<string name="possible_slow_function_title">Langzame functie</string> <string name="possible_slow_function_title">Langzame functie</string>
<string name="developer_options_section">Ontwikkelaars opties</string> <string name="developer_options_section">Ontwikkelaars opties</string>
@ -1588,7 +1587,6 @@
<string name="restart_chat_button">Chat opnieuw starten</string> <string name="restart_chat_button">Chat opnieuw starten</string>
<string name="remote_host_error_timeout"><![CDATA[Time-out bereikt tijdens het verbinden met de mobiel <b>%s</b>]]></string> <string name="remote_host_error_timeout"><![CDATA[Time-out bereikt tijdens het verbinden met de mobiel <b>%s</b>]]></string>
<string name="remote_ctrl_error_bad_state">De verbinding met de desktop is in slechte staat</string> <string name="remote_ctrl_error_bad_state">De verbinding met de desktop is in slechte staat</string>
<string name="possible_deadlock_desc">Het uitvoeren van de code duurt te lang: %1$d seconden. Waarschijnlijk is de app vastgelopen: %2$s</string>
<string name="remote_ctrl_error_bad_invitation">Desktop heeft verkeerde uitnodigingscode</string> <string name="remote_ctrl_error_bad_invitation">Desktop heeft verkeerde uitnodigingscode</string>
<string name="remote_host_error_bad_version"><![CDATA[Mobiel <b>%s</b> heeft een niet-ondersteunde versie. Zorg ervoor dat u op beide apparaten dezelfde versie gebruikt]]></string> <string name="remote_host_error_bad_version"><![CDATA[Mobiel <b>%s</b> heeft een niet-ondersteunde versie. Zorg ervoor dat u op beide apparaten dezelfde versie gebruikt]]></string>
<string name="remote_ctrl_error_timeout">Time-out bereikt tijdens het verbinden met de desktop</string> <string name="remote_ctrl_error_timeout">Time-out bereikt tijdens het verbinden met de desktop</string>

View File

@ -1606,7 +1606,6 @@
<string name="remote_ctrl_error_bad_version">Komputer ma niewspieraną wersję. Proszę upewnić się, że używasz tych samych wersji na obu urządzeniach</string> <string name="remote_ctrl_error_bad_version">Komputer ma niewspieraną wersję. Proszę upewnić się, że używasz tych samych wersji na obu urządzeniach</string>
<string name="blocked_by_admin_items_description">%d wiadomości zablokowanych przez admina</string> <string name="blocked_by_admin_items_description">%d wiadomości zablokowanych przez admina</string>
<string name="error_creating_message">Błąd tworzenia wiadomości</string> <string name="error_creating_message">Błąd tworzenia wiadomości</string>
<string name="possible_deadlock_desc">Wykonanie kodu zajmuje za dużo czasu: %1$d sekund. Prawdopodobnie aplikacja jest zamrożona: %2$s</string>
<string name="possible_slow_function_desc">Wykonanie kodu zajmuje za dużo czasu: %1$d sekund: %2$s</string> <string name="possible_slow_function_desc">Wykonanie kodu zajmuje za dużo czasu: %1$d sekund: %2$s</string>
<string name="note_folder_local_display_name">Prywatne notatki</string> <string name="note_folder_local_display_name">Prywatne notatki</string>
<string name="group_member_status_unknown">nieznany status</string> <string name="group_member_status_unknown">nieznany status</string>
@ -1621,7 +1620,6 @@
<string name="remote_host_error_inactive"><![CDATA[Telefon <b>%s</b> jest nieaktywny]]></string> <string name="remote_host_error_inactive"><![CDATA[Telefon <b>%s</b> jest nieaktywny]]></string>
<string name="remote_host_error_bad_version"><![CDATA[Telefon <b>%s</b> ma niewspieraną wersję. Proszę, upewnij się, że używasz tej samej wersji na obydwu urządzeniach]]></string> <string name="remote_host_error_bad_version"><![CDATA[Telefon <b>%s</b> ma niewspieraną wersję. Proszę, upewnij się, że używasz tej samej wersji na obydwu urządzeniach]]></string>
<string name="group_member_status_unknown_short">nieznany</string> <string name="group_member_status_unknown_short">nieznany</string>
<string name="possible_deadlock_title">Blokada</string>
<string name="profile_update_event_contact_name_changed">kontakt %1$s zmieniony na %2$s</string> <string name="profile_update_event_contact_name_changed">kontakt %1$s zmieniony na %2$s</string>
<string name="profile_update_event_removed_address">usunięto adres kontaktu</string> <string name="profile_update_event_removed_address">usunięto adres kontaktu</string>
<string name="profile_update_event_removed_picture">usunięto zdjęcie profilu</string> <string name="profile_update_event_removed_picture">usunięto zdjęcie profilu</string>

View File

@ -1680,8 +1680,6 @@
<string name="error_showing_message">ошибка отображения сообщения</string> <string name="error_showing_message">ошибка отображения сообщения</string>
<string name="error_showing_content">ошибка отображения содержания</string> <string name="error_showing_content">ошибка отображения содержания</string>
<string name="remote_ctrl_disconnected_with_reason">Отсоединён по причине: %s</string> <string name="remote_ctrl_disconnected_with_reason">Отсоединён по причине: %s</string>
<string name="possible_deadlock_title">Взаимная блокировка</string>
<string name="possible_deadlock_desc">Выполнение задачи занимает долгое время: %1$d секунд. Возможно, приложение заблокировано: %2$s</string>
<string name="possible_slow_function_desc">Выполнение задачи занимает долгое время: %1$d секунд: %2$s</string> <string name="possible_slow_function_desc">Выполнение задачи занимает долгое время: %1$d секунд: %2$s</string>
<string name="possible_slow_function_title">Медленный вызов</string> <string name="possible_slow_function_title">Медленный вызов</string>
<string name="profile_update_event_contact_name_changed">контакт %1$s изменён на %2$s</string> <string name="profile_update_event_contact_name_changed">контакт %1$s изменён на %2$s</string>

View File

@ -1586,8 +1586,6 @@
<string name="remote_host_error_bad_state"><![CDATA[到移动主机 <b>%s</b>的连接状态不佳]]></string> <string name="remote_host_error_bad_state"><![CDATA[到移动主机 <b>%s</b>的连接状态不佳]]></string>
<string name="remote_host_error_timeout"><![CDATA[连接到移动主机<b>%s</b>时超时]]></string> <string name="remote_host_error_timeout"><![CDATA[连接到移动主机<b>%s</b>时超时]]></string>
<string name="failed_to_create_user_invalid_desc">显示名无效。请另选一个名称。</string> <string name="failed_to_create_user_invalid_desc">显示名无效。请另选一个名称。</string>
<string name="possible_deadlock_title">死锁</string>
<string name="possible_deadlock_desc">代码执行花费的时间过久:%1$d秒。应用可能卡住了%2$s</string>
<string name="possible_slow_function_title">慢函数</string> <string name="possible_slow_function_title">慢函数</string>
<string name="show_slow_api_calls">显示缓慢的 API 调用</string> <string name="show_slow_api_calls">显示缓慢的 API 调用</string>
<string name="past_member_vName">过往成员 %1$s</string> <string name="past_member_vName">过往成员 %1$s</string>

View File

@ -14,8 +14,7 @@ import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.* import androidx.compose.ui.window.*
import chat.simplex.common.model.ChatController import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.ui.theme.SimpleXTheme
@ -40,7 +39,8 @@ fun showApp() {
WindowExceptionHandler { e -> WindowExceptionHandler { e ->
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.app_was_crashed), title = generalGetString(MR.strings.app_was_crashed),
text = e.stackTraceToString() text = e.stackTraceToString(),
shareText = true
) )
Log.e(TAG, "App crashed, thread name: " + Thread.currentThread().name + ", exception: " + e.stackTraceToString()) Log.e(TAG, "App crashed, thread name: " + Thread.currentThread().name + ", exception: " + e.stackTraceToString())
window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING)) window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))

View File

@ -42,7 +42,7 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL
} }
var fileSource = getLoadedFileSource(cItem.file) var fileSource = getLoadedFileSource(cItem.file)
if (chatModel.connectedToRemote() && fileSource == null) { if (chatModel.connectedToRemote() && fileSource == null) {
withLongRunningApi(slow = 60_000, deadlock = 600_000) { withLongRunningApi(slow = 600_000) {
cItem.file?.loadRemoteFile(true) cItem.file?.loadRemoteFile(true)
fileSource = getLoadedFileSource(cItem.file) fileSource = getLoadedFileSource(cItem.file)
saveIfExists() saveIfExists()
@ -51,7 +51,7 @@ actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserL
}) })
} }
actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) = withLongRunningApi(slow = 60_000, deadlock = 600_000) { actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) = withLongRunningApi(slow = 600_000) {
var fileSource = getLoadedFileSource(cItem.file) var fileSource = getLoadedFileSource(cItem.file)
if (chatModel.connectedToRemote() && fileSource == null) { if (chatModel.connectedToRemote() && fileSource == null) {
cItem.file?.loadRemoteFile(true) cItem.file?.loadRemoteFile(true)

View File

@ -25,11 +25,11 @@ android.nonTransitiveRClass=true
android.enableJetifier=true android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.androidSourceSetLayoutVersion=2
android.version_name=5.5 android.version_name=5.5.3
android.version_code=175 android.version_code=181
desktop.version_name=5.5 desktop.version_name=5.5.3
desktop.version_code=26 desktop.version_code=29
kotlin.version=1.8.20 kotlin.version=1.8.20
gradle.plugin.version=7.4.2 gradle.plugin.version=7.4.2

View File

@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: 7a0cd8041bbb7d7ab2f089395a244dc4af0f9e3b tag: e64b6cba4b7e4107f78ae596ab2a6a28ef24ff78
source-repository-package source-repository-package
type: git type: git

View File

@ -1,13 +1,13 @@
--- ---
title: Download SimpleX apps title: Download SimpleX apps
permalink: /downloads/index.html permalink: /downloads/index.html
revision: 25.11.2023 revision: 11.02.2024
--- ---
| Updated 25.11.2023 | Languages: EN | | Updated 11.02.2024 | Languages: EN |
# Download SimpleX apps # Download SimpleX apps
The latest stable version is v5.5. The latest stable version is v5.5.3.
You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases). You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases).
@ -21,24 +21,24 @@ You can get the latest beta releases from [GitHub](https://github.com/simplex-ch
Using the same profile as on mobile device is not yet supported you need to create a separate profile to use desktop apps. Using the same profile as on mobile device is not yet supported you need to create a separate profile to use desktop apps.
**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-ubuntu-22_04-x86_64.deb). **Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-22_04-x86_64.deb).
**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-macos-aarch64.dmg) (Apple Silicon). **Mac**: [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-aarch64.dmg) (Apple Silicon), [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-x86_64.dmg) (Intel).
**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-desktop-windows-x86_64.msi). **Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-windows-x86_64.msi).
## Mobile apps ## Mobile apps
**iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084), [TestFlight](https://testflight.apple.com/join/DWuT2LQu). **iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084), [TestFlight](https://testflight.apple.com/join/DWuT2LQu).
**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-armv7a.apk). **Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-armv7a.apk).
## Terminal (console) app ## Terminal (console) app
See [Using terminal app](/docs/CLI.md). See [Using terminal app](/docs/CLI.md).
**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-chat-ubuntu-22_04-x86-64). **Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-ubuntu-22_04-x86-64).
**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-chat-macos-x86-64), aarch64 - [compile from source](/docs/CLI.md#). **Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-macos-x86-64), aarch64 - [compile from source](/docs/CLI.md#).
**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.5.0/simplex-chat-windows-x86-64). **Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-chat-windows-x86-64).

View File

@ -386,6 +386,7 @@
"chat_send_cmd" "chat_send_cmd"
"chat_send_remote_cmd" "chat_send_remote_cmd"
"chat_valid_name" "chat_valid_name"
"chat_json_length"
"chat_write_file" "chat_write_file"
]; ];
postInstall = '' postInstall = ''
@ -489,6 +490,7 @@
"chat_send_cmd" "chat_send_cmd"
"chat_send_remote_cmd" "chat_send_remote_cmd"
"chat_valid_name" "chat_valid_name"
"chat_json_length"
"chat_write_file" "chat_write_file"
]; ];
postInstall = '' postInstall = ''

View File

@ -12,6 +12,7 @@ EXPORTS
chat_parse_server chat_parse_server
chat_password_hash chat_password_hash
chat_valid_name chat_valid_name
chat_json_length
chat_encrypt_media chat_encrypt_media
chat_decrypt_media chat_decrypt_media
chat_write_file chat_write_file

View File

@ -1,5 +1,5 @@
name: simplex-chat name: simplex-chat
version: 5.5.0.4 version: 5.5.3.0
#synopsis: #synopsis:
#description: #description:
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
@ -36,7 +36,6 @@ dependencies:
- network >= 3.1.2.7 && < 3.2 - network >= 3.1.2.7 && < 3.2
- network-transport == 0.5.6 - network-transport == 0.5.6
- optparse-applicative >= 0.15 && < 0.17 - optparse-applicative >= 0.15 && < 0.17
- process == 1.6.*
- random >= 1.1 && < 1.3 - random >= 1.1 && < 1.3
- record-hasfield == 1.0.* - record-hasfield == 1.0.*
- simple-logger == 0.1.* - simple-logger == 0.1.*
@ -64,11 +63,13 @@ when:
- condition: impl(ghc >= 9.6.2) - condition: impl(ghc >= 9.6.2)
dependencies: dependencies:
- bytestring == 0.11.* - bytestring == 0.11.*
- process == 1.6.*
- template-haskell == 2.20.* - template-haskell == 2.20.*
- text >= 2.0.1 && < 2.2 - text >= 2.0.1 && < 2.2
- condition: impl(ghc < 9.6.2) - condition: impl(ghc < 9.6.2)
dependencies: dependencies:
- bytestring == 0.10.* - bytestring == 0.10.*
- process >= 1.6 && < 1.6.18
- template-haskell == 2.16.* - template-haskell == 2.16.*
- text >= 1.2.3.0 && < 1.3 - text >= 1.2.3.0 && < 1.3
@ -125,13 +126,19 @@ tests:
- apps/simplex-broadcast-bot/src - apps/simplex-broadcast-bot/src
- apps/simplex-directory-service/src - apps/simplex-directory-service/src
main: Test.hs main: Test.hs
when:
- condition: impl(ghc >= 9.6.2)
dependencies:
- hspec == 2.11.*
- condition: impl(ghc < 9.6.2)
dependencies:
- hspec == 2.7.*
dependencies: dependencies:
- QuickCheck == 2.14.* - QuickCheck == 2.14.*
- simplex-chat - simplex-chat
- async == 2.2.* - async == 2.2.*
- deepseq == 1.4.* - deepseq == 1.4.*
- generic-random == 1.5.* - generic-random == 1.5.*
- hspec == 2.11.*
- network == 3.1.* - network == 3.1.*
- silently == 1.2.* - silently == 1.2.*
- stm == 2.5.* - stm == 2.5.*

View File

@ -20,6 +20,10 @@ root_dir="$(dirname "$(dirname "$(readlink "$0")")")"
cd $root_dir cd $root_dir
BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-${GHC_VERSION}/simplex-chat-* BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-${GHC_VERSION}/simplex-chat-*
exports=( $(sed 's/foreign export ccall "chat_migrate_init_key"//' src/Simplex/Chat/Mobile.hs | sed 's/foreign export ccall "chat_reopen_store"//' |grep "foreign export ccall" | cut -d '"' -f2) )
for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done
for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done
rm -rf $BUILD_DIR rm -rf $BUILD_DIR
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded' cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded'
cd $BUILD_DIR/build cd $BUILD_DIR/build

View File

@ -19,6 +19,10 @@ GHC_LIBS_DIR=$(ghc --print-libdir)
BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-* BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-*
exports=( $(sed 's/foreign export ccall "chat_migrate_init_key"//' src/Simplex/Chat/Mobile.hs | sed 's/foreign export ccall "chat_reopen_store"//' |grep "foreign export ccall" | cut -d '"' -f2) )
for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done
for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done
rm -rf $BUILD_DIR rm -rf $BUILD_DIR
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi"

View File

@ -17,6 +17,10 @@ fi
BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-*/simplex-chat-* BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-*/simplex-chat-*
exports=( $(sed 's/foreign export ccall "chat_migrate_init_key"//' src/Simplex/Chat/Mobile.hs | sed 's/foreign export ccall "chat_reopen_store"//' |grep "foreign export ccall" | cut -d '"' -f2) )
for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done
for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done
# IMPORTANT: in order to get a working build you should use x86_64 MinGW with make, cmake, gcc. # IMPORTANT: in order to get a working build you should use x86_64 MinGW with make, cmake, gcc.
# 100% working MinGW is https://github.com/brechtsanders/winlibs_mingw/releases/download/13.1.0-16.0.5-11.0.0-ucrt-r5/winlibs-x86_64-posix-seh-gcc-13.1.0-mingw-w64ucrt-11.0.0-r5.zip # 100% working MinGW is https://github.com/brechtsanders/winlibs_mingw/releases/download/13.1.0-16.0.5-11.0.0-ucrt-r5/winlibs-x86_64-posix-seh-gcc-13.1.0-mingw-w64ucrt-11.0.0-r5.zip
# Many other distributions I tested don't work in some cases or don't have required tools. # Many other distributions I tested don't work in some cases or don't have required tools.

View File

@ -1,5 +1,5 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."7a0cd8041bbb7d7ab2f089395a244dc4af0f9e3b" = "0jxf9dnsg14ffd1y3i7md2ninrds4daq1fmpnd6j5z99im07ns52"; "https://github.com/simplex-chat/simplexmq.git"."e64b6cba4b7e4107f78ae596ab2a6a28ef24ff78" = "0fxgklq65bh2f4kx36vjicdxqmi88m91xs601hm81v5pn6kk0ppd";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";

View File

@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
name: simplex-chat name: simplex-chat
version: 5.5.0.4 version: 5.5.3.0
category: Web, System, Services, Cryptography category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat author: simplex.chat
@ -197,7 +197,6 @@ library
, network >=3.1.2.7 && <3.2 , network >=3.1.2.7 && <3.2
, network-transport ==0.5.6 , network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17 , optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3 , random >=1.1 && <1.3
, record-hasfield ==1.0.* , record-hasfield ==1.0.*
, simple-logger ==0.1.* , simple-logger ==0.1.*
@ -217,11 +216,13 @@ library
if impl(ghc >= 9.6.2) if impl(ghc >= 9.6.2)
build-depends: build-depends:
bytestring ==0.11.* bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.* , template-haskell ==2.20.*
, text >=2.0.1 && <2.2 , text >=2.0.1 && <2.2
if impl(ghc < 9.6.2) if impl(ghc < 9.6.2)
build-depends: build-depends:
bytestring ==0.10.* bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.* , template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3 , text >=1.2.3.0 && <1.3
@ -256,7 +257,6 @@ executable simplex-bot
, network >=3.1.2.7 && <3.2 , network >=3.1.2.7 && <3.2
, network-transport ==0.5.6 , network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17 , optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3 , random >=1.1 && <1.3
, record-hasfield ==1.0.* , record-hasfield ==1.0.*
, simple-logger ==0.1.* , simple-logger ==0.1.*
@ -277,11 +277,13 @@ executable simplex-bot
if impl(ghc >= 9.6.2) if impl(ghc >= 9.6.2)
build-depends: build-depends:
bytestring ==0.11.* bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.* , template-haskell ==2.20.*
, text >=2.0.1 && <2.2 , text >=2.0.1 && <2.2
if impl(ghc < 9.6.2) if impl(ghc < 9.6.2)
build-depends: build-depends:
bytestring ==0.10.* bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.* , template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3 , text >=1.2.3.0 && <1.3
@ -316,7 +318,6 @@ executable simplex-bot-advanced
, network >=3.1.2.7 && <3.2 , network >=3.1.2.7 && <3.2
, network-transport ==0.5.6 , network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17 , optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3 , random >=1.1 && <1.3
, record-hasfield ==1.0.* , record-hasfield ==1.0.*
, simple-logger ==0.1.* , simple-logger ==0.1.*
@ -337,11 +338,13 @@ executable simplex-bot-advanced
if impl(ghc >= 9.6.2) if impl(ghc >= 9.6.2)
build-depends: build-depends:
bytestring ==0.11.* bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.* , template-haskell ==2.20.*
, text >=2.0.1 && <2.2 , text >=2.0.1 && <2.2
if impl(ghc < 9.6.2) if impl(ghc < 9.6.2)
build-depends: build-depends:
bytestring ==0.10.* bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.* , template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3 , text >=1.2.3.0 && <1.3
@ -378,7 +381,6 @@ executable simplex-broadcast-bot
, network >=3.1.2.7 && <3.2 , network >=3.1.2.7 && <3.2
, network-transport ==0.5.6 , network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17 , optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3 , random >=1.1 && <1.3
, record-hasfield ==1.0.* , record-hasfield ==1.0.*
, simple-logger ==0.1.* , simple-logger ==0.1.*
@ -399,11 +401,13 @@ executable simplex-broadcast-bot
if impl(ghc >= 9.6.2) if impl(ghc >= 9.6.2)
build-depends: build-depends:
bytestring ==0.11.* bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.* , template-haskell ==2.20.*
, text >=2.0.1 && <2.2 , text >=2.0.1 && <2.2
if impl(ghc < 9.6.2) if impl(ghc < 9.6.2)
build-depends: build-depends:
bytestring ==0.10.* bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.* , template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3 , text >=1.2.3.0 && <1.3
@ -439,7 +443,6 @@ executable simplex-chat
, network ==3.1.* , network ==3.1.*
, network-transport ==0.5.6 , network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17 , optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3 , random >=1.1 && <1.3
, record-hasfield ==1.0.* , record-hasfield ==1.0.*
, simple-logger ==0.1.* , simple-logger ==0.1.*
@ -461,11 +464,13 @@ executable simplex-chat
if impl(ghc >= 9.6.2) if impl(ghc >= 9.6.2)
build-depends: build-depends:
bytestring ==0.11.* bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.* , template-haskell ==2.20.*
, text >=2.0.1 && <2.2 , text >=2.0.1 && <2.2
if impl(ghc < 9.6.2) if impl(ghc < 9.6.2)
build-depends: build-depends:
bytestring ==0.10.* bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.* , template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3 , text >=1.2.3.0 && <1.3
@ -505,7 +510,6 @@ executable simplex-directory-service
, network >=3.1.2.7 && <3.2 , network >=3.1.2.7 && <3.2
, network-transport ==0.5.6 , network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17 , optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3 , random >=1.1 && <1.3
, record-hasfield ==1.0.* , record-hasfield ==1.0.*
, simple-logger ==0.1.* , simple-logger ==0.1.*
@ -526,11 +530,13 @@ executable simplex-directory-service
if impl(ghc >= 9.6.2) if impl(ghc >= 9.6.2)
build-depends: build-depends:
bytestring ==0.11.* bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.* , template-haskell ==2.20.*
, text >=2.0.1 && <2.2 , text >=2.0.1 && <2.2
if impl(ghc < 9.6.2) if impl(ghc < 9.6.2)
build-depends: build-depends:
bytestring ==0.10.* bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.* , template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3 , text >=1.2.3.0 && <1.3
@ -592,7 +598,6 @@ test-suite simplex-chat-test
, exceptions ==0.10.* , exceptions ==0.10.*
, filepath ==1.4.* , filepath ==1.4.*
, generic-random ==1.5.* , generic-random ==1.5.*
, hspec ==2.11.*
, http-types ==0.12.* , http-types ==0.12.*
, http2 >=4.2.2 && <4.3 , http2 >=4.2.2 && <4.3
, memory ==0.18.* , memory ==0.18.*
@ -600,7 +605,6 @@ test-suite simplex-chat-test
, network ==3.1.* , network ==3.1.*
, network-transport ==0.5.6 , network-transport ==0.5.6
, optparse-applicative >=0.15 && <0.17 , optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, random >=1.1 && <1.3 , random >=1.1 && <1.3
, record-hasfield ==1.0.* , record-hasfield ==1.0.*
, silently ==1.2.* , silently ==1.2.*
@ -622,10 +626,18 @@ test-suite simplex-chat-test
if impl(ghc >= 9.6.2) if impl(ghc >= 9.6.2)
build-depends: build-depends:
bytestring ==0.11.* bytestring ==0.11.*
, process ==1.6.*
, template-haskell ==2.20.* , template-haskell ==2.20.*
, text >=2.0.1 && <2.2 , text >=2.0.1 && <2.2
if impl(ghc < 9.6.2) if impl(ghc < 9.6.2)
build-depends: build-depends:
bytestring ==0.10.* bytestring ==0.10.*
, process >=1.6 && <1.6.18
, template-haskell ==2.16.* , template-haskell ==2.16.*
, text >=1.2.3.0 && <1.3 , text >=1.2.3.0 && <1.3
if impl(ghc >= 9.6.2)
build-depends:
hspec ==2.11.*
if impl(ghc < 9.6.2)
build-depends:
hspec ==2.7.*

View File

@ -1028,6 +1028,7 @@ processChatCommand' vr = \case
when (memberActive membership && isOwner) . void $ sendGroupMessage' user gInfo members XGrpDel when (memberActive membership && isOwner) . void $ sendGroupMessage' user gInfo members XGrpDel
deleteGroupLinkIfExists user gInfo deleteGroupLinkIfExists user gInfo
deleteMembersConnections user members deleteMembersConnections user members
updateCIGroupInvitationStatus user gInfo CIGISRejected `catchChatError` \_ -> pure ()
-- functions below are called in separate transactions to prevent crashes on android -- functions below are called in separate transactions to prevent crashes on android
-- (possibly, race condition on integrity check?) -- (possibly, race condition on integrity check?)
withStore' $ \db -> deleteGroupConnectionsAndFiles db user gInfo members withStore' $ \db -> deleteGroupConnectionsAndFiles db user gInfo members
@ -1686,17 +1687,9 @@ processChatCommand' vr = \case
createMemberConnection db userId fromMember agentConnId (fromJVersionRange peerChatVRange) subMode createMemberConnection db userId fromMember agentConnId (fromJVersionRange peerChatVRange) subMode
updateGroupMemberStatus db userId fromMember GSMemAccepted updateGroupMemberStatus db userId fromMember GSMemAccepted
updateGroupMemberStatus db userId membership GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted
updateCIGroupInvitationStatus user updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` \_ -> pure ()
pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing
Nothing -> throwChatError $ CEContactNotActive ct Nothing -> throwChatError $ CEContactNotActive ct
where
updateCIGroupInvitationStatus user = do
AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db vr user groupId
case (cInfo, content) of
(DirectChat ct, CIRcvGroupInvitation ciGroupInv memRole) -> do
let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation ciGroupInv {status = CIGISAccepted} memRole
updateDirectChatItemView user ct itemId aciContent False Nothing
_ -> pure () -- prohibited
APIMemberRole groupId memberId memRole -> withUser $ \user -> do APIMemberRole groupId memberId memRole -> withUser $ \user -> do
Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user groupId Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user groupId
if memberId == groupMemberId' membership if memberId == groupMemberId' membership
@ -2512,6 +2505,14 @@ processChatCommand' vr = \case
cReqHashes :: (ConnReqUriHash, ConnReqUriHash) cReqHashes :: (ConnReqUriHash, ConnReqUriHash)
cReqHashes = bimap hash hash cReqSchemas cReqHashes = bimap hash hash cReqSchemas
hash = ConnReqUriHash . C.sha256Hash . strEncode hash = ConnReqUriHash . C.sha256Hash . strEncode
updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do
AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db vr user groupId
case (cInfo, content) of
(DirectChat ct, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole)
| status == CIGISPending -> do
let aciContent = ACIContent SMDRcv $ CIRcvGroupInvitation ciGroupInv {status = newStatus} memRole
updateDirectChatItemView user ct itemId aciContent False Nothing
_ -> pure () -- prohibited
toggleNtf :: ChatMonad m => User -> GroupMember -> Bool -> m () toggleNtf :: ChatMonad m => User -> GroupMember -> Bool -> m ()
toggleNtf user m ntfOn = toggleNtf user m ntfOn =

View File

@ -20,6 +20,7 @@ import qualified Data.ByteArray as BA
import qualified Data.ByteString.Base64.URL as U import qualified Data.ByteString.Base64.URL as U
import Data.ByteString.Char8 (ByteString) import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Functor (($>)) import Data.Functor (($>))
import Data.List (find) import Data.List (find)
import qualified Data.List.NonEmpty as L import qualified Data.List.NonEmpty as L
@ -94,6 +95,8 @@ foreign export ccall "chat_password_hash" cChatPasswordHash :: CString -> CStrin
foreign export ccall "chat_valid_name" cChatValidName :: CString -> IO CString foreign export ccall "chat_valid_name" cChatValidName :: CString -> IO CString
foreign export ccall "chat_json_length" cChatJsonLength :: CString -> IO CInt
foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString
foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString
@ -176,6 +179,10 @@ cChatPasswordHash cPwd cSalt = do
cChatValidName :: CString -> IO CString cChatValidName :: CString -> IO CString
cChatValidName cName = newCString . mkValidName =<< peekCString cName cChatValidName cName = newCString . mkValidName =<< peekCString cName
-- | returns length of JSON encoded string
cChatJsonLength :: CString -> IO CInt
cChatJsonLength s = fromIntegral . subtract 2 . LB.length . J.encode . safeDecodeUtf8 <$> B.packCString s
mobileChatOpts :: String -> ChatOpts mobileChatOpts :: String -> ChatOpts
mobileChatOpts dbFilePrefix = mobileChatOpts dbFilePrefix =
ChatOpts ChatOpts
@ -264,9 +271,18 @@ chatSendRemoteCmd :: ChatController -> Maybe RemoteHostId -> B.ByteString -> IO
chatSendRemoteCmd cc rh s = J.encode . APIResponse Nothing rh <$> runReaderT (execChatCommand rh s) cc chatSendRemoteCmd cc rh s = J.encode . APIResponse Nothing rh <$> runReaderT (execChatCommand rh s) cc
chatRecvMsg :: ChatController -> IO JSONByteString chatRecvMsg :: ChatController -> IO JSONByteString
chatRecvMsg ChatController {outputQ} = json <$> atomically (readTBQueue outputQ) chatRecvMsg ChatController {outputQ} = json <$> readChatResponse
where where
json (corr, remoteHostId, resp) = J.encode APIResponse {corr, remoteHostId, resp} json (corr, remoteHostId, resp) = J.encode APIResponse {corr, remoteHostId, resp}
readChatResponse = do
out@(_, _, cr) <- atomically $ readTBQueue outputQ
if filterEvent cr then pure out else readChatResponse
filterEvent = \case
CRGroupSubscribed {} -> False
CRGroupEmpty {} -> False
CRMemberSubSummary {} -> False
CRPendingSubSummary {} -> False
_ -> True
chatRecvMsgWait :: ChatController -> Int -> IO JSONByteString chatRecvMsgWait :: ChatController -> Int -> IO JSONByteString
chatRecvMsgWait cc time = fromMaybe "" <$> timeout time (chatRecvMsg cc) chatRecvMsgWait cc time = fromMaybe "" <$> timeout time (chatRecvMsg cc)

View File

@ -68,6 +68,8 @@ mobileTests = do
it "no exception on missing file" testMissingFileEncryptionCApi it "no exception on missing file" testMissingFileEncryptionCApi
describe "validate name" $ do describe "validate name" $ do
it "should convert invalid name to a valid name" testValidNameCApi it "should convert invalid name to a valid name" testValidNameCApi
describe "JSON length" $ do
it "should compute length of JSON encoded string" testChatJsonLengthCApi
noActiveUser :: LB.ByteString noActiveUser :: LB.ByteString
noActiveUser = noActiveUser =
@ -222,8 +224,6 @@ testChatApi tmp = do
chatSendCmd cc "/_start" `shouldReturn` chatStarted chatSendCmd cc "/_start" `shouldReturn` chatStarted
chatRecvMsg cc `shouldReturn` networkStatuses chatRecvMsg cc `shouldReturn` networkStatuses
chatRecvMsg cc `shouldReturn` userContactSubSummary chatRecvMsg cc `shouldReturn` userContactSubSummary
chatRecvMsg cc `shouldReturn` memberSubSummary
chatRecvMsgWait cc 10000 `shouldReturn` pendingSubSummary
chatRecvMsgWait cc 10000 `shouldReturn` "" chatRecvMsgWait cc 10000 `shouldReturn` ""
chatParseMarkdown "hello" `shouldBe` "{}" chatParseMarkdown "hello" `shouldBe` "{}"
chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown
@ -356,6 +356,13 @@ testValidNameCApi _ = do
cName2 <- cChatValidName =<< newCString " @'Джон' Доу 👍 " cName2 <- cChatValidName =<< newCString " @'Джон' Доу 👍 "
peekCString cName2 `shouldReturn` goodName peekCString cName2 `shouldReturn` goodName
testChatJsonLengthCApi :: FilePath -> IO ()
testChatJsonLengthCApi _ = do
cInt1 <- cChatJsonLength =<< newCString "Hello!"
cInt1 `shouldBe` 6
cInt2 <- cChatJsonLength =<< newCString "こんにちは!"
cInt2 `shouldBe` 18
jDecode :: FromJSON a => String -> IO (Maybe a) jDecode :: FromJSON a => String -> IO (Maybe a)
jDecode = pure . J.decode . LB.pack jDecode = pure . J.decode . LB.pack

View File

@ -250,5 +250,7 @@
"stable-versions-built-by-f-droid-org": "Stable versions built by F-Droid.org", "stable-versions-built-by-f-droid-org": "Stable versions built by F-Droid.org",
"releases-to-this-repo-are-done-1-2-days-later": "The releases to this repo are done 1-2 days later", "releases-to-this-repo-are-done-1-2-days-later": "The releases to this repo are done 1-2 days later",
"f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat and F-Droid.org repositories sign builds with the different keys. To switch, please <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>export</a> the chat database and re-install the app.", "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat and F-Droid.org repositories sign builds with the different keys. To switch, please <a href='/docs/guide/chat-profiles.html#move-your-chat-profiles-to-another-device'>export</a> the chat database and re-install the app.",
"jobs": "Join team" "jobs": "Join team",
"please-enable-javascript": "Please enable JavaScript to see the QR code.",
"please-use-link-in-mobile-app": "Please use the link in the mobile app"
} }

View File

@ -30,8 +30,12 @@
<div class="absolute mt-[-100px]"> <div class="absolute mt-[-100px]">
<img class="" src="/img/new/contact_page_mobile.png" alt=""> <img class="" src="/img/new/contact_page_mobile.png" alt="">
</div> </div>
<noscript class="z-10 flex flex-col items-center pt-[40px] ml-[-15px]">
<p class="text-2xl font-medium text-center max-w-[234px] mb-32">{{ "please-enable-javascript" | i18n({}, lang ) | safe }}</p>
</noscript>
<div class="z-10 flex flex-col items-center pt-[40px] ml-[-15px]"> <div class="z-10 flex flex-col items-center pt-[40px] ml-[-15px] d-none-if-js-disabled">
<p class="text-base font-medium text-center max-w-[234px]">{{ "scan-qr-code-from-mobile-app" | i18n({}, lang ) | safe }}</p> <p class="text-base font-medium text-center max-w-[234px]">{{ "scan-qr-code-from-mobile-app" | i18n({}, lang ) | safe }}</p>
<canvas class="conn_req_uri_qrcode"></canvas> <canvas class="conn_req_uri_qrcode"></canvas>
</div> </div>
@ -61,7 +65,11 @@
</div> </div>
<div class="flex flex-col justify-center items-center w-full max-w-[468px] h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 mb-6 relative"> <div class="flex flex-col justify-center items-center w-full max-w-[468px] h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 mb-6 relative">
<p class="text-xl font-medium text-grey-black dark:text-white mb-4">{{ "connect-in-app" | i18n({}, lang ) | safe }}</p> <p class="text-xl font-medium text-grey-black dark:text-white mb-4 d-none-if-js-disabled">{{ "connect-in-app" | i18n({}, lang ) | safe }}</p>
<noscript>
<p class="text-xl font-medium text-grey-black dark:text-white mb-4">{{ "please-use-link-in-mobile-app" | i18n({}, lang ) | safe }}</p>
</noscript>
<a id="mobile_conn_req_uri" class="bg-[#0053D0] text-white py-3 px-8 rounded-[34px] h-[44px] text-[16px] leading-[19px] tracking-[0.02em]">{{ "open-simplex-app" | i18n({}, lang ) | safe }}</a> <a id="mobile_conn_req_uri" class="bg-[#0053D0] text-white py-3 px-8 rounded-[34px] h-[44px] text-[16px] leading-[19px] tracking-[0.02em]">{{ "open-simplex-app" | i18n({}, lang ) | safe }}</a>
<div class="absolute bg-[#0197FF] h-[44px] w-[44px] rounded-full flex items-center justify-center top-0 left-0 translate-x-[-30%] translate-y-[-30%]"> <div class="absolute bg-[#0197FF] h-[44px] w-[44px] rounded-full flex items-center justify-center top-0 left-0 translate-x-[-30%] translate-y-[-30%]">
@ -69,7 +77,7 @@
</div> </div>
</div> </div>
<div class="flex flex-col justify-center items-center w-full max-w-[468px] h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 relative"> <div class="flex flex-col justify-center items-center w-full max-w-[468px] h-[131px] rounded-[30px] border-[1px] border-[#A8B0B4] dark:border-white border-opacity-60 relative d-none-if-js-disabled">
<p class="text-xl font-medium text-grey-black dark:text-white max-w-[230px] text-center">{{ "tap-the-connect-button-in-the-app" | i18n({}, lang ) | safe }}</p> <p class="text-xl font-medium text-grey-black dark:text-white max-w-[230px] text-center">{{ "tap-the-connect-button-in-the-app" | i18n({}, lang ) | safe }}</p>
<div class="absolute bg-[#0197FF] h-[44px] w-[44px] rounded-full flex items-center justify-center top-0 left-0 translate-x-[-30%] translate-y-[-30%]"> <div class="absolute bg-[#0197FF] h-[44px] w-[44px] rounded-full flex items-center justify-center top-0 left-0 translate-x-[-30%] translate-y-[-30%]">
@ -81,7 +89,7 @@
</section> </section>
<section class="hidden md:block bg-secondary-bg-light dark:bg-secondary-bg-dark py-[20px]"> <section class="hidden md:block bg-secondary-bg-light dark:bg-secondary-bg-dark py-[20px] d-none-if-js-disabled">
<div class="container px-5"> <div class="container px-5">
<div class="text-grey-black dark:text-white"> <div class="text-grey-black dark:text-white">
@ -164,3 +172,7 @@
{# join simplex #} {# join simplex #}
{% include "sections/join_simplex.html" %} {% include "sections/join_simplex.html" %}
<script>
document.querySelectorAll('.d-none-if-js-disabled').forEach(el => el.classList.remove('d-none-if-js-disabled'));
</script>

View File

@ -957,3 +957,7 @@ p a{
top: calc(66px + 2rem); top: calc(66px + 2rem);
} }
} }
.d-none-if-js-disabled{
display: none !important;
}