mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-29 20:59:53 +00:00
Merge branch 'master' into av/ios-open-file
This commit is contained in:
commit
72722498f1
44 changed files with 1913 additions and 583 deletions
|
@ -163,7 +163,7 @@ class ChatTagsModel: ObservableObject {
|
||||||
|
|
||||||
func markChatTagRead(_ chat: Chat) -> Void {
|
func markChatTagRead(_ chat: Chat) -> Void {
|
||||||
if chat.unreadTag, let tags = chat.chatInfo.chatTags {
|
if chat.unreadTag, let tags = chat.chatInfo.chatTags {
|
||||||
markChatTagRead_(chat, tags)
|
decTagsReadCount(tags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,11 +175,11 @@ class ChatTagsModel: ObservableObject {
|
||||||
unreadTags[tag] = (unreadTags[tag] ?? 0) + 1
|
unreadTags[tag] = (unreadTags[tag] ?? 0) + 1
|
||||||
}
|
}
|
||||||
} else if !nowUnread && wasUnread {
|
} else if !nowUnread && wasUnread {
|
||||||
markChatTagRead_(chat, tags)
|
decTagsReadCount(tags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func markChatTagRead_(_ chat: Chat, _ tags: [Int64]) -> Void {
|
func decTagsReadCount(_ tags: [Int64]) -> Void {
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
if let count = unreadTags[tag] {
|
if let count = unreadTags[tag] {
|
||||||
unreadTags[tag] = max(0, count - 1)
|
unreadTags[tag] = max(0, count - 1)
|
||||||
|
|
|
@ -748,7 +748,11 @@ private func setChatTag(tagId: Int64?, chat: Chat, closeSheet: @escaping () -> V
|
||||||
|
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
ChatTagsModel.shared.userTags = userTags
|
let tm = ChatTagsModel.shared
|
||||||
|
tm.userTags = userTags
|
||||||
|
if chat.unreadTag, let tags = chat.chatInfo.chatTags {
|
||||||
|
tm.decTagsReadCount(tags)
|
||||||
|
}
|
||||||
if var contact = chat.chatInfo.contact {
|
if var contact = chat.chatInfo.contact {
|
||||||
contact.chatTags = chatTags
|
contact.chatTags = chatTags
|
||||||
m.updateContact(contact)
|
m.updateContact(contact)
|
||||||
|
@ -756,6 +760,7 @@ private func setChatTag(tagId: Int64?, chat: Chat, closeSheet: @escaping () -> V
|
||||||
group.chatTags = chatTags
|
group.chatTags = chatTags
|
||||||
m.updateGroup(group)
|
m.updateGroup(group)
|
||||||
}
|
}
|
||||||
|
ChatTagsModel.shared.updateChatTagRead(chat, wasUnread: false)
|
||||||
closeSheet()
|
closeSheet()
|
||||||
}
|
}
|
||||||
} catch let error {
|
} catch let error {
|
||||||
|
|
|
@ -53,7 +53,7 @@ struct OperatorView: View {
|
||||||
ServersErrorView(errStr: errStr)
|
ServersErrorView(errStr: errStr)
|
||||||
} else {
|
} else {
|
||||||
switch (userServers[operatorIndex].operator_.conditionsAcceptance) {
|
switch (userServers[operatorIndex].operator_.conditionsAcceptance) {
|
||||||
case let .accepted(acceptedAt):
|
case let .accepted(acceptedAt, _):
|
||||||
if let acceptedAt = acceptedAt {
|
if let acceptedAt = acceptedAt {
|
||||||
Text("Conditions accepted on: \(conditionsTimestamp(acceptedAt)).")
|
Text("Conditions accepted on: \(conditionsTimestamp(acceptedAt)).")
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
|
|
@ -167,9 +167,9 @@
|
||||||
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; };
|
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; };
|
||||||
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; };
|
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; };
|
||||||
649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D82CFE07CF00536B68 /* libffi.a */; };
|
649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D82CFE07CF00536B68 /* libffi.a */; };
|
||||||
649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW-ghc9.6.3.a */; };
|
649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t-ghc9.6.3.a */; };
|
||||||
649B28DF2CFE07CF00536B68 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DA2CFE07CF00536B68 /* libgmpxx.a */; };
|
649B28DF2CFE07CF00536B68 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DA2CFE07CF00536B68 /* libgmpxx.a */; };
|
||||||
649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW.a */; };
|
649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t.a */; };
|
||||||
649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DC2CFE07CF00536B68 /* libgmp.a */; };
|
649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 649B28DC2CFE07CF00536B68 /* libgmp.a */; };
|
||||||
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; };
|
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; };
|
||||||
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
|
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
|
||||||
|
@ -517,9 +517,9 @@
|
||||||
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = "<group>"; };
|
648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemForwardingView.swift; sourceTree = "<group>"; };
|
||||||
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
649B28D82CFE07CF00536B68 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
649B28D82CFE07CF00536B68 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||||
649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW-ghc9.6.3.a"; sourceTree = "<group>"; };
|
649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||||
649B28DA2CFE07CF00536B68 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
649B28DA2CFE07CF00536B68 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||||
649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW.a"; sourceTree = "<group>"; };
|
649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t.a"; sourceTree = "<group>"; };
|
||||||
649B28DC2CFE07CF00536B68 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
649B28DC2CFE07CF00536B68 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||||
649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; };
|
649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; };
|
||||||
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
|
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -673,9 +673,9 @@
|
||||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||||
649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */,
|
649B28E12CFE07CF00536B68 /* libgmp.a in Frameworks */,
|
||||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||||
649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW.a in Frameworks */,
|
649B28E02CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t.a in Frameworks */,
|
||||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||||
649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW-ghc9.6.3.a in Frameworks */,
|
649B28DE2CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t-ghc9.6.3.a in Frameworks */,
|
||||||
649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */,
|
649B28DD2CFE07CF00536B68 /* libffi.a in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -756,8 +756,8 @@
|
||||||
649B28D82CFE07CF00536B68 /* libffi.a */,
|
649B28D82CFE07CF00536B68 /* libffi.a */,
|
||||||
649B28DC2CFE07CF00536B68 /* libgmp.a */,
|
649B28DC2CFE07CF00536B68 /* libgmp.a */,
|
||||||
649B28DA2CFE07CF00536B68 /* libgmpxx.a */,
|
649B28DA2CFE07CF00536B68 /* libgmpxx.a */,
|
||||||
649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW-ghc9.6.3.a */,
|
649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t-ghc9.6.3.a */,
|
||||||
649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW.a */,
|
649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t.a */,
|
||||||
);
|
);
|
||||||
path = Libraries;
|
path = Libraries;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1935,7 +1935,7 @@
|
||||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 255;
|
CURRENT_PROJECT_VERSION = 258;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
|
@ -1960,7 +1960,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
LLVM_LTO = YES_THIN;
|
LLVM_LTO = YES_THIN;
|
||||||
MARKETING_VERSION = 6.2.1;
|
MARKETING_VERSION = 6.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||||
PRODUCT_NAME = SimpleX;
|
PRODUCT_NAME = SimpleX;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -1984,7 +1984,7 @@
|
||||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 255;
|
CURRENT_PROJECT_VERSION = 258;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
|
@ -2009,7 +2009,7 @@
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
LLVM_LTO = YES;
|
LLVM_LTO = YES;
|
||||||
MARKETING_VERSION = 6.2.1;
|
MARKETING_VERSION = 6.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||||
PRODUCT_NAME = SimpleX;
|
PRODUCT_NAME = SimpleX;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -2025,11 +2025,11 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 255;
|
CURRENT_PROJECT_VERSION = 258;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 6.2.1;
|
MARKETING_VERSION = 6.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -2045,11 +2045,11 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 255;
|
CURRENT_PROJECT_VERSION = 258;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||||
MARKETING_VERSION = 6.2.1;
|
MARKETING_VERSION = 6.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -2070,7 +2070,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 = 255;
|
CURRENT_PROJECT_VERSION = 258;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GCC_OPTIMIZATION_LEVEL = s;
|
GCC_OPTIMIZATION_LEVEL = s;
|
||||||
|
@ -2085,7 +2085,7 @@
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LLVM_LTO = YES;
|
LLVM_LTO = YES;
|
||||||
MARKETING_VERSION = 6.2.1;
|
MARKETING_VERSION = 6.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 = "";
|
||||||
|
@ -2107,7 +2107,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 = 255;
|
CURRENT_PROJECT_VERSION = 258;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_CODE_COVERAGE = NO;
|
ENABLE_CODE_COVERAGE = NO;
|
||||||
|
@ -2122,7 +2122,7 @@
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LLVM_LTO = YES;
|
LLVM_LTO = YES;
|
||||||
MARKETING_VERSION = 6.2.1;
|
MARKETING_VERSION = 6.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 = "";
|
||||||
|
@ -2144,7 +2144,7 @@
|
||||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 255;
|
CURRENT_PROJECT_VERSION = 258;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
|
@ -2170,7 +2170,7 @@
|
||||||
"$(PROJECT_DIR)/Libraries/sim",
|
"$(PROJECT_DIR)/Libraries/sim",
|
||||||
);
|
);
|
||||||
LLVM_LTO = YES;
|
LLVM_LTO = YES;
|
||||||
MARKETING_VERSION = 6.2.1;
|
MARKETING_VERSION = 6.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;
|
||||||
|
@ -2195,7 +2195,7 @@
|
||||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 255;
|
CURRENT_PROJECT_VERSION = 258;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
|
@ -2221,7 +2221,7 @@
|
||||||
"$(PROJECT_DIR)/Libraries/sim",
|
"$(PROJECT_DIR)/Libraries/sim",
|
||||||
);
|
);
|
||||||
LLVM_LTO = YES;
|
LLVM_LTO = YES;
|
||||||
MARKETING_VERSION = 6.2.1;
|
MARKETING_VERSION = 6.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;
|
||||||
|
@ -2246,7 +2246,7 @@
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 255;
|
CURRENT_PROJECT_VERSION = 258;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
@ -2261,7 +2261,7 @@
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 6.2.1;
|
MARKETING_VERSION = 6.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
@ -2280,7 +2280,7 @@
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 255;
|
CURRENT_PROJECT_VERSION = 258;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
|
@ -2295,7 +2295,7 @@
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MARKETING_VERSION = 6.2.1;
|
MARKETING_VERSION = 6.3;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
|
|
@ -1324,7 +1324,7 @@ public struct ServerOperatorConditions: Decodable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ConditionsAcceptance: Equatable, Codable, Hashable {
|
public enum ConditionsAcceptance: Equatable, Codable, Hashable {
|
||||||
case accepted(acceptedAt: Date?)
|
case accepted(acceptedAt: Date?, autoAccepted: Bool)
|
||||||
// If deadline is present, it means there's a grace period to review and accept conditions during which user can continue to use the operator.
|
// If deadline is present, it means there's a grace period to review and accept conditions during which user can continue to use the operator.
|
||||||
// No deadline indicates it's required to accept conditions for the operator to start using it.
|
// No deadline indicates it's required to accept conditions for the operator to start using it.
|
||||||
case required(deadline: Date?)
|
case required(deadline: Date?)
|
||||||
|
@ -1398,7 +1398,7 @@ public struct ServerOperator: Identifiable, Equatable, Codable {
|
||||||
tradeName: "SimpleX Chat",
|
tradeName: "SimpleX Chat",
|
||||||
legalName: "SimpleX Chat Ltd",
|
legalName: "SimpleX Chat Ltd",
|
||||||
serverDomains: ["simplex.im"],
|
serverDomains: ["simplex.im"],
|
||||||
conditionsAcceptance: .accepted(acceptedAt: nil),
|
conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
smpRoles: ServerRoles(storage: true, proxy: true),
|
smpRoles: ServerRoles(storage: true, proxy: true),
|
||||||
xftpRoles: ServerRoles(storage: true, proxy: true)
|
xftpRoles: ServerRoles(storage: true, proxy: true)
|
||||||
|
@ -1431,7 +1431,7 @@ public struct UserOperatorServers: Identifiable, Equatable, Codable {
|
||||||
tradeName: "",
|
tradeName: "",
|
||||||
legalName: "",
|
legalName: "",
|
||||||
serverDomains: [],
|
serverDomains: [],
|
||||||
conditionsAcceptance: .accepted(acceptedAt: nil),
|
conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false),
|
||||||
enabled: false,
|
enabled: false,
|
||||||
smpRoles: ServerRoles(storage: true, proxy: true),
|
smpRoles: ServerRoles(storage: true, proxy: true),
|
||||||
xftpRoles: ServerRoles(storage: true, proxy: true)
|
xftpRoles: ServerRoles(storage: true, proxy: true)
|
||||||
|
|
|
@ -27,6 +27,14 @@
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
|
|
||||||
|
<!-- Allows to query app name and icon that can open specific file type -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="SimplexApp"
|
android:name="SimplexApp"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
|
|
@ -470,53 +470,65 @@ class SimplexService: Service() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode, showOffAlert: Boolean) = AlertManager.shared.showAlert {
|
private var showingIgnoreNotification = false
|
||||||
val ignoreOptimization = {
|
private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode, showOffAlert: Boolean) {
|
||||||
AlertManager.shared.hideAlert()
|
// that's workaround for situation when the app receives onPause/onResume events multiple times
|
||||||
askAboutIgnoringBatteryOptimization()
|
// (for example, after showing system alert for enabling notifications) which triggers showing that alert multiple times
|
||||||
|
if (showingIgnoreNotification) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
val disableNotifications = {
|
showingIgnoreNotification = true
|
||||||
AlertManager.shared.hideAlert()
|
AlertManager.shared.showAlert {
|
||||||
disableNotifications(mode, showOffAlert)
|
val ignoreOptimization = {
|
||||||
}
|
AlertManager.shared.hideAlert()
|
||||||
AlertDialog(
|
showingIgnoreNotification = false
|
||||||
onDismissRequest = disableNotifications,
|
askAboutIgnoringBatteryOptimization()
|
||||||
title = {
|
}
|
||||||
Row {
|
val disableNotifications = {
|
||||||
Icon(
|
AlertManager.shared.hideAlert()
|
||||||
painterResource(MR.images.ic_bolt),
|
showingIgnoreNotification = false
|
||||||
contentDescription =
|
disableNotifications(mode, showOffAlert)
|
||||||
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
|
}
|
||||||
)
|
AlertDialog(
|
||||||
Text(
|
onDismissRequest = disableNotifications,
|
||||||
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications) else stringResource(MR.strings.periodic_notifications),
|
title = {
|
||||||
fontWeight = FontWeight.Bold
|
Row {
|
||||||
)
|
Icon(
|
||||||
}
|
painterResource(MR.images.ic_bolt),
|
||||||
},
|
contentDescription =
|
||||||
text = {
|
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
|
||||||
Column {
|
)
|
||||||
Text(
|
Text(
|
||||||
if (mode == NotificationsMode.SERVICE) annotatedStringResource(MR.strings.to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery) else annotatedStringResource(MR.strings.periodic_notifications_desc),
|
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications) else stringResource(MR.strings.periodic_notifications),
|
||||||
Modifier.padding(bottom = 8.dp)
|
fontWeight = FontWeight.Bold
|
||||||
)
|
|
||||||
Text(annotatedStringResource(MR.strings.turn_off_battery_optimization))
|
|
||||||
|
|
||||||
if (platform.androidIsXiaomiDevice() && (mode == NotificationsMode.PERIODIC || mode == NotificationsMode.SERVICE)) {
|
|
||||||
Text(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization),
|
|
||||||
Modifier.padding(top = 8.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
text = {
|
||||||
dismissButton = {
|
Column {
|
||||||
TextButton(onClick = disableNotifications) { Text(stringResource(MR.strings.disable_notifications_button), color = MaterialTheme.colors.error) }
|
Text(
|
||||||
},
|
if (mode == NotificationsMode.SERVICE) annotatedStringResource(MR.strings.to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery) else annotatedStringResource(MR.strings.periodic_notifications_desc),
|
||||||
confirmButton = {
|
Modifier.padding(bottom = 8.dp)
|
||||||
TextButton(onClick = ignoreOptimization) { Text(stringResource(MR.strings.turn_off_battery_optimization_button)) }
|
)
|
||||||
},
|
Text(annotatedStringResource(MR.strings.turn_off_battery_optimization))
|
||||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
|
||||||
)
|
if (platform.androidIsXiaomiDevice() && (mode == NotificationsMode.PERIODIC || mode == NotificationsMode.SERVICE)) {
|
||||||
|
Text(
|
||||||
|
annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization),
|
||||||
|
Modifier.padding(top = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = disableNotifications) { Text(stringResource(MR.strings.disable_notifications_button), color = MaterialTheme.colors.error) }
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = ignoreOptimization) { Text(stringResource(MR.strings.turn_off_battery_optimization_button)) }
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showBGServiceNoticeSystemRestricted(mode: NotificationsMode, showOffAlert: Boolean) = AlertManager.shared.showAlert {
|
private fun showBGServiceNoticeSystemRestricted(mode: NotificationsMode, showOffAlert: Boolean) = AlertManager.shared.showAlert {
|
||||||
|
|
|
@ -87,6 +87,9 @@ kotlin {
|
||||||
implementation("io.coil-kt:coil-compose:2.6.0")
|
implementation("io.coil-kt:coil-compose:2.6.0")
|
||||||
implementation("io.coil-kt:coil-gif:2.6.0")
|
implementation("io.coil-kt:coil-gif:2.6.0")
|
||||||
|
|
||||||
|
// Emojis
|
||||||
|
implementation("androidx.emoji2:emoji2-emojipicker:1.4.0")
|
||||||
|
|
||||||
implementation("com.jakewharton:process-phoenix:3.0.0")
|
implementation("com.jakewharton:process-phoenix:3.0.0")
|
||||||
|
|
||||||
val cameraXVersion = "1.3.4"
|
val cameraXVersion = "1.3.4"
|
||||||
|
|
|
@ -3,19 +3,30 @@ package chat.simplex.common.platform
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.*
|
import android.content.*
|
||||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
import androidx.compose.ui.platform.UriHandler
|
import androidx.compose.ui.platform.UriHandler
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import chat.simplex.common.helpers.*
|
import chat.simplex.common.helpers.*
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.res.MR
|
||||||
import java.io.BufferedOutputStream
|
import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import chat.simplex.res.MR
|
import java.net.URI
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
data class OpenDefaultApp(
|
||||||
|
val name: String,
|
||||||
|
val icon: ImageBitmap,
|
||||||
|
val isSystemChooser: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
actual fun ClipboardManager.shareText(text: String) {
|
actual fun ClipboardManager.shareText(text: String) {
|
||||||
var text = text
|
var text = text
|
||||||
for (i in 10 downTo 1) {
|
for (i in 10 downTo 1) {
|
||||||
|
@ -37,7 +48,7 @@ actual fun ClipboardManager.shareText(text: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean) {
|
fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean, useChooser: Boolean = true) {
|
||||||
val uri = if (fileSource.cryptoArgs != null) {
|
val uri = if (fileSource.cryptoArgs != null) {
|
||||||
val tmpFile = File(tmpDir, fileSource.filePath)
|
val tmpFile = File(tmpDir, fileSource.filePath)
|
||||||
tmpFile.deleteOnExit()
|
tmpFile.deleteOnExit()
|
||||||
|
@ -67,9 +78,35 @@ fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean) {
|
||||||
type = mimeType
|
type = mimeType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
if (useChooser) {
|
||||||
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||||
androidAppContext.startActivity(shareIntent)
|
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
androidAppContext.startActivity(shareIntent)
|
||||||
|
} else {
|
||||||
|
sendIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
androidAppContext.startActivity(sendIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryDefaultAppForExtension(ext: String, encryptedFileUri: URI): OpenDefaultApp? {
|
||||||
|
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return null
|
||||||
|
val openIntent = Intent(Intent.ACTION_VIEW)
|
||||||
|
openIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
openIntent.setDataAndType(encryptedFileUri.toUri(), mimeType)
|
||||||
|
val pm = androidAppContext.packageManager
|
||||||
|
//// This method returns the list of apps but no priority, nor default flag
|
||||||
|
// val resInfoList: List<ResolveInfo> = if (Build.VERSION.SDK_INT >= 33) {
|
||||||
|
// pm.queryIntentActivities(openIntent, PackageManager.ResolveInfoFlags.of((PackageManager.MATCH_DEFAULT_ONLY).toLong()))
|
||||||
|
// } else {
|
||||||
|
// pm.queryIntentActivities(openIntent, PackageManager.MATCH_DEFAULT_ONLY)
|
||||||
|
// }.sortedBy { it.priority }
|
||||||
|
// val first = resInfoList.firstOrNull { it.isDefault } ?: resInfoList.firstOrNull() ?: return null
|
||||||
|
val act = pm.resolveActivity(openIntent, PackageManager.MATCH_DEFAULT_ONLY) ?: return null
|
||||||
|
// Log.d(TAG, "Default launch action ${act} ${act.loadLabel(pm)} ${act.activityInfo?.name}")
|
||||||
|
val label = act.loadLabel(pm).toString()
|
||||||
|
val icon = act.loadIcon(pm).toBitmap().asImageBitmap()
|
||||||
|
val chooser = act.activityInfo?.name?.endsWith("ResolverActivity") == true
|
||||||
|
return OpenDefaultApp(label, icon, chooser)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun shareFile(text: String, fileSource: CryptoFile) {
|
actual fun shareFile(text: String, fileSource: CryptoFile) {
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package chat.simplex.common.views.chat.item
|
||||||
|
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import chat.simplex.common.model.CryptoFile
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.views.helpers.DefaultDropdownMenu
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun SaveOrOpenFileMenu(
|
||||||
|
showMenu: MutableState<Boolean>,
|
||||||
|
encrypted: Boolean,
|
||||||
|
ext: String?,
|
||||||
|
encryptedUri: URI,
|
||||||
|
fileSource: CryptoFile,
|
||||||
|
saveFile: () -> Unit
|
||||||
|
) {
|
||||||
|
val defaultApp = remember(encryptedUri.toString()) { if (ext != null) queryDefaultAppForExtension(ext, encryptedUri) else null }
|
||||||
|
DefaultDropdownMenu(showMenu) {
|
||||||
|
if (defaultApp != null) {
|
||||||
|
if (!defaultApp.isSystemChooser) {
|
||||||
|
ItemAction(
|
||||||
|
stringResource(MR.strings.open_with_app).format(defaultApp.name),
|
||||||
|
defaultApp.icon,
|
||||||
|
textColor = MaterialTheme.colors.primary,
|
||||||
|
onClick = {
|
||||||
|
openOrShareFile("", fileSource, justOpen = true, useChooser = false)
|
||||||
|
showMenu.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ItemAction(
|
||||||
|
stringResource(MR.strings.open_with_app).format("…"),
|
||||||
|
painterResource(MR.images.ic_open_in_new),
|
||||||
|
color = MaterialTheme.colors.primary,
|
||||||
|
onClick = {
|
||||||
|
openOrShareFile("", fileSource, justOpen = true, useChooser = false)
|
||||||
|
showMenu.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ItemAction(
|
||||||
|
stringResource(MR.strings.save_verb),
|
||||||
|
painterResource(if (encrypted) MR.images.ic_lock_open_right else MR.images.ic_download),
|
||||||
|
color = MaterialTheme.colors.primary,
|
||||||
|
onClick = {
|
||||||
|
saveFile()
|
||||||
|
showMenu.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package chat.simplex.common.views.chatlist
|
||||||
|
|
||||||
|
import SectionItemView
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.emoji2.emojipicker.EmojiPickerView
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
|
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
|
||||||
|
import chat.simplex.common.views.chat.topPaddingToContent
|
||||||
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun ChatTagInput(name: MutableState<String>, showError: State<Boolean>, emoji: MutableState<String?>) {
|
||||||
|
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
|
||||||
|
Box(Modifier
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
.clickable {
|
||||||
|
ModalManager.start.showModalCloseable { close ->
|
||||||
|
EmojiPicker(close = {
|
||||||
|
close()
|
||||||
|
emoji.value = it
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(4.dp)
|
||||||
|
) {
|
||||||
|
val emojiValue = emoji.value
|
||||||
|
if (emojiValue != null) {
|
||||||
|
Text(emojiValue)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(MR.images.ic_add_reaction),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
TagListNameTextField(name, showError = showError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmojiPicker(close: (String?) -> Unit) {
|
||||||
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
|
val topPaddingToContent = topPaddingToContent(false)
|
||||||
|
|
||||||
|
Column (
|
||||||
|
modifier = Modifier.fillMaxSize().navigationBarsPadding().padding(
|
||||||
|
start = DEFAULT_PADDING_HALF,
|
||||||
|
end = DEFAULT_PADDING_HALF,
|
||||||
|
top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent,
|
||||||
|
bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { context ->
|
||||||
|
EmojiPickerView(context).apply {
|
||||||
|
emojiGridColumns = 10
|
||||||
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
setOnEmojiPickedListener { pickedEmoji ->
|
||||||
|
close(pickedEmoji.emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,19 +4,31 @@ import android.Manifest
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import chat.simplex.common.platform.ntfManager
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import com.google.accompanist.permissions.PermissionStatus
|
import chat.simplex.common.platform.*
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.*
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun SetNotificationsModeAdditions() {
|
actual fun SetNotificationsModeAdditions() {
|
||||||
if (Build.VERSION.SDK_INT >= 33) {
|
if (Build.VERSION.SDK_INT >= 33) {
|
||||||
val notificationsPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
val notificationsPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
LaunchedEffect(notificationsPermissionState.status == PermissionStatus.Granted) {
|
LaunchedEffect(notificationsPermissionState.status == PermissionStatus.Granted) {
|
||||||
if (notificationsPermissionState.status == PermissionStatus.Granted) {
|
val canAsk = appPrefs.canAskToEnableNotifications.get()
|
||||||
ntfManager.androidCreateNtfChannelsMaybeShowAlert()
|
if (notificationsPermissionState.status is PermissionStatus.Denied) {
|
||||||
|
if (notificationsPermissionState.status.shouldShowRationale || !canAsk) {
|
||||||
|
if (canAsk) {
|
||||||
|
appPrefs.canAskToEnableNotifications.set(false)
|
||||||
|
}
|
||||||
|
Log.w(TAG, "Notifications are disabled and nobody will ask to enable them")
|
||||||
|
} else {
|
||||||
|
notificationsPermissionState.launchPermissionRequest()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
notificationsPermissionState.launchPermissionRequest()
|
if (!canAsk) {
|
||||||
|
// the user allowed notifications in system alert or manually in settings, allow to ask him next time if needed
|
||||||
|
appPrefs.canAskToEnableNotifications.set(true)
|
||||||
|
}
|
||||||
|
ntfManager.androidCreateNtfChannelsMaybeShowAlert()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.call.*
|
import chat.simplex.common.views.call.*
|
||||||
import chat.simplex.common.views.chat.*
|
import chat.simplex.common.views.chat.*
|
||||||
|
import chat.simplex.common.views.chatlist.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.migration.MigrationToDeviceState
|
import chat.simplex.common.views.migration.MigrationToDeviceState
|
||||||
import chat.simplex.common.views.migration.MigrationToState
|
import chat.simplex.common.views.migration.MigrationToState
|
||||||
|
@ -81,6 +82,12 @@ object ChatModel {
|
||||||
val groupMembers = mutableStateListOf<GroupMember>()
|
val groupMembers = mutableStateListOf<GroupMember>()
|
||||||
val groupMembersIndexes = mutableStateMapOf<Long, Int>()
|
val groupMembersIndexes = mutableStateMapOf<Long, Int>()
|
||||||
|
|
||||||
|
// Chat Tags
|
||||||
|
val userTags = mutableStateOf(emptyList<ChatTag>())
|
||||||
|
val activeChatTagFilter = mutableStateOf<ActiveFilter?>(null)
|
||||||
|
val presetTags = mutableStateMapOf<PresetTagKind, Int>()
|
||||||
|
val unreadTags = mutableStateMapOf<Long, Int>()
|
||||||
|
|
||||||
// false: default placement, true: floating window.
|
// false: default placement, true: floating window.
|
||||||
// Used for deciding to add terminal items on main thread or not. Floating means appPrefs.terminalAlwaysVisible
|
// Used for deciding to add terminal items on main thread or not. Floating means appPrefs.terminalAlwaysVisible
|
||||||
var terminalsVisible = setOf<Boolean>()
|
var terminalsVisible = setOf<Boolean>()
|
||||||
|
@ -196,6 +203,116 @@ object ChatModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateChatTags(rhId: Long?) {
|
||||||
|
val newPresetTags = mutableMapOf<PresetTagKind, Int>()
|
||||||
|
val newUnreadTags = mutableMapOf<Long, Int>()
|
||||||
|
|
||||||
|
for (chat in chats.value.filter { it.remoteHostId == rhId }) {
|
||||||
|
for (tag in PresetTagKind.entries) {
|
||||||
|
if (presetTagMatchesChat(tag, chat.chatInfo)) {
|
||||||
|
newPresetTags[tag] = (newPresetTags[tag] ?: 0) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (chat.unreadTag) {
|
||||||
|
val chatTags: List<Long> = when (val cInfo = chat.chatInfo) {
|
||||||
|
is ChatInfo.Direct -> cInfo.contact.chatTags
|
||||||
|
is ChatInfo.Group -> cInfo.groupInfo.chatTags
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
chatTags.forEach { tag ->
|
||||||
|
newUnreadTags[tag] = (newUnreadTags[tag] ?: 0) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeChatTagFilter.value is ActiveFilter.PresetTag &&
|
||||||
|
(newPresetTags[(activeChatTagFilter.value as ActiveFilter.PresetTag).tag] ?: 0) == 0) {
|
||||||
|
activeChatTagFilter.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
presetTags.clear()
|
||||||
|
presetTags.putAll(newPresetTags)
|
||||||
|
unreadTags.clear()
|
||||||
|
unreadTags.putAll(newUnreadTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateChatFavorite(favorite: Boolean, wasFavorite: Boolean) {
|
||||||
|
val count = presetTags[PresetTagKind.FAVORITES]
|
||||||
|
|
||||||
|
if (favorite && !wasFavorite) {
|
||||||
|
presetTags[PresetTagKind.FAVORITES] = (count ?: 0) + 1
|
||||||
|
} else if (!favorite && wasFavorite && count != null) {
|
||||||
|
presetTags[PresetTagKind.FAVORITES] = maxOf(0, count - 1)
|
||||||
|
if (activeChatTagFilter.value == ActiveFilter.PresetTag(PresetTagKind.FAVORITES) && (presetTags[PresetTagKind.FAVORITES] ?: 0) == 0) {
|
||||||
|
activeChatTagFilter.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addPresetChatTags(chatInfo: ChatInfo) {
|
||||||
|
for (tag in PresetTagKind.entries) {
|
||||||
|
if (presetTagMatchesChat(tag, chatInfo)) {
|
||||||
|
presetTags[tag] = (presetTags[tag] ?: 0) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removePresetChatTags(chatInfo: ChatInfo) {
|
||||||
|
for (tag in PresetTagKind.entries) {
|
||||||
|
if (presetTagMatchesChat(tag, chatInfo)) {
|
||||||
|
val count = presetTags[tag]
|
||||||
|
if (count != null) {
|
||||||
|
presetTags[tag] = maxOf(0, count - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markChatTagRead(chat: Chat) {
|
||||||
|
if (chat.unreadTag) {
|
||||||
|
chat.chatInfo.chatTags?.let { tags ->
|
||||||
|
markChatTagRead_(chat, tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateChatTagRead(chat: Chat, wasUnread: Boolean) {
|
||||||
|
val tags = chat.chatInfo.chatTags ?: return
|
||||||
|
val nowUnread = chat.unreadTag
|
||||||
|
|
||||||
|
if (nowUnread && !wasUnread) {
|
||||||
|
tags.forEach { tag ->
|
||||||
|
unreadTags[tag] = (unreadTags[tag] ?: 0) + 1
|
||||||
|
}
|
||||||
|
} else if (!nowUnread && wasUnread) {
|
||||||
|
markChatTagRead_(chat, tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveChatTagUnread(chat: Chat, oldTags: List<Long>?, newTags: List<Long>) {
|
||||||
|
if (chat.unreadTag) {
|
||||||
|
oldTags?.forEach { t ->
|
||||||
|
val oldCount = unreadTags[t]
|
||||||
|
if (oldCount != null) {
|
||||||
|
unreadTags[t] = maxOf(0, oldCount - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newTags.forEach { t ->
|
||||||
|
unreadTags[t] = (unreadTags[t] ?: 0) + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun markChatTagRead_(chat: Chat, tags: List<Long>) {
|
||||||
|
for (tag in tags) {
|
||||||
|
val count = unreadTags[tag]
|
||||||
|
if (count != null) {
|
||||||
|
unreadTags[tag] = maxOf(0, count - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// toList() here is to prevent ConcurrentModificationException that is rarely happens but happens
|
// toList() here is to prevent ConcurrentModificationException that is rarely happens but happens
|
||||||
fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null
|
fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null
|
||||||
// TODO pass rhId?
|
// TODO pass rhId?
|
||||||
|
@ -280,6 +397,7 @@ object ChatModel {
|
||||||
updateChatInfo(rhId, cInfo)
|
updateChatInfo(rhId, cInfo)
|
||||||
} else if (addMissing) {
|
} else if (addMissing) {
|
||||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf()))
|
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf()))
|
||||||
|
addPresetChatTags(cInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,6 +447,7 @@ object ChatModel {
|
||||||
}
|
}
|
||||||
else -> cItem
|
else -> cItem
|
||||||
}
|
}
|
||||||
|
val wasUnread = chat.unreadTag
|
||||||
chats[i] = chat.copy(
|
chats[i] = chat.copy(
|
||||||
chatItems = arrayListOf(newPreviewItem),
|
chatItems = arrayListOf(newPreviewItem),
|
||||||
chatStats =
|
chatStats =
|
||||||
|
@ -339,6 +458,8 @@ object ChatModel {
|
||||||
else
|
else
|
||||||
chat.chatStats
|
chat.chatStats
|
||||||
)
|
)
|
||||||
|
updateChatTagRead(chats[i], wasUnread)
|
||||||
|
|
||||||
if (appPlatform.isDesktop && cItem.chatDir.sent) {
|
if (appPlatform.isDesktop && cItem.chatDir.sent) {
|
||||||
reorderChat(chats[i], 0)
|
reorderChat(chats[i], 0)
|
||||||
} else {
|
} else {
|
||||||
|
@ -455,6 +576,7 @@ object ChatModel {
|
||||||
if (i >= 0) {
|
if (i >= 0) {
|
||||||
decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount)
|
decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount)
|
||||||
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
|
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
|
||||||
|
markChatTagRead(chats[i])
|
||||||
}
|
}
|
||||||
// clear current chat
|
// clear current chat
|
||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
|
@ -522,11 +644,13 @@ object ChatModel {
|
||||||
val chat = chats[chatIdx]
|
val chat = chats[chatIdx]
|
||||||
val lastId = chat.chatItems.lastOrNull()?.id
|
val lastId = chat.chatItems.lastOrNull()?.id
|
||||||
if (lastId != null) {
|
if (lastId != null) {
|
||||||
|
val wasUnread = chat.unreadTag
|
||||||
val unreadCount = if (itemIds != null) chat.chatStats.unreadCount - markedRead else 0
|
val unreadCount = if (itemIds != null) chat.chatStats.unreadCount - markedRead else 0
|
||||||
decreaseUnreadCounter(remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
decreaseUnreadCounter(remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||||
chats[chatIdx] = chat.copy(
|
chats[chatIdx] = chat.copy(
|
||||||
chatStats = chat.chatStats.copy(unreadCount = unreadCount)
|
chatStats = chat.chatStats.copy(unreadCount = unreadCount)
|
||||||
)
|
)
|
||||||
|
updateChatTagRead(chats[chatIdx], wasUnread)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -537,16 +661,29 @@ object ChatModel {
|
||||||
|
|
||||||
val chat = chats[chatIndex]
|
val chat = chats[chatIndex]
|
||||||
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
|
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
|
||||||
|
val wasUnread = chat.unreadTag
|
||||||
decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||||
chats[chatIndex] = chat.copy(
|
chats[chatIndex] = chat.copy(
|
||||||
chatStats = chat.chatStats.copy(
|
chatStats = chat.chatStats.copy(
|
||||||
unreadCount = unreadCount,
|
unreadCount = unreadCount,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
updateChatTagRead(chats[chatIndex], wasUnread)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeChat(rhId: Long?, id: String) {
|
fun removeChat(rhId: Long?, id: String) {
|
||||||
chats.removeAll { it.id == id && it.remoteHostId == rhId }
|
var removed: ChatInfo? = null
|
||||||
|
chats.removeAll {
|
||||||
|
val found = it.id == id && it.remoteHostId == rhId
|
||||||
|
if (found) {
|
||||||
|
removed = it.chatInfo
|
||||||
|
}
|
||||||
|
found
|
||||||
|
}
|
||||||
|
|
||||||
|
removed?.let {
|
||||||
|
removePresetChatTags(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean {
|
suspend fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean {
|
||||||
|
@ -977,6 +1114,8 @@ data class Chat(
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val unreadTag: Boolean get() = chatInfo.ntfsEnabled && (chatStats.unreadCount > 0 || chatStats.unreadChat)
|
||||||
|
|
||||||
val id: String get() = chatInfo.id
|
val id: String get() = chatInfo.id
|
||||||
|
|
||||||
fun groupFeatureEnabled(feature: GroupFeature): Boolean =
|
fun groupFeatureEnabled(feature: GroupFeature): Boolean =
|
||||||
|
@ -1189,6 +1328,12 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val chatTags: List<Long>?
|
||||||
|
get() = when (this) {
|
||||||
|
is Direct -> contact.chatTags
|
||||||
|
is Group -> groupInfo.chatTags
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
@ -1232,6 +1377,7 @@ data class Contact(
|
||||||
val chatTs: Instant?,
|
val chatTs: Instant?,
|
||||||
val contactGroupMemberId: Long? = null,
|
val contactGroupMemberId: Long? = null,
|
||||||
val contactGrpInvSent: Boolean,
|
val contactGrpInvSent: Boolean,
|
||||||
|
val chatTags: List<Long>,
|
||||||
override val chatDeleted: Boolean,
|
override val chatDeleted: Boolean,
|
||||||
val uiThemes: ThemeModeOverrides? = null,
|
val uiThemes: ThemeModeOverrides? = null,
|
||||||
): SomeChat, NamedChat {
|
): SomeChat, NamedChat {
|
||||||
|
@ -1315,6 +1461,7 @@ data class Contact(
|
||||||
contactGrpInvSent = false,
|
contactGrpInvSent = false,
|
||||||
chatDeleted = false,
|
chatDeleted = false,
|
||||||
uiThemes = null,
|
uiThemes = null,
|
||||||
|
chatTags = emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1476,6 +1623,7 @@ data class GroupInfo (
|
||||||
override val updatedAt: Instant,
|
override val updatedAt: Instant,
|
||||||
val chatTs: Instant?,
|
val chatTs: Instant?,
|
||||||
val uiThemes: ThemeModeOverrides? = null,
|
val uiThemes: ThemeModeOverrides? = null,
|
||||||
|
val chatTags: List<Long>
|
||||||
): SomeChat, NamedChat {
|
): SomeChat, NamedChat {
|
||||||
override val chatType get() = ChatType.Group
|
override val chatType get() = ChatType.Group
|
||||||
override val id get() = "#$groupId"
|
override val id get() = "#$groupId"
|
||||||
|
@ -1520,6 +1668,7 @@ data class GroupInfo (
|
||||||
updatedAt = Clock.System.now(),
|
updatedAt = Clock.System.now(),
|
||||||
chatTs = Clock.System.now(),
|
chatTs = Clock.System.now(),
|
||||||
uiThemes = null,
|
uiThemes = null,
|
||||||
|
chatTags = emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3850,6 +3999,13 @@ sealed class ChatItemTTL: Comparable<ChatItemTTL?> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ChatTag(
|
||||||
|
val chatTagId: Long,
|
||||||
|
val chatTagText: String,
|
||||||
|
val chatTagEmoji: String?
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ChatItemInfo(
|
class ChatItemInfo(
|
||||||
val itemVersions: List<ChatItemVersion>,
|
val itemVersions: List<ChatItemVersion>,
|
||||||
|
|
|
@ -80,6 +80,7 @@ class AppPreferences {
|
||||||
if (!runServiceInBackground.get()) NotificationsMode.OFF else NotificationsMode.default
|
if (!runServiceInBackground.get()) NotificationsMode.OFF else NotificationsMode.default
|
||||||
) { NotificationsMode.values().firstOrNull { it.name == this } }
|
) { NotificationsMode.values().firstOrNull { it.name == this } }
|
||||||
val notificationPreviewMode = mkStrPreference(SHARED_PREFS_NOTIFICATION_PREVIEW_MODE, NotificationPreviewMode.default.name)
|
val notificationPreviewMode = mkStrPreference(SHARED_PREFS_NOTIFICATION_PREVIEW_MODE, NotificationPreviewMode.default.name)
|
||||||
|
val canAskToEnableNotifications = mkBoolPreference(SHARED_PREFS_CAN_ASK_TO_ENABLE_NOTIFICATIONS, true)
|
||||||
val backgroundServiceNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false)
|
val backgroundServiceNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false)
|
||||||
val backgroundServiceBatteryNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false)
|
val backgroundServiceBatteryNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false)
|
||||||
val autoRestartWorkerVersion = mkIntPreference(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, 0)
|
val autoRestartWorkerVersion = mkIntPreference(SHARED_PREFS_AUTO_RESTART_WORKER_VERSION, 0)
|
||||||
|
@ -358,6 +359,7 @@ class AppPreferences {
|
||||||
private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground"
|
private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground"
|
||||||
private const val SHARED_PREFS_NOTIFICATIONS_MODE = "NotificationsMode"
|
private const val SHARED_PREFS_NOTIFICATIONS_MODE = "NotificationsMode"
|
||||||
private const val SHARED_PREFS_NOTIFICATION_PREVIEW_MODE = "NotificationPreviewMode"
|
private const val SHARED_PREFS_NOTIFICATION_PREVIEW_MODE = "NotificationPreviewMode"
|
||||||
|
private const val SHARED_PREFS_CAN_ASK_TO_ENABLE_NOTIFICATIONS = "CanAskToEnableNotifications"
|
||||||
private const val SHARED_PREFS_SERVICE_NOTICE_SHOWN = "BackgroundServiceNoticeShown"
|
private const val SHARED_PREFS_SERVICE_NOTICE_SHOWN = "BackgroundServiceNoticeShown"
|
||||||
private const val SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN = "BackgroundServiceBatteryNoticeShown"
|
private const val SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN = "BackgroundServiceBatteryNoticeShown"
|
||||||
private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay"
|
private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay"
|
||||||
|
@ -624,6 +626,9 @@ object ChatController {
|
||||||
val chats = apiGetChats(rhId)
|
val chats = apiGetChats(rhId)
|
||||||
updateChats(chats)
|
updateChats(chats)
|
||||||
}
|
}
|
||||||
|
chatModel.userTags.value = apiGetChatTags(rhId).takeIf { hasUser } ?: emptyList()
|
||||||
|
chatModel.activeChatTagFilter.value = null
|
||||||
|
chatModel.updateChatTags(rhId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startReceiver() {
|
private fun startReceiver() {
|
||||||
|
@ -879,6 +884,16 @@ object ChatController {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun apiGetChatTags(rh: Long?): List<ChatTag>?{
|
||||||
|
val userId = currentUserId("apiGetChatTags")
|
||||||
|
val r = sendCmd(rh, CC.ApiGetChatTags(userId))
|
||||||
|
|
||||||
|
if (r is CR.ChatTags) return r.userTags
|
||||||
|
Log.e(TAG, "apiGetChatTags bad response: ${r.responseType} ${r.details}")
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_loading_chat_tags), "${r.responseType}: ${r.details}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination, search: String = ""): Pair<Chat, NavigationInfo>? {
|
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination, search: String = ""): Pair<Chat, NavigationInfo>? {
|
||||||
val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search))
|
val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search))
|
||||||
if (r is CR.ApiChat) return if (rh == null) r.chat to r.navInfo else r.chat.copy(remoteHostId = rh) to r.navInfo
|
if (r is CR.ApiChat) return if (rh == null) r.chat to r.navInfo else r.chat.copy(remoteHostId = rh) to r.navInfo
|
||||||
|
@ -891,6 +906,28 @@ object ChatController {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun apiCreateChatTag(rh: Long?, tag: ChatTagData): List<ChatTag>? {
|
||||||
|
val r = sendCmd(rh, CC.ApiCreateChatTag(tag))
|
||||||
|
if (r is CR.ChatTags) return r.userTags
|
||||||
|
Log.e(TAG, "apiCreateChatTag bad response: ${r.responseType} ${r.details}")
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_creating_chat_tags), "${r.responseType}: ${r.details}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun apiSetChatTags(rh: Long?, type: ChatType, id: Long, tagIds: List<Long>): Pair<List<ChatTag>, List<Long>>? {
|
||||||
|
val r = sendCmd(rh, CC.ApiSetChatTags(type, id, tagIds))
|
||||||
|
if (r is CR.TagsUpdated) return r.userTags to r.chatTags
|
||||||
|
Log.e(TAG, "apiSetChatTags bad response: ${r.responseType} ${r.details}")
|
||||||
|
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_updating_chat_tags), "${r.responseType}: ${r.details}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun apiDeleteChatTag(rh: Long?, tagId: Long) = sendCommandOkResp(rh, CC.ApiDeleteChatTag(tagId))
|
||||||
|
|
||||||
|
suspend fun apiUpdateChatTag(rh: Long?, tagId: Long, tag: ChatTagData) = sendCommandOkResp(rh, CC.ApiUpdateChatTag(tagId, tag))
|
||||||
|
|
||||||
|
suspend fun apiReorderChatTags(rh: Long?, tagIds: List<Long>) = sendCommandOkResp(rh, CC.ApiReorderChatTags(tagIds))
|
||||||
|
|
||||||
suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, live: Boolean = false, ttl: Int? = null, composedMessages: List<ComposedMessage>): List<AChatItem>? {
|
suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, live: Boolean = false, ttl: Int? = null, composedMessages: List<ComposedMessage>): List<AChatItem>? {
|
||||||
val cmd = CC.ApiSendMessages(type, id, live, ttl, composedMessages)
|
val cmd = CC.ApiSendMessages(type, id, live, ttl, composedMessages)
|
||||||
return processSendMessageCmd(rh, cmd)
|
return processSendMessageCmd(rh, cmd)
|
||||||
|
@ -3152,10 +3189,16 @@ sealed class CC {
|
||||||
class TestStorageEncryption(val key: String): CC()
|
class TestStorageEncryption(val key: String): CC()
|
||||||
class ApiSaveSettings(val settings: AppSettings): CC()
|
class ApiSaveSettings(val settings: AppSettings): CC()
|
||||||
class ApiGetSettings(val settings: AppSettings): CC()
|
class ApiGetSettings(val settings: AppSettings): CC()
|
||||||
|
class ApiGetChatTags(val userId: Long): CC()
|
||||||
class ApiGetChats(val userId: Long): CC()
|
class ApiGetChats(val userId: Long): CC()
|
||||||
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
|
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
|
||||||
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
|
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
|
||||||
class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
|
class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
|
||||||
|
class ApiCreateChatTag(val tag: ChatTagData): CC()
|
||||||
|
class ApiSetChatTags(val type: ChatType, val id: Long, val tagIds: List<Long>): CC()
|
||||||
|
class ApiDeleteChatTag(val tagId: Long): CC()
|
||||||
|
class ApiUpdateChatTag(val tagId: Long, val tagData: ChatTagData): CC()
|
||||||
|
class ApiReorderChatTags(val tagIds: List<Long>): CC()
|
||||||
class ApiCreateChatItems(val noteFolderId: Long, val composedMessages: List<ComposedMessage>): CC()
|
class ApiCreateChatItems(val noteFolderId: Long, val composedMessages: List<ComposedMessage>): CC()
|
||||||
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC()
|
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC()
|
||||||
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
|
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
|
||||||
|
@ -3307,6 +3350,7 @@ sealed class CC {
|
||||||
is TestStorageEncryption -> "/db test key $key"
|
is TestStorageEncryption -> "/db test key $key"
|
||||||
is ApiSaveSettings -> "/_save app settings ${json.encodeToString(settings)}"
|
is ApiSaveSettings -> "/_save app settings ${json.encodeToString(settings)}"
|
||||||
is ApiGetSettings -> "/_get app settings ${json.encodeToString(settings)}"
|
is ApiGetSettings -> "/_get app settings ${json.encodeToString(settings)}"
|
||||||
|
is ApiGetChatTags -> "/_get tags $userId"
|
||||||
is ApiGetChats -> "/_get chats $userId pcc=on"
|
is ApiGetChats -> "/_get chats $userId pcc=on"
|
||||||
is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||||
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
|
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
|
||||||
|
@ -3315,6 +3359,11 @@ sealed class CC {
|
||||||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||||
"/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json $msgs"
|
"/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json $msgs"
|
||||||
}
|
}
|
||||||
|
is ApiCreateChatTag -> "/_create tag ${json.encodeToString(tag)}"
|
||||||
|
is ApiSetChatTags -> "/_tags ${chatRef(type, id)} ${tagIds.joinToString(",")}"
|
||||||
|
is ApiDeleteChatTag -> "/_delete tag $tagId"
|
||||||
|
is ApiUpdateChatTag -> "/_update tag $tagId ${json.encodeToString(tagData)}"
|
||||||
|
is ApiReorderChatTags -> "/_reorder tags ${tagIds.joinToString(",")}"
|
||||||
is ApiCreateChatItems -> {
|
is ApiCreateChatItems -> {
|
||||||
val msgs = json.encodeToString(composedMessages)
|
val msgs = json.encodeToString(composedMessages)
|
||||||
"/_create *$noteFolderId json $msgs"
|
"/_create *$noteFolderId json $msgs"
|
||||||
|
@ -3471,10 +3520,16 @@ sealed class CC {
|
||||||
is TestStorageEncryption -> "testStorageEncryption"
|
is TestStorageEncryption -> "testStorageEncryption"
|
||||||
is ApiSaveSettings -> "apiSaveSettings"
|
is ApiSaveSettings -> "apiSaveSettings"
|
||||||
is ApiGetSettings -> "apiGetSettings"
|
is ApiGetSettings -> "apiGetSettings"
|
||||||
|
is ApiGetChatTags -> "apiGetChatTags"
|
||||||
is ApiGetChats -> "apiGetChats"
|
is ApiGetChats -> "apiGetChats"
|
||||||
is ApiGetChat -> "apiGetChat"
|
is ApiGetChat -> "apiGetChat"
|
||||||
is ApiGetChatItemInfo -> "apiGetChatItemInfo"
|
is ApiGetChatItemInfo -> "apiGetChatItemInfo"
|
||||||
is ApiSendMessages -> "apiSendMessages"
|
is ApiSendMessages -> "apiSendMessages"
|
||||||
|
is ApiCreateChatTag -> "apiCreateChatTag"
|
||||||
|
is ApiSetChatTags -> "apiSetChatTags"
|
||||||
|
is ApiDeleteChatTag -> "apiDeleteChatTag"
|
||||||
|
is ApiUpdateChatTag -> "apiUpdateChatTag"
|
||||||
|
is ApiReorderChatTags -> "apiReorderChatTags"
|
||||||
is ApiCreateChatItems -> "apiCreateChatItems"
|
is ApiCreateChatItems -> "apiCreateChatItems"
|
||||||
is ApiUpdateChatItem -> "apiUpdateChatItem"
|
is ApiUpdateChatItem -> "apiUpdateChatItem"
|
||||||
is ApiDeleteChatItem -> "apiDeleteChatItem"
|
is ApiDeleteChatItem -> "apiDeleteChatItem"
|
||||||
|
@ -3657,6 +3712,9 @@ sealed class ChatPagination {
|
||||||
@Serializable
|
@Serializable
|
||||||
class ComposedMessage(val fileSource: CryptoFile?, val quotedItemId: Long?, val msgContent: MsgContent)
|
class ComposedMessage(val fileSource: CryptoFile?, val quotedItemId: Long?, val msgContent: MsgContent)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChatTagData(val emoji: String?, val text: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null)
|
class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null)
|
||||||
|
|
||||||
|
@ -3757,7 +3815,7 @@ data class ServerOperatorConditionsDetail(
|
||||||
|
|
||||||
@Serializable()
|
@Serializable()
|
||||||
sealed class ConditionsAcceptance {
|
sealed class ConditionsAcceptance {
|
||||||
@Serializable @SerialName("accepted") data class Accepted(val acceptedAt: Instant?) : ConditionsAcceptance()
|
@Serializable @SerialName("accepted") data class Accepted(val acceptedAt: Instant?, val autoAccepted: Boolean) : ConditionsAcceptance()
|
||||||
@Serializable @SerialName("required") data class Required(val deadline: Instant?) : ConditionsAcceptance()
|
@Serializable @SerialName("required") data class Required(val deadline: Instant?) : ConditionsAcceptance()
|
||||||
|
|
||||||
val conditionsAccepted: Boolean
|
val conditionsAccepted: Boolean
|
||||||
|
@ -3801,7 +3859,7 @@ data class ServerOperator(
|
||||||
tradeName = "SimpleX Chat",
|
tradeName = "SimpleX Chat",
|
||||||
legalName = "SimpleX Chat Ltd",
|
legalName = "SimpleX Chat Ltd",
|
||||||
serverDomains = listOf("simplex.im"),
|
serverDomains = listOf("simplex.im"),
|
||||||
conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null),
|
conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null, autoAccepted = false),
|
||||||
enabled = true,
|
enabled = true,
|
||||||
smpRoles = ServerRoles(storage = true, proxy = true),
|
smpRoles = ServerRoles(storage = true, proxy = true),
|
||||||
xftpRoles = ServerRoles(storage = true, proxy = true)
|
xftpRoles = ServerRoles(storage = true, proxy = true)
|
||||||
|
@ -3883,7 +3941,7 @@ data class UserOperatorServers(
|
||||||
tradeName = "",
|
tradeName = "",
|
||||||
legalName = null,
|
legalName = null,
|
||||||
serverDomains = emptyList(),
|
serverDomains = emptyList(),
|
||||||
conditionsAcceptance = ConditionsAcceptance.Accepted(null),
|
conditionsAcceptance = ConditionsAcceptance.Accepted(null, autoAccepted = false),
|
||||||
enabled = false,
|
enabled = false,
|
||||||
smpRoles = ServerRoles(storage = true, proxy = true),
|
smpRoles = ServerRoles(storage = true, proxy = true),
|
||||||
xftpRoles = ServerRoles(storage = true, proxy = true)
|
xftpRoles = ServerRoles(storage = true, proxy = true)
|
||||||
|
@ -5390,6 +5448,7 @@ sealed class CR {
|
||||||
@Serializable @SerialName("chatStopped") class ChatStopped: CR()
|
@Serializable @SerialName("chatStopped") class ChatStopped: CR()
|
||||||
@Serializable @SerialName("apiChats") class ApiChats(val user: UserRef, val chats: List<Chat>): CR()
|
@Serializable @SerialName("apiChats") class ApiChats(val user: UserRef, val chats: List<Chat>): CR()
|
||||||
@Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat, val navInfo: NavigationInfo = NavigationInfo()): CR()
|
@Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat, val navInfo: NavigationInfo = NavigationInfo()): CR()
|
||||||
|
@Serializable @SerialName("chatTags") class ChatTags(val user: UserRef, val userTags: List<ChatTag>): CR()
|
||||||
@Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR()
|
@Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR()
|
||||||
@Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR()
|
@Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR()
|
||||||
@Serializable @SerialName("serverOperatorConditions") class ServerOperatorConditions(val conditions: ServerOperatorConditionsDetail): CR()
|
@Serializable @SerialName("serverOperatorConditions") class ServerOperatorConditions(val conditions: ServerOperatorConditionsDetail): CR()
|
||||||
|
@ -5416,6 +5475,7 @@ sealed class CR {
|
||||||
@Serializable @SerialName("contactCode") class ContactCode(val user: UserRef, val contact: Contact, val connectionCode: String): CR()
|
@Serializable @SerialName("contactCode") class ContactCode(val user: UserRef, val contact: Contact, val connectionCode: String): CR()
|
||||||
@Serializable @SerialName("groupMemberCode") class GroupMemberCode(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionCode: String): CR()
|
@Serializable @SerialName("groupMemberCode") class GroupMemberCode(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionCode: String): CR()
|
||||||
@Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR()
|
@Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR()
|
||||||
|
@Serializable @SerialName("tagsUpdated") class TagsUpdated(val user: UserRef, val userTags: List<ChatTag>, val chatTags: List<Long>): CR()
|
||||||
@Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR()
|
@Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR()
|
||||||
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
|
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
|
||||||
@Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR()
|
@Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR()
|
||||||
|
@ -5574,6 +5634,7 @@ sealed class CR {
|
||||||
is ChatStopped -> "chatStopped"
|
is ChatStopped -> "chatStopped"
|
||||||
is ApiChats -> "apiChats"
|
is ApiChats -> "apiChats"
|
||||||
is ApiChat -> "apiChat"
|
is ApiChat -> "apiChat"
|
||||||
|
is ChatTags -> "chatTags"
|
||||||
is ApiChatItemInfo -> "chatItemInfo"
|
is ApiChatItemInfo -> "chatItemInfo"
|
||||||
is ServerTestResult -> "serverTestResult"
|
is ServerTestResult -> "serverTestResult"
|
||||||
is ServerOperatorConditions -> "serverOperatorConditions"
|
is ServerOperatorConditions -> "serverOperatorConditions"
|
||||||
|
@ -5600,6 +5661,7 @@ sealed class CR {
|
||||||
is ContactCode -> "contactCode"
|
is ContactCode -> "contactCode"
|
||||||
is GroupMemberCode -> "groupMemberCode"
|
is GroupMemberCode -> "groupMemberCode"
|
||||||
is ConnectionVerified -> "connectionVerified"
|
is ConnectionVerified -> "connectionVerified"
|
||||||
|
is TagsUpdated -> "tagsUpdated"
|
||||||
is Invitation -> "invitation"
|
is Invitation -> "invitation"
|
||||||
is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated"
|
is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated"
|
||||||
is ConnectionUserChanged -> "ConnectionUserChanged"
|
is ConnectionUserChanged -> "ConnectionUserChanged"
|
||||||
|
@ -5748,6 +5810,7 @@ sealed class CR {
|
||||||
is ChatStopped -> noDetails()
|
is ChatStopped -> noDetails()
|
||||||
is ApiChats -> withUser(user, json.encodeToString(chats))
|
is ApiChats -> withUser(user, json.encodeToString(chats))
|
||||||
is ApiChat -> withUser(user, "chat: ${json.encodeToString(chat)}\nnavInfo: ${navInfo}")
|
is ApiChat -> withUser(user, "chat: ${json.encodeToString(chat)}\nnavInfo: ${navInfo}")
|
||||||
|
is ChatTags -> withUser(user, "userTags: ${json.encodeToString(userTags)}")
|
||||||
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}")
|
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\n${json.encodeToString(chatItemInfo)}")
|
||||||
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
|
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
|
||||||
is ServerOperatorConditions -> "conditions: ${json.encodeToString(conditions)}"
|
is ServerOperatorConditions -> "conditions: ${json.encodeToString(conditions)}"
|
||||||
|
@ -5774,6 +5837,7 @@ sealed class CR {
|
||||||
is ContactCode -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionCode: $connectionCode")
|
is ContactCode -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionCode: $connectionCode")
|
||||||
is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode")
|
is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode")
|
||||||
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
|
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
|
||||||
|
is TagsUpdated -> withUser(user, "userTags: ${json.encodeToString(userTags)}\nchatTags: ${json.encodeToString(chatTags)}")
|
||||||
is Invitation -> withUser(user, "connReqInvitation: $connReqInvitation\nconnection: $connection")
|
is Invitation -> withUser(user, "connReqInvitation: $connReqInvitation\nconnection: $connection")
|
||||||
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
|
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
|
||||||
is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" )
|
is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" )
|
||||||
|
|
|
@ -697,13 +697,19 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
|
||||||
Icon(painterResource(MR.images.ic_verified_user), null, tint = MaterialTheme.colors.secondary)
|
Icon(painterResource(MR.images.ic_verified_user), null, tint = MaterialTheme.colors.secondary)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
val clipboard = LocalClipboardManager.current
|
||||||
|
val copyNameToClipboard = {
|
||||||
|
clipboard.setText(AnnotatedString(contact.profile.displayName))
|
||||||
|
showToast(generalGetString(MR.strings.copied))
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
inlineContent = inlineContent,
|
inlineContent = inlineContent,
|
||||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 3,
|
maxLines = 3,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||||
)
|
)
|
||||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName) {
|
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName) {
|
||||||
Text(
|
Text(
|
||||||
|
@ -711,7 +717,8 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
|
||||||
color = MaterialTheme.colors.onBackground,
|
color = MaterialTheme.colors.onBackground,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 4,
|
maxLines = 4,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import chat.simplex.common.model.CIDirection.GroupRcv
|
||||||
import chat.simplex.common.model.ChatController.appPrefs
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
import chat.simplex.common.model.ChatModel.activeCall
|
import chat.simplex.common.model.ChatModel.activeCall
|
||||||
import chat.simplex.common.model.ChatModel.controller
|
import chat.simplex.common.model.ChatModel.controller
|
||||||
|
import chat.simplex.common.model.ChatModel.markChatTagRead
|
||||||
import chat.simplex.common.model.ChatModel.withChats
|
import chat.simplex.common.model.ChatModel.withChats
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.call.*
|
import chat.simplex.common.views.call.*
|
||||||
|
@ -2106,6 +2107,7 @@ private fun markUnreadChatAsRead(chatId: String) {
|
||||||
if (success) {
|
if (success) {
|
||||||
withChats {
|
withChats {
|
||||||
replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
||||||
|
markChatTagRead(chat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import SectionSpacer
|
||||||
import SectionTextFooter
|
import SectionTextFooter
|
||||||
import SectionView
|
import SectionView
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.*
|
import androidx.compose.foundation.lazy.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
|
@ -17,6 +18,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
@ -446,12 +449,18 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||||
|
val clipboard = LocalClipboardManager.current
|
||||||
|
val copyNameToClipboard = {
|
||||||
|
clipboard.setText(AnnotatedString(cInfo.displayName))
|
||||||
|
showToast(generalGetString(MR.strings.copied))
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||||
color = MaterialTheme.colors.onBackground,
|
color = MaterialTheme.colors.onBackground,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 4,
|
maxLines = 4,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||||
)
|
)
|
||||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) {
|
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) {
|
||||||
Text(
|
Text(
|
||||||
|
@ -459,7 +468,8 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
|
||||||
color = MaterialTheme.colors.onBackground,
|
color = MaterialTheme.colors.onBackground,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 8,
|
maxLines = 8,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import SectionSpacer
|
||||||
import SectionTextFooter
|
import SectionTextFooter
|
||||||
import SectionView
|
import SectionView
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.text.InlineTextContent
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
import androidx.compose.foundation.text.appendInlineContent
|
import androidx.compose.foundation.text.appendInlineContent
|
||||||
|
@ -537,13 +538,19 @@ fun GroupMemberInfoHeader(member: GroupMember) {
|
||||||
Icon(painterResource(MR.images.ic_verified_user), null, tint = MaterialTheme.colors.secondary)
|
Icon(painterResource(MR.images.ic_verified_user), null, tint = MaterialTheme.colors.secondary)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
val clipboard = LocalClipboardManager.current
|
||||||
|
val copyNameToClipboard = {
|
||||||
|
clipboard.setText(AnnotatedString(member.displayName))
|
||||||
|
showToast(generalGetString(MR.strings.copied))
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
inlineContent = inlineContent,
|
inlineContent = inlineContent,
|
||||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 3,
|
maxLines = 3,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||||
)
|
)
|
||||||
if (member.fullName != "" && member.fullName != member.displayName) {
|
if (member.fullName != "" && member.fullName != member.displayName) {
|
||||||
Text(
|
Text(
|
||||||
|
@ -551,7 +558,8 @@ fun GroupMemberInfoHeader(member: GroupMember) {
|
||||||
color = MaterialTheme.colors.onBackground,
|
color = MaterialTheme.colors.onBackground,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 4,
|
maxLines = 4,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
package chat.simplex.common.views.chat.item
|
package chat.simplex.common.views.chat.item
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CornerSize
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
@ -184,14 +184,26 @@ fun CIFileView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val showOpenSaveMenu = rememberSaveable(file?.fileId) { mutableStateOf(false) }
|
||||||
|
val ext = file?.fileSource?.filePath?.substringAfterLast(".")?.takeIf { it.isNotBlank() }
|
||||||
|
val loadedFilePath = if (appPlatform.isAndroid && file?.fileSource != null) getLoadedFilePath(file) else null
|
||||||
|
if (loadedFilePath != null && file?.fileSource != null) {
|
||||||
|
val encrypted = file.fileSource.cryptoArgs != null
|
||||||
|
SaveOrOpenFileMenu(showOpenSaveMenu, encrypted, ext, File(loadedFilePath).toURI(), file.fileSource, saveFile = { fileAction() })
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = { fileAction() },
|
onClick = {
|
||||||
|
if (appPlatform.isAndroid && loadedFilePath != null) {
|
||||||
|
showOpenSaveMenu.value = true
|
||||||
|
} else {
|
||||||
|
fileAction()
|
||||||
|
}
|
||||||
|
},
|
||||||
onLongClick = { showMenu.value = true }
|
onLongClick = { showMenu.value = true }
|
||||||
)
|
)
|
||||||
.padding(if (smallView) PaddingValues() else PaddingValues(top = 4.sp.toDp(), bottom = 6.sp.toDp(), start = 6.sp.toDp(), end = 12.sp.toDp())),
|
.padding(if (smallView) PaddingValues() else PaddingValues(top = 4.sp.toDp(), bottom = 6.sp.toDp(), start = 6.sp.toDp(), end = 12.sp.toDp())),
|
||||||
//Modifier.clickable(enabled = file?.fileSource != null) { if (file?.fileSource != null && getLoadedFilePath(file) != null) openFile(file.fileSource) }.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp),
|
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.Bottom,
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.sp.toDp())
|
horizontalArrangement = Arrangement.spacedBy(2.sp.toDp())
|
||||||
) {
|
) {
|
||||||
|
@ -223,6 +235,16 @@ fun CIFileView(
|
||||||
|
|
||||||
fun fileSizeValid(file: CIFile): Boolean = file.fileSize <= getMaxFileSize(file.fileProtocol)
|
fun fileSizeValid(file: CIFile): Boolean = file.fileSize <= getMaxFileSize(file.fileProtocol)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
expect fun SaveOrOpenFileMenu(
|
||||||
|
showMenu: MutableState<Boolean>,
|
||||||
|
encrypted: Boolean,
|
||||||
|
ext: String?,
|
||||||
|
encryptedUri: URI,
|
||||||
|
fileSource: CryptoFile,
|
||||||
|
saveFile: () -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher =
|
fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher =
|
||||||
rememberFileChooserLauncher(false, ciFile) { to: URI? ->
|
rememberFileChooserLauncher(false, ciFile) { to: URI? ->
|
||||||
|
|
|
@ -867,6 +867,32 @@ fun ItemAction(text: String, icon: Painter, color: Color = Color.Unspecified, on
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ItemAction(text: String, icon: ImageBitmap, textColor: Color = Color.Unspecified, iconColor: Color = Color.Unspecified, onClick: () -> Unit) {
|
||||||
|
val finalColor = if (textColor == Color.Unspecified) {
|
||||||
|
MenuTextColor
|
||||||
|
} else textColor
|
||||||
|
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1F)
|
||||||
|
.padding(end = 15.dp),
|
||||||
|
color = finalColor,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
if (iconColor == Color.Unspecified) {
|
||||||
|
Image(icon, text, Modifier.size(22.dp))
|
||||||
|
} else {
|
||||||
|
Icon(icon, text, Modifier.size(22.dp), tint = iconColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ItemAction(
|
fun ItemAction(
|
||||||
text: String,
|
text: String,
|
||||||
|
|
|
@ -4,28 +4,34 @@ import SectionItemView
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material.MaterialTheme.colors
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.ui.*
|
||||||
|
import androidx.compose.ui.focus.*
|
||||||
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatModel.markChatTagRead
|
||||||
|
import chat.simplex.common.model.ChatModel.updateChatTagRead
|
||||||
import chat.simplex.common.model.ChatModel.withChats
|
import chat.simplex.common.model.ChatModel.withChats
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.chat.*
|
import chat.simplex.common.views.chat.*
|
||||||
import chat.simplex.common.views.chat.group.deleteGroupDialog
|
import chat.simplex.common.views.chat.group.*
|
||||||
import chat.simplex.common.views.chat.group.leaveGroupDialog
|
|
||||||
import chat.simplex.common.views.chat.item.ItemAction
|
import chat.simplex.common.views.chat.item.ItemAction
|
||||||
import chat.simplex.common.views.contacts.onRequestAccepted
|
import chat.simplex.common.views.contacts.onRequestAccepted
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
|
@ -33,7 +39,6 @@ import chat.simplex.common.views.newchat.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
|
||||||
|
@ -252,6 +257,7 @@ fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMen
|
||||||
}
|
}
|
||||||
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
|
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
|
||||||
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
|
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
|
||||||
|
TagListAction(chat, showMenu)
|
||||||
ClearChatAction(chat, showMenu)
|
ClearChatAction(chat, showMenu)
|
||||||
}
|
}
|
||||||
DeleteContactAction(chat, chatModel, showMenu)
|
DeleteContactAction(chat, chatModel, showMenu)
|
||||||
|
@ -291,6 +297,7 @@ fun GroupMenuItems(
|
||||||
}
|
}
|
||||||
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
|
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
|
||||||
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
|
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
|
||||||
|
TagListAction(chat, showMenu)
|
||||||
ClearChatAction(chat, showMenu)
|
ClearChatAction(chat, showMenu)
|
||||||
if (groupInfo.membership.memberCurrent) {
|
if (groupInfo.membership.memberCurrent) {
|
||||||
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
|
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
|
||||||
|
@ -337,6 +344,28 @@ fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableStat
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TagListAction(
|
||||||
|
chat: Chat,
|
||||||
|
showMenu: MutableState<Boolean>
|
||||||
|
) {
|
||||||
|
val userTags = remember { chatModel.userTags }
|
||||||
|
ItemAction(
|
||||||
|
stringResource(MR.strings.list_menu),
|
||||||
|
painterResource(MR.images.ic_label),
|
||||||
|
onClick = {
|
||||||
|
ModalManager.start.showModalCloseable { close ->
|
||||||
|
if (userTags.value.isEmpty()) {
|
||||||
|
TagListEditor(rhId = chat.remoteHostId, chat = chat, close = close)
|
||||||
|
} else {
|
||||||
|
TagListView(rhId = chat.remoteHostId, chat = chat, close = close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showMenu.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolean, showMenu: MutableState<Boolean>) {
|
fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolean, showMenu: MutableState<Boolean>) {
|
||||||
ItemAction(
|
ItemAction(
|
||||||
|
@ -557,6 +586,7 @@ fun markChatRead(c: Chat, chatModel: ChatModel) {
|
||||||
if (success) {
|
if (success) {
|
||||||
withChats {
|
withChats {
|
||||||
replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
||||||
|
markChatTagRead(chat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -568,6 +598,7 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
|
||||||
if (chat.chatStats.unreadChat) return
|
if (chat.chatStats.unreadChat) return
|
||||||
|
|
||||||
withApi {
|
withApi {
|
||||||
|
val wasUnread = chat.unreadTag
|
||||||
val success = chatModel.controller.apiChatUnread(
|
val success = chatModel.controller.apiChatUnread(
|
||||||
chat.remoteHostId,
|
chat.remoteHostId,
|
||||||
chat.chatInfo.chatType,
|
chat.chatInfo.chatType,
|
||||||
|
@ -577,6 +608,7 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
|
||||||
if (success) {
|
if (success) {
|
||||||
withChats {
|
withChats {
|
||||||
replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
|
replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
|
||||||
|
updateChatTagRead(chat, wasUnread)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -826,12 +858,20 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
if (res && newChatInfo != null) {
|
if (res && newChatInfo != null) {
|
||||||
|
val chat = chatModel.getChat(chatInfo.id)
|
||||||
|
val wasUnread = chat?.unreadTag ?: false
|
||||||
|
val wasFavorite = chatInfo.chatSettings?.favorite ?: false
|
||||||
|
chatModel.updateChatFavorite(favorite = chatSettings.favorite, wasFavorite)
|
||||||
withChats {
|
withChats {
|
||||||
updateChatInfo(remoteHostId, newChatInfo)
|
updateChatInfo(remoteHostId, newChatInfo)
|
||||||
}
|
}
|
||||||
if (chatSettings.enableNtfs != MsgFilter.All) {
|
if (chatSettings.enableNtfs != MsgFilter.All) {
|
||||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||||
}
|
}
|
||||||
|
val updatedChat = chatModel.getChat(chatInfo.id)
|
||||||
|
if (updatedChat != null) {
|
||||||
|
chatModel.updateChatTagRead(updatedChat, wasUnread)
|
||||||
|
}
|
||||||
val current = currentState?.value
|
val current = currentState?.value
|
||||||
if (current != null) {
|
if (current != null) {
|
||||||
currentState.value = !current
|
currentState.value = !current
|
||||||
|
|
|
@ -16,11 +16,13 @@ import androidx.compose.ui.focus.*
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.platform.*
|
import androidx.compose.ui.platform.*
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.text.*
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.*
|
import androidx.compose.ui.unit.*
|
||||||
import chat.simplex.common.AppLock
|
import chat.simplex.common.AppLock
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
@ -31,22 +33,30 @@ import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.views.call.Call
|
import chat.simplex.common.views.call.Call
|
||||||
import chat.simplex.common.views.chat.item.CIFileViewScope
|
import chat.simplex.common.views.chat.item.*
|
||||||
import chat.simplex.common.views.chat.topPaddingToContent
|
import chat.simplex.common.views.chat.topPaddingToContent
|
||||||
import chat.simplex.common.views.mkValidName
|
|
||||||
import chat.simplex.common.views.newchat.*
|
import chat.simplex.common.views.newchat.*
|
||||||
import chat.simplex.common.views.onboarding.*
|
import chat.simplex.common.views.onboarding.*
|
||||||
import chat.simplex.common.views.showInvalidNameAlert
|
|
||||||
import chat.simplex.common.views.usersettings.*
|
import chat.simplex.common.views.usersettings.*
|
||||||
import chat.simplex.common.views.usersettings.networkAndServers.ConditionsLinkButton
|
import chat.simplex.common.views.usersettings.networkAndServers.ConditionsLinkButton
|
||||||
import chat.simplex.common.views.usersettings.networkAndServers.UsageConditionsView
|
import chat.simplex.common.views.usersettings.networkAndServers.UsageConditionsView
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.ImageResource
|
||||||
|
import dev.icerock.moko.resources.StringResource
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
enum class PresetTagKind { FAVORITES, CONTACTS, GROUPS, BUSINESS }
|
||||||
|
|
||||||
|
sealed class ActiveFilter {
|
||||||
|
data class PresetTag(val tag: PresetTagKind) : ActiveFilter()
|
||||||
|
data class UserTag(val tag: ChatTag) : ActiveFilter()
|
||||||
|
data object Unread: ActiveFilter()
|
||||||
|
}
|
||||||
|
|
||||||
private fun showNewChatSheet(oneHandUI: State<Boolean>) {
|
private fun showNewChatSheet(oneHandUI: State<Boolean>) {
|
||||||
ModalManager.start.closeModals()
|
ModalManager.start.closeModals()
|
||||||
ModalManager.end.closeModals()
|
ModalManager.end.closeModals()
|
||||||
|
@ -187,6 +197,12 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (appPlatform.isAndroid) {
|
if (appPlatform.isAndroid) {
|
||||||
|
val wasAllowedToSetupNotifications = rememberSaveable { mutableStateOf(false) }
|
||||||
|
val canEnableNotifications = remember { derivedStateOf { chatModel.chatRunning.value == true } }
|
||||||
|
if (wasAllowedToSetupNotifications.value || canEnableNotifications.value) {
|
||||||
|
SetNotificationsModeAdditions()
|
||||||
|
LaunchedEffect(Unit) { wasAllowedToSetupNotifications.value = true }
|
||||||
|
}
|
||||||
tryOrShowError("UserPicker", error = {}) {
|
tryOrShowError("UserPicker", error = {}) {
|
||||||
UserPicker(
|
UserPicker(
|
||||||
chatModel = chatModel,
|
chatModel = chatModel,
|
||||||
|
@ -557,17 +573,24 @@ private fun BoxScope.unreadBadge(text: String? = "") {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ToggleFilterEnabledButton() {
|
private fun ToggleFilterEnabledButton() {
|
||||||
val pref = remember { ChatController.appPrefs.showUnreadAndFavorites }
|
val showUnread = remember { chatModel.activeChatTagFilter }.value == ActiveFilter.Unread
|
||||||
IconButton(onClick = { pref.set(!pref.get()) }) {
|
|
||||||
|
IconButton(onClick = {
|
||||||
|
if (showUnread) {
|
||||||
|
chatModel.activeChatTagFilter.value = null
|
||||||
|
} else {
|
||||||
|
chatModel.activeChatTagFilter.value = ActiveFilter.Unread
|
||||||
|
}
|
||||||
|
}) {
|
||||||
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
|
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(MR.images.ic_filter_list),
|
painterResource(MR.images.ic_filter_list),
|
||||||
null,
|
null,
|
||||||
tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary,
|
tint = if (showUnread) MaterialTheme.colors.background else MaterialTheme.colors.secondary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(3.dp)
|
.padding(3.dp)
|
||||||
.background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
.background(color = if (showUnread) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||||
.border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
.border(width = 1.dp, color = if (showUnread) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50))
|
||||||
.padding(3.dp)
|
.padding(3.dp)
|
||||||
.size(sp16)
|
.size(sp16)
|
||||||
)
|
)
|
||||||
|
@ -731,6 +754,7 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state }
|
val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state }
|
||||||
val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state }
|
val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state }
|
||||||
|
val activeFilter = remember { chatModel.activeChatTagFilter }
|
||||||
|
|
||||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||||
val currentIndex = listState.firstVisibleItemIndex
|
val currentIndex = listState.firstVisibleItemIndex
|
||||||
|
@ -753,14 +777,13 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||||
}
|
}
|
||||||
val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value
|
|
||||||
val allChats = remember { chatModel.chats }
|
val allChats = remember { chatModel.chats }
|
||||||
// In some not always reproducible situations this code produce IndexOutOfBoundsException on Compose's side
|
// In some not always reproducible situations this code produce IndexOutOfBoundsException on Compose's side
|
||||||
// which is related to [derivedStateOf]. Using safe alternative instead
|
// which is related to [derivedStateOf]. Using safe alternative instead
|
||||||
// val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } }
|
// val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } }
|
||||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||||
val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList())
|
val chats = filteredChats(searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList(), activeFilter.value)
|
||||||
val topPaddingToContent = topPaddingToContent(false)
|
val topPaddingToContent = topPaddingToContent(false)
|
||||||
val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent
|
val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent
|
||||||
LazyColumnWithScrollBar(
|
LazyColumnWithScrollBar(
|
||||||
|
@ -791,11 +814,15 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||||
) {
|
) {
|
||||||
if (oneHandUI.value) {
|
if (oneHandUI.value) {
|
||||||
Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) {
|
Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) {
|
||||||
|
Divider()
|
||||||
|
TagsView()
|
||||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
||||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
|
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
||||||
|
TagsView()
|
||||||
|
Divider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -815,8 +842,8 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) {
|
if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) {
|
||||||
Box(Modifier.fillMaxSize().imePadding(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxSize().imePadding().padding(horizontal = DEFAULT_PADDING), contentAlignment = Alignment.Center) {
|
||||||
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
|
NoChatsView(searchText = searchText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oneHandUI.value) {
|
if (oneHandUI.value) {
|
||||||
|
@ -839,6 +866,41 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(activeFilter.value) {
|
||||||
|
searchText.value = TextFieldValue("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NoChatsView(searchText: MutableState<TextFieldValue>) {
|
||||||
|
val activeFilter = remember { chatModel.activeChatTagFilter }.value
|
||||||
|
|
||||||
|
if (searchText.value.text.isBlank()) {
|
||||||
|
when (activeFilter) {
|
||||||
|
is ActiveFilter.PresetTag -> Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) // this should not happen
|
||||||
|
is ActiveFilter.UserTag -> Text(String.format(generalGetString(MR.strings.no_chats_in_list), activeFilter.tag.chatTagText), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
|
||||||
|
is ActiveFilter.Unread -> {
|
||||||
|
Row(
|
||||||
|
Modifier.clip(shape = CircleShape).clickable { chatModel.activeChatTagFilter.value = null }.padding(DEFAULT_PADDING_HALF),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(MR.images.ic_filter_list),
|
||||||
|
null,
|
||||||
|
tint = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
Text(generalGetString(MR.strings.no_unread_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null -> {
|
||||||
|
Text(generalGetString(MR.strings.no_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(generalGetString(MR.strings.no_chats_found), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -860,31 +922,301 @@ private fun ChatListFeatureCards() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val TAG_MIN_HEIGHT = 35.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TagsView() {
|
||||||
|
val userTags = remember { chatModel.userTags }
|
||||||
|
val presetTags = remember { chatModel.presetTags }
|
||||||
|
val activeFilter = remember { chatModel.activeChatTagFilter }
|
||||||
|
val unreadTags = remember { chatModel.unreadTags }
|
||||||
|
val rhId = chatModel.remoteHostId()
|
||||||
|
|
||||||
|
fun showTagList() {
|
||||||
|
ModalManager.start.showCustomModal { close ->
|
||||||
|
val editMode = remember { stateGetOrPut("editMode") { false } }
|
||||||
|
ModalView(close, showClose = true, endButtons = {
|
||||||
|
TextButton(onClick = { editMode.value = !editMode.value }, modifier = Modifier.clip(shape = CircleShape)) {
|
||||||
|
Text(stringResource(if (editMode.value) MR.strings.cancel_verb else MR.strings.edit_verb))
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
TagListView(rhId = rhId, close = close, editMode = editMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier)
|
||||||
|
|
||||||
|
TagsRow {
|
||||||
|
if (presetTags.size > 1) {
|
||||||
|
if (presetTags.size + userTags.value.size <= 3) {
|
||||||
|
PresetTagKind.entries.filter { t -> (presetTags[t] ?: 0) > 0 }.forEach { tag ->
|
||||||
|
ExpandedTagFilterView(tag)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CollapsedTagsFilterView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userTags.value.forEach { tag ->
|
||||||
|
val current = when (val af = activeFilter.value) {
|
||||||
|
is ActiveFilter.UserTag -> af.tag == tag
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
Row(
|
||||||
|
rowSizeModifier
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
if (chatModel.activeChatTagFilter.value == ActiveFilter.UserTag(tag)) {
|
||||||
|
chatModel.activeChatTagFilter.value = null
|
||||||
|
} else {
|
||||||
|
chatModel.activeChatTagFilter.value = ActiveFilter.UserTag(tag)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = { showTagList() },
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = LocalIndication.current
|
||||||
|
)
|
||||||
|
.onRightClick { showTagList() }
|
||||||
|
.padding(4.dp),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (tag.chatTagEmoji != null) {
|
||||||
|
ReactionIcon(tag.chatTagEmoji, fontSize = 14.sp)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
painterResource(if (current) MR.images.ic_label_filled else MR.images.ic_label),
|
||||||
|
null,
|
||||||
|
Modifier.size(18.sp.toDp()),
|
||||||
|
tint = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Box {
|
||||||
|
val badgeText = if ((unreadTags[tag.chatTagId] ?: 0) > 0) " ●" else ""
|
||||||
|
val invisibleText = buildAnnotatedString {
|
||||||
|
append(tag.chatTagText)
|
||||||
|
withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.SemiBold)) {
|
||||||
|
append(badgeText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = invisibleText,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = Color.Transparent,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
// Visible text with styles
|
||||||
|
val visibleText = buildAnnotatedString {
|
||||||
|
append(tag.chatTagText)
|
||||||
|
withStyle(SpanStyle(fontSize = 12.5.sp, color = MaterialTheme.colors.primary)) {
|
||||||
|
append(badgeText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = visibleText,
|
||||||
|
fontWeight = if (current) FontWeight.Medium else FontWeight.Normal,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
color = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val plusClickModifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
ModalManager.start.showModalCloseable { close ->
|
||||||
|
TagListEditor(rhId = rhId, close = close)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userTags.value.isEmpty()) {
|
||||||
|
Row(rowSizeModifier.clip(shape = CircleShape).then(plusClickModifier).padding(start = 2.dp, top = 4.dp, end = 6.dp, bottom = 4.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), Modifier.size(18.sp.toDp()), tint = MaterialTheme.colors.secondary)
|
||||||
|
Spacer(Modifier.width(2.dp))
|
||||||
|
Text(stringResource(MR.strings.chat_list_add_list), color = MaterialTheme.colors.secondary, fontSize = 15.sp)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(rowSizeModifier, contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), Modifier.clip(shape = CircleShape).then(plusClickModifier).padding(2.dp), tint = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun TagsRow(content: @Composable() (() -> Unit)) {
|
||||||
|
if (appPlatform.isAndroid) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 14.dp)
|
||||||
|
.horizontalScroll(rememberScrollState()),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
FlowRow(modifier = Modifier.padding(horizontal = 14.dp)) { content() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ExpandedTagFilterView(tag: PresetTagKind) {
|
||||||
|
val activeFilter = remember { chatModel.activeChatTagFilter }
|
||||||
|
val active = when (val af = activeFilter.value) {
|
||||||
|
is ActiveFilter.PresetTag -> af.tag == tag
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier)
|
||||||
|
val (icon, text) = presetTagLabel(tag, active)
|
||||||
|
val color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = rowSizeModifier
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
.clickable {
|
||||||
|
if (activeFilter.value == ActiveFilter.PresetTag(tag)) {
|
||||||
|
chatModel.activeChatTagFilter.value = null
|
||||||
|
} else {
|
||||||
|
chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(horizontal = 5.dp, vertical = 4.dp)
|
||||||
|
,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(icon),
|
||||||
|
stringResource(text),
|
||||||
|
Modifier.size(18.sp.toDp()),
|
||||||
|
tint = color
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Box {
|
||||||
|
Text(
|
||||||
|
stringResource(text),
|
||||||
|
color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||||
|
fontWeight = if (active) FontWeight.Medium else FontWeight.Normal,
|
||||||
|
fontSize = 15.sp
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(text),
|
||||||
|
color = Color.Transparent,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 15.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CollapsedTagsFilterView() {
|
||||||
|
val activeFilter = remember { chatModel.activeChatTagFilter }
|
||||||
|
val presetTags = remember { chatModel.presetTags }
|
||||||
|
val showMenu = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val selectedPresetTag = when (val af = activeFilter.value) {
|
||||||
|
is ActiveFilter.PresetTag -> af.tag
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier)
|
||||||
|
Box(rowSizeModifier
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.clip(shape = CircleShape)
|
||||||
|
.size(30.sp.toDp())
|
||||||
|
.clickable { showMenu.value = true },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (selectedPresetTag != null) {
|
||||||
|
val (icon, text) = presetTagLabel(selectedPresetTag, true)
|
||||||
|
Icon(
|
||||||
|
painterResource(icon),
|
||||||
|
stringResource(text),
|
||||||
|
Modifier.size(18.sp.toDp()),
|
||||||
|
tint = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
painterResource(MR.images.ic_menu),
|
||||||
|
stringResource(MR.strings.chat_list_all),
|
||||||
|
tint = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultDropdownMenu(showMenu = showMenu) {
|
||||||
|
if (selectedPresetTag != null) {
|
||||||
|
ItemAction(
|
||||||
|
stringResource(MR.strings.chat_list_all),
|
||||||
|
painterResource(MR.images.ic_menu),
|
||||||
|
onClick = {
|
||||||
|
chatModel.activeChatTagFilter.value = null
|
||||||
|
showMenu.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PresetTagKind.entries.forEach { tag ->
|
||||||
|
if ((presetTags[tag] ?: 0) > 0) {
|
||||||
|
ItemPresetFilterAction(tag, tag == selectedPresetTag, showMenu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ItemPresetFilterAction(
|
||||||
|
presetTag: PresetTagKind,
|
||||||
|
active: Boolean,
|
||||||
|
showMenu: MutableState<Boolean>
|
||||||
|
) {
|
||||||
|
val (icon, text) = presetTagLabel(presetTag, active)
|
||||||
|
ItemAction(
|
||||||
|
stringResource(text),
|
||||||
|
painterResource(icon),
|
||||||
|
onClick = {
|
||||||
|
chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(presetTag)
|
||||||
|
showMenu.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun filteredChats(
|
fun filteredChats(
|
||||||
showUnreadAndFavorites: Boolean,
|
|
||||||
searchShowingSimplexLink: State<Boolean>,
|
searchShowingSimplexLink: State<Boolean>,
|
||||||
searchChatFilteredBySimplexLink: State<String?>,
|
searchChatFilteredBySimplexLink: State<String?>,
|
||||||
searchText: String,
|
searchText: String,
|
||||||
chats: List<Chat>
|
chats: List<Chat>,
|
||||||
|
activeFilter: ActiveFilter? = null,
|
||||||
): List<Chat> {
|
): List<Chat> {
|
||||||
val linkChatId = searchChatFilteredBySimplexLink.value
|
val linkChatId = searchChatFilteredBySimplexLink.value
|
||||||
return if (linkChatId != null) {
|
return if (linkChatId != null) {
|
||||||
chats.filter { it.id == linkChatId }
|
chats.filter { it.id == linkChatId }
|
||||||
} else {
|
} else {
|
||||||
val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase()
|
val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase()
|
||||||
if (s.isEmpty() && !showUnreadAndFavorites)
|
if (s.isEmpty())
|
||||||
chats.filter { chat -> !chat.chatInfo.chatDeleted && chatContactType(chat) != ContactType.CARD }
|
chats.filter { chat -> !chat.chatInfo.chatDeleted && chatContactType(chat) != ContactType.CARD && filtered(chat, activeFilter) }
|
||||||
else {
|
else {
|
||||||
chats.filter { chat ->
|
chats.filter { chat ->
|
||||||
when (val cInfo = chat.chatInfo) {
|
when (val cInfo = chat.chatInfo) {
|
||||||
is ChatInfo.Direct -> chatContactType(chat) != ContactType.CARD && !chat.chatInfo.chatDeleted && (
|
is ChatInfo.Direct -> chatContactType(chat) != ContactType.CARD && !chat.chatInfo.chatDeleted && (
|
||||||
if (s.isEmpty()) {
|
if (s.isEmpty()) {
|
||||||
chat.id == chatModel.chatId.value || filtered(chat)
|
chat.id == chatModel.chatId.value || filtered(chat, activeFilter)
|
||||||
} else {
|
} else {
|
||||||
cInfo.anyNameContains(s)
|
cInfo.anyNameContains(s)
|
||||||
})
|
})
|
||||||
is ChatInfo.Group -> if (s.isEmpty()) {
|
is ChatInfo.Group -> if (s.isEmpty()) {
|
||||||
chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
|
chat.id == chatModel.chatId.value || filtered(chat, activeFilter) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
|
||||||
} else {
|
} else {
|
||||||
cInfo.anyNameContains(s)
|
cInfo.anyNameContains(s)
|
||||||
}
|
}
|
||||||
|
@ -898,10 +1230,41 @@ fun filteredChats(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun filtered(chat: Chat): Boolean =
|
private fun filtered(chat: Chat, activeFilter: ActiveFilter?): Boolean =
|
||||||
(chat.chatInfo.chatSettings?.favorite ?: false) ||
|
when (activeFilter) {
|
||||||
chat.chatStats.unreadChat ||
|
is ActiveFilter.PresetTag -> presetTagMatchesChat(activeFilter.tag, chat.chatInfo)
|
||||||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
is ActiveFilter.UserTag -> chat.chatInfo.chatTags?.contains(activeFilter.tag.chatTagId) ?: false
|
||||||
|
is ActiveFilter.Unread -> chat.chatStats.unreadChat || chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo): Boolean =
|
||||||
|
when (tag) {
|
||||||
|
PresetTagKind.FAVORITES -> chatInfo.chatSettings?.favorite == true
|
||||||
|
PresetTagKind.CONTACTS -> when (chatInfo) {
|
||||||
|
is ChatInfo.Direct -> !(chatInfo.contact.activeConn == null && chatInfo.contact.profile.contactLink != null && chatInfo.contact.active) && !chatInfo.contact.chatDeleted
|
||||||
|
is ChatInfo.ContactRequest -> true
|
||||||
|
is ChatInfo.ContactConnection -> true
|
||||||
|
is ChatInfo.Group -> chatInfo.groupInfo.businessChat?.chatType == BusinessChatType.Customer
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
PresetTagKind.GROUPS -> when (chatInfo) {
|
||||||
|
is ChatInfo.Group -> chatInfo.groupInfo.businessChat == null
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
PresetTagKind.BUSINESS -> when (chatInfo) {
|
||||||
|
is ChatInfo.Group -> chatInfo.groupInfo.businessChat?.chatType == BusinessChatType.Business
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Pair<ImageResource, StringResource> =
|
||||||
|
when (tag) {
|
||||||
|
PresetTagKind.FAVORITES -> (if (active) MR.images.ic_star_filled else MR.images.ic_star) to MR.strings.chat_list_favorites
|
||||||
|
PresetTagKind.CONTACTS -> (if (active) MR.images.ic_person_filled else MR.images.ic_person) to MR.strings.chat_list_contacts
|
||||||
|
PresetTagKind.GROUPS -> (if (active) MR.images.ic_group_filled else MR.images.ic_group) to MR.strings.chat_list_groups
|
||||||
|
PresetTagKind.BUSINESS -> (if (active) MR.images.ic_work_filled else MR.images.ic_work) to MR.strings.chat_list_businesses
|
||||||
|
}
|
||||||
|
|
||||||
fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) {
|
fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) {
|
||||||
scope.launch { try { listState.animateScrollToItem(0) } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) } }
|
scope.launch { try { listState.animateScrollToItem(0) } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) } }
|
||||||
|
|
|
@ -191,7 +191,7 @@ private fun ShareList(
|
||||||
val chats by remember(search) {
|
val chats by remember(search) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready }.sortedByDescending { it.chatInfo is ChatInfo.Local }
|
val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready }.sortedByDescending { it.chatInfo is ChatInfo.Local }
|
||||||
filteredChats(false, mutableStateOf(false), mutableStateOf(null), search, sorted)
|
filteredChats(mutableStateOf(false), mutableStateOf(null), search, sorted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val topPaddingToContent = topPaddingToContent(false)
|
val topPaddingToContent = topPaddingToContent(false)
|
||||||
|
|
|
@ -0,0 +1,500 @@
|
||||||
|
package chat.simplex.common.views.chatlist
|
||||||
|
|
||||||
|
import SectionCustomFooter
|
||||||
|
import SectionDivider
|
||||||
|
import SectionItemView
|
||||||
|
import TextIconSpaced
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.MaterialTheme.colors
|
||||||
|
import androidx.compose.material.TextFieldDefaults.indicatorLine
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.*
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatController.apiDeleteChatTag
|
||||||
|
import chat.simplex.common.model.ChatController.apiSetChatTags
|
||||||
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
|
import chat.simplex.common.model.ChatModel.withChats
|
||||||
|
import chat.simplex.common.platform.*
|
||||||
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.views.chat.item.ItemAction
|
||||||
|
import chat.simplex.common.views.chat.item.ReactionIcon
|
||||||
|
import chat.simplex.common.views.chat.topPaddingToContent
|
||||||
|
import chat.simplex.common.views.helpers.*
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: MutableState<Boolean> = remember { mutableStateOf(false) }) {
|
||||||
|
if (remember { editMode }.value) {
|
||||||
|
BackHandler {
|
||||||
|
editMode.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val userTags = remember { chatModel.userTags }
|
||||||
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
|
val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState()
|
||||||
|
val saving = remember { mutableStateOf(false) }
|
||||||
|
val chatTagIds = derivedStateOf { chat?.chatInfo?.chatTags ?: emptyList() }
|
||||||
|
|
||||||
|
fun reorderTags(tagIds: List<Long>) {
|
||||||
|
saving.value = true
|
||||||
|
withBGApi {
|
||||||
|
try {
|
||||||
|
chatModel.controller.apiReorderChatTags(rhId, tagIds)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(TAG, "ChatListTag reorderTags error: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dragDropState =
|
||||||
|
rememberDragDropState(listState) { fromIndex, toIndex ->
|
||||||
|
userTags.value = userTags.value.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
|
||||||
|
reorderTags(userTags.value.map { it.chatTagId })
|
||||||
|
}
|
||||||
|
val topPaddingToContent = topPaddingToContent(false)
|
||||||
|
|
||||||
|
LazyColumnWithScrollBar(
|
||||||
|
modifier = if (editMode.value) Modifier.dragContainer(dragDropState) else Modifier,
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent,
|
||||||
|
bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp
|
||||||
|
),
|
||||||
|
state = listState,
|
||||||
|
verticalArrangement = if (oneHandUI.value) Arrangement.Bottom else Arrangement.Top,
|
||||||
|
) {
|
||||||
|
@Composable fun CreateList() {
|
||||||
|
SectionItemView({
|
||||||
|
ModalManager.start.showModalCloseable { close ->
|
||||||
|
TagListEditor(rhId = rhId, close = close, chat = chat)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(painterResource(MR.images.ic_add), stringResource(MR.strings.create_list), tint = MaterialTheme.colors.primary)
|
||||||
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
|
Text(stringResource(MR.strings.create_list), color = MaterialTheme.colors.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oneHandUI.value && !editMode.value) {
|
||||||
|
item {
|
||||||
|
CreateList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemsIndexed(userTags.value, key = { _, item -> item.chatTagId }) { index, tag ->
|
||||||
|
DraggableItem(dragDropState, index) { isDragging ->
|
||||||
|
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
elevation = elevation,
|
||||||
|
backgroundColor = if (isDragging) colors.surface else Color.Unspecified
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
val showMenu = remember { mutableStateOf(false) }
|
||||||
|
val selected = chatTagIds.value.contains(tag.chatTagId)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT)
|
||||||
|
.combinedClickable(
|
||||||
|
enabled = !saving.value,
|
||||||
|
onClick = {
|
||||||
|
if (chat == null) {
|
||||||
|
ModalManager.start.showModalCloseable { close ->
|
||||||
|
TagListEditor(
|
||||||
|
rhId = rhId,
|
||||||
|
tagId = tag.chatTagId,
|
||||||
|
close = close,
|
||||||
|
emoji = tag.chatTagEmoji,
|
||||||
|
name = tag.chatTagText,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saving.value = true
|
||||||
|
setTag(rhId = rhId, tagId = if (selected) null else tag.chatTagId, chat = chat, close = {
|
||||||
|
saving.value = false
|
||||||
|
close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = if (editMode.value) null else {
|
||||||
|
{ showMenu.value = true }
|
||||||
|
},
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = LocalIndication.current
|
||||||
|
)
|
||||||
|
.onRightClick { showMenu.value = true }
|
||||||
|
.padding(PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (tag.chatTagEmoji != null) {
|
||||||
|
ReactionIcon(tag.chatTagEmoji, fontSize = 14.sp)
|
||||||
|
} else {
|
||||||
|
Icon(painterResource(MR.images.ic_label), null, Modifier.size(18.sp.toDp()), tint = MaterialTheme.colors.onBackground)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||||
|
Text(
|
||||||
|
tag.chatTagText,
|
||||||
|
color = MenuTextColor,
|
||||||
|
fontWeight = if (selected) FontWeight.Medium else FontWeight.Normal
|
||||||
|
)
|
||||||
|
if (selected) {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||||
|
} else if (editMode.value) {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Icon(painterResource(MR.images.ic_drag_handle), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary)
|
||||||
|
}
|
||||||
|
DefaultDropdownMenu(showMenu, dropdownMenuItems = {
|
||||||
|
EditTagAction(rhId, tag, showMenu)
|
||||||
|
DeleteTagAction(rhId, tag, showMenu, saving)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
SectionDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!oneHandUI.value && !editMode.value) {
|
||||||
|
item {
|
||||||
|
CreateList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ModalData.TagListEditor(
|
||||||
|
rhId: Long?,
|
||||||
|
chat: Chat? = null,
|
||||||
|
tagId: Long? = null,
|
||||||
|
emoji: String? = null,
|
||||||
|
name: String = "",
|
||||||
|
close: () -> Unit
|
||||||
|
) {
|
||||||
|
val userTags = remember { chatModel.userTags }
|
||||||
|
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||||
|
val newEmoji = remember { stateGetOrPutNullable("chatTagEmoji") { emoji } }
|
||||||
|
val newName = remember { stateGetOrPut("chatTagName") { name } }
|
||||||
|
val saving = remember { mutableStateOf<Boolean?>(null) }
|
||||||
|
val trimmedName = remember { derivedStateOf { newName.value.trim() } }
|
||||||
|
val isDuplicateEmojiOrName = remember {
|
||||||
|
derivedStateOf {
|
||||||
|
userTags.value.any { tag ->
|
||||||
|
tag.chatTagId != tagId &&
|
||||||
|
((newEmoji.value != null && tag.chatTagEmoji == newEmoji.value) || tag.chatTagText == trimmedName.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createTag() {
|
||||||
|
saving.value = true
|
||||||
|
withBGApi {
|
||||||
|
try {
|
||||||
|
val updatedTags = chatModel.controller.apiCreateChatTag(rhId, ChatTagData(newEmoji.value, trimmedName.value))
|
||||||
|
if (updatedTags != null) {
|
||||||
|
saving.value = false
|
||||||
|
userTags.value = updatedTags
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
saving.value = null
|
||||||
|
return@withBGApi
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chat != null) {
|
||||||
|
val createdTag = updatedTags.firstOrNull() { it.chatTagText == trimmedName.value && it.chatTagEmoji == newEmoji.value }
|
||||||
|
|
||||||
|
if (createdTag != null) {
|
||||||
|
setTag(rhId, createdTag.chatTagId, chat, close = {
|
||||||
|
saving.value = false
|
||||||
|
close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(TAG, "createChatTag tag error: ${e.message}")
|
||||||
|
saving.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateTag() {
|
||||||
|
saving.value = true
|
||||||
|
withBGApi {
|
||||||
|
try {
|
||||||
|
if (chatModel.controller.apiUpdateChatTag(rhId, tagId!!, ChatTagData(newEmoji.value, trimmedName.value))) {
|
||||||
|
userTags.value = userTags.value.map { tag ->
|
||||||
|
if (tag.chatTagId == tagId) {
|
||||||
|
tag.copy(chatTagEmoji = newEmoji.value, chatTagText = trimmedName.value)
|
||||||
|
} else {
|
||||||
|
tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
saving.value = null
|
||||||
|
return@withBGApi
|
||||||
|
}
|
||||||
|
saving.value = false
|
||||||
|
close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(TAG, "ChatListTagEditor updateChatTag tag error: ${e.message}")
|
||||||
|
saving.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val showError = derivedStateOf { isDuplicateEmojiOrName.value && saving.value != false }
|
||||||
|
|
||||||
|
ColumnWithScrollBar(Modifier.consumeWindowInsets(PaddingValues(bottom = if (oneHandUI.value) WindowInsets.ime.asPaddingValues().calculateBottomPadding().coerceIn(0.dp, WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()) else 0.dp))) {
|
||||||
|
if (oneHandUI.value) {
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
ChatTagInput(newName, showError, newEmoji)
|
||||||
|
val disabled = saving.value == true ||
|
||||||
|
(trimmedName.value == name && newEmoji.value == emoji) ||
|
||||||
|
trimmedName.value.isEmpty() ||
|
||||||
|
isDuplicateEmojiOrName.value
|
||||||
|
|
||||||
|
SectionItemView(click = { if (tagId == null) createTag() else updateTag() }, disabled = disabled) {
|
||||||
|
Text(
|
||||||
|
generalGetString(if (chat != null) MR.strings.add_to_list else if (tagId == null) MR.strings.create_list else MR.strings.save_list),
|
||||||
|
color = if (disabled) colors.secondary else colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val showErrorMessage = isDuplicateEmojiOrName.value && saving.value != false
|
||||||
|
SectionCustomFooter {
|
||||||
|
Row(
|
||||||
|
Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(MR.images.ic_error),
|
||||||
|
contentDescription = stringResource(MR.strings.error),
|
||||||
|
tint = if (showErrorMessage) Color.Red else Color.Transparent,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(19.sp.toDp())
|
||||||
|
.offset(x = 2.sp.toDp())
|
||||||
|
)
|
||||||
|
TextIconSpaced()
|
||||||
|
Text(
|
||||||
|
generalGetString(MR.strings.duplicated_list_error),
|
||||||
|
color = if (showErrorMessage) colors.secondary else Color.Transparent,
|
||||||
|
lineHeight = 18.sp,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DeleteTagAction(rhId: Long?, tag: ChatTag, showMenu: MutableState<Boolean>, saving: MutableState<Boolean>) {
|
||||||
|
ItemAction(
|
||||||
|
stringResource(MR.strings.delete_chat_list_menu_action),
|
||||||
|
painterResource(MR.images.ic_delete),
|
||||||
|
onClick = {
|
||||||
|
deleteTagDialog(rhId, tag, saving)
|
||||||
|
showMenu.value = false
|
||||||
|
},
|
||||||
|
color = Color.Red
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EditTagAction(rhId: Long?, tag: ChatTag, showMenu: MutableState<Boolean>) {
|
||||||
|
ItemAction(
|
||||||
|
stringResource(MR.strings.edit_chat_list_menu_action),
|
||||||
|
painterResource(MR.images.ic_edit),
|
||||||
|
onClick = {
|
||||||
|
showMenu.value = false
|
||||||
|
ModalManager.start.showModalCloseable { close ->
|
||||||
|
TagListEditor(
|
||||||
|
rhId = rhId,
|
||||||
|
tagId = tag.chatTagId,
|
||||||
|
close = close,
|
||||||
|
emoji = tag.chatTagEmoji,
|
||||||
|
name = tag.chatTagText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
color = MenuTextColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
expect fun ChatTagInput(name: MutableState<String>, showError: State<Boolean>, emoji: MutableState<String?>)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TagListNameTextField(name: MutableState<String>, showError: State<Boolean>) {
|
||||||
|
var focused by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val colors = TextFieldDefaults.textFieldColors(
|
||||||
|
backgroundColor = Color.Unspecified,
|
||||||
|
focusedIndicatorColor = MaterialTheme.colors.secondary.copy(alpha = 0.6f),
|
||||||
|
unfocusedIndicatorColor = CurrentColors.value.colors.secondary.copy(alpha = 0.3f),
|
||||||
|
cursorColor = MaterialTheme.colors.secondary,
|
||||||
|
)
|
||||||
|
BasicTextField(
|
||||||
|
value = name.value,
|
||||||
|
onValueChange = { name.value = it },
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.indicatorLine(true, showError.value, interactionSource, colors)
|
||||||
|
.heightIn(min = TextFieldDefaults.MinHeight)
|
||||||
|
.onFocusChanged { focused = it.isFocused }
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
textStyle = TextStyle(fontSize = 18.sp, color = MaterialTheme.colors.onBackground),
|
||||||
|
singleLine = true,
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
|
||||||
|
decorationBox = @Composable { innerTextField ->
|
||||||
|
TextFieldDefaults.TextFieldDecorationBox(
|
||||||
|
value = name.value,
|
||||||
|
innerTextField = innerTextField,
|
||||||
|
placeholder = {
|
||||||
|
Text(generalGetString(MR.strings.list_name_field_placeholder), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary, lineHeight = 22.sp))
|
||||||
|
},
|
||||||
|
contentPadding = PaddingValues(),
|
||||||
|
label = null,
|
||||||
|
visualTransformation = VisualTransformation.None,
|
||||||
|
leadingIcon = null,
|
||||||
|
singleLine = true,
|
||||||
|
enabled = true,
|
||||||
|
isError = false,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.Unspecified)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setTag(rhId: Long?, tagId: Long?, chat: Chat, close: () -> Unit) {
|
||||||
|
withBGApi {
|
||||||
|
val tagIds: List<Long> = if (tagId == null) {
|
||||||
|
emptyList()
|
||||||
|
} else {
|
||||||
|
listOf(tagId)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result = apiSetChatTags(rh = rhId, type = chat.chatInfo.chatType, id = chat.chatInfo.apiId, tagIds = tagIds)
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
val oldTags = chat.chatInfo.chatTags
|
||||||
|
chatModel.userTags.value = result.first
|
||||||
|
when (val cInfo = chat.chatInfo) {
|
||||||
|
is ChatInfo.Direct -> {
|
||||||
|
val contact = cInfo.contact.copy(chatTags = result.second)
|
||||||
|
withChats {
|
||||||
|
updateContact(rhId, contact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChatInfo.Group -> {
|
||||||
|
val group = cInfo.groupInfo.copy(chatTags = result.second)
|
||||||
|
withChats {
|
||||||
|
updateGroup(rhId, group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
chatModel.moveChatTagUnread(chat, oldTags, result.second)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(TAG, "setChatTag error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteTag(rhId: Long?, tag: ChatTag, saving: MutableState<Boolean>) {
|
||||||
|
withBGApi {
|
||||||
|
saving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
val tagId = tag.chatTagId
|
||||||
|
if (apiDeleteChatTag(rhId, tagId)) {
|
||||||
|
chatModel.userTags.value = chatModel.userTags.value.filter { it.chatTagId != tagId }
|
||||||
|
if (chatModel.activeChatTagFilter.value == ActiveFilter.UserTag(tag)) {
|
||||||
|
chatModel.activeChatTagFilter.value = null
|
||||||
|
}
|
||||||
|
chatModel.chats.value.forEach { c ->
|
||||||
|
when (val cInfo = c.chatInfo) {
|
||||||
|
is ChatInfo.Direct -> {
|
||||||
|
val contact = cInfo.contact.copy(chatTags = cInfo.contact.chatTags.filter { it != tagId })
|
||||||
|
withChats {
|
||||||
|
updateContact(rhId, contact)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ChatInfo.Group -> {
|
||||||
|
val group = cInfo.groupInfo.copy(chatTags = cInfo.groupInfo.chatTags.filter { it != tagId })
|
||||||
|
withChats {
|
||||||
|
updateGroup(rhId, group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(TAG, "deleteTag error: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteTagDialog(rhId: Long?, tag: ChatTag, saving: MutableState<Boolean>) {
|
||||||
|
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||||
|
title = generalGetString(MR.strings.delete_chat_list_question),
|
||||||
|
text = String.format(generalGetString(MR.strings.delete_chat_list_warning), tag.chatTagText),
|
||||||
|
buttons = {
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
deleteTag(rhId, tag, saving)
|
||||||
|
}) {
|
||||||
|
Text(
|
||||||
|
generalGetString(MR.strings.confirm_verb),
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = colors.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SectionItemView({
|
||||||
|
AlertManager.shared.hideAlert()
|
||||||
|
}) {
|
||||||
|
Text(
|
||||||
|
stringResource(MR.strings.cancel_verb),
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = colors.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
package chat.simplex.common.views.helpers
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This was adapted from google example of drag and drop for Jetpack Compose
|
||||||
|
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt
|
||||||
|
*/
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||||
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.lazy.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberDragDropState(lazyListState: LazyListState, onMove: (Int, Int) -> Unit): DragDropState {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val state =
|
||||||
|
remember(lazyListState) {
|
||||||
|
DragDropState(state = lazyListState, onMove = onMove, scope = scope)
|
||||||
|
}
|
||||||
|
LaunchedEffect(state) {
|
||||||
|
while (true) {
|
||||||
|
val diff = state.scrollChannel.receive()
|
||||||
|
lazyListState.scrollBy(diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
class DragDropState
|
||||||
|
internal constructor(
|
||||||
|
private val state: LazyListState,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val onMove: (Int, Int) -> Unit
|
||||||
|
) {
|
||||||
|
var draggingItemIndex by mutableStateOf<Int?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
internal val scrollChannel = Channel<Float>()
|
||||||
|
|
||||||
|
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
|
||||||
|
private var draggingItemInitialOffset by mutableIntStateOf(0)
|
||||||
|
internal val draggingItemOffset: Float
|
||||||
|
get() =
|
||||||
|
draggingItemLayoutInfo?.let { item ->
|
||||||
|
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
|
||||||
|
} ?: 0f
|
||||||
|
|
||||||
|
private val draggingItemLayoutInfo: LazyListItemInfo?
|
||||||
|
get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex }
|
||||||
|
|
||||||
|
internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
internal var previousItemOffset = Animatable(0f)
|
||||||
|
private set
|
||||||
|
|
||||||
|
internal fun onDragStart(offset: Offset) {
|
||||||
|
val touchY = offset.y.toInt()
|
||||||
|
val item = state.layoutInfo.visibleItemsInfo.minByOrNull {
|
||||||
|
val itemCenter = (it.offset - state.layoutInfo.viewportStartOffset) + it.size / 2
|
||||||
|
kotlin.math.abs(touchY - itemCenter) // Find the item closest to the touch position, needs to take viewportStartOffset into account
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item != null) {
|
||||||
|
draggingItemIndex = item.index
|
||||||
|
draggingItemInitialOffset = item.offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal fun onDragInterrupted() {
|
||||||
|
if (draggingItemIndex != null) {
|
||||||
|
previousIndexOfDraggedItem = draggingItemIndex
|
||||||
|
val startOffset = draggingItemOffset
|
||||||
|
scope.launch {
|
||||||
|
previousItemOffset.snapTo(startOffset)
|
||||||
|
previousItemOffset.animateTo(
|
||||||
|
0f,
|
||||||
|
spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 1f)
|
||||||
|
)
|
||||||
|
previousIndexOfDraggedItem = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draggingItemDraggedDelta = 0f
|
||||||
|
draggingItemIndex = null
|
||||||
|
draggingItemInitialOffset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun onDrag(offset: Offset) {
|
||||||
|
draggingItemDraggedDelta += offset.y
|
||||||
|
|
||||||
|
val draggingItem = draggingItemLayoutInfo ?: return
|
||||||
|
val startOffset = draggingItem.offset + draggingItemOffset
|
||||||
|
val endOffset = startOffset + draggingItem.size
|
||||||
|
val middleOffset = startOffset + (endOffset - startOffset) / 2f
|
||||||
|
|
||||||
|
val targetItem =
|
||||||
|
state.layoutInfo.visibleItemsInfo.find { item ->
|
||||||
|
middleOffset.toInt() in item.offset..item.offsetEnd &&
|
||||||
|
draggingItem.index != item.index
|
||||||
|
}
|
||||||
|
if (targetItem != null) {
|
||||||
|
if (
|
||||||
|
draggingItem.index == state.firstVisibleItemIndex ||
|
||||||
|
targetItem.index == state.firstVisibleItemIndex
|
||||||
|
) {
|
||||||
|
state.requestScrollToItem(
|
||||||
|
state.firstVisibleItemIndex,
|
||||||
|
state.firstVisibleItemScrollOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onMove.invoke(draggingItem.index, targetItem.index)
|
||||||
|
draggingItemIndex = targetItem.index
|
||||||
|
} else {
|
||||||
|
val overscroll =
|
||||||
|
when {
|
||||||
|
draggingItemDraggedDelta > 0 ->
|
||||||
|
(endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
|
||||||
|
draggingItemDraggedDelta < 0 ->
|
||||||
|
(startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
|
||||||
|
else -> 0f
|
||||||
|
}
|
||||||
|
if (overscroll != 0f) {
|
||||||
|
scrollChannel.trySend(overscroll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val LazyListItemInfo.offsetEnd: Int
|
||||||
|
get() = this.offset + this.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.dragContainer(dragDropState: DragDropState): Modifier {
|
||||||
|
return pointerInput(dragDropState) {
|
||||||
|
detectDragGesturesAfterLongPress(
|
||||||
|
onDrag = { change, offset ->
|
||||||
|
change.consume()
|
||||||
|
dragDropState.onDrag(offset = offset)
|
||||||
|
},
|
||||||
|
onDragStart = { offset -> dragDropState.onDragStart(offset) },
|
||||||
|
onDragEnd = { dragDropState.onDragInterrupted() },
|
||||||
|
onDragCancel = { dragDropState.onDragInterrupted() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LazyItemScope.DraggableItem(
|
||||||
|
dragDropState: DragDropState,
|
||||||
|
index: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable ColumnScope.(isDragging: Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
val dragging = index == dragDropState.draggingItemIndex
|
||||||
|
val draggingModifier =
|
||||||
|
if (dragging) {
|
||||||
|
Modifier.zIndex(1f).graphicsLayer { translationY = dragDropState.draggingItemOffset }
|
||||||
|
} else if (index == dragDropState.previousIndexOfDraggedItem) {
|
||||||
|
Modifier.zIndex(1f).graphicsLayer {
|
||||||
|
translationY = dragDropState.previousItemOffset.value
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||||
|
}
|
||||||
|
Column(modifier = modifier.then(draggingModifier)) { content(dragging) }
|
||||||
|
}
|
|
@ -188,6 +188,9 @@
|
||||||
<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_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>
|
||||||
|
<string name="error_updating_chat_tags">Error updating chat list</string>
|
||||||
|
<string name="error_creating_chat_tags">Error creating chat list</string>
|
||||||
|
<string name="error_loading_chat_tags">Error loading chat lists</string>
|
||||||
|
|
||||||
<!-- background service notice - SimpleXAPI.kt -->
|
<!-- background service notice - SimpleXAPI.kt -->
|
||||||
<string name="icon_descr_instant_notifications">Instant notifications</string>
|
<string name="icon_descr_instant_notifications">Instant notifications</string>
|
||||||
|
@ -361,6 +364,7 @@
|
||||||
<string name="revoke_file__confirm">Revoke</string>
|
<string name="revoke_file__confirm">Revoke</string>
|
||||||
<string name="forward_chat_item">Forward</string>
|
<string name="forward_chat_item">Forward</string>
|
||||||
<string name="download_file">Download</string>
|
<string name="download_file">Download</string>
|
||||||
|
<string name="list_menu">List</string>
|
||||||
|
|
||||||
<string name="message_forwarded_title">Message forwarded</string>
|
<string name="message_forwarded_title">Message forwarded</string>
|
||||||
<string name="message_forwarded_desc">No direct connection yet, message is forwarded by admin.</string>
|
<string name="message_forwarded_desc">No direct connection yet, message is forwarded by admin.</string>
|
||||||
|
@ -390,6 +394,10 @@
|
||||||
<string name="you_have_no_chats">You have no chats</string>
|
<string name="you_have_no_chats">You have no chats</string>
|
||||||
<string name="loading_chats">Loading chats…</string>
|
<string name="loading_chats">Loading chats…</string>
|
||||||
<string name="no_filtered_chats">No filtered chats</string>
|
<string name="no_filtered_chats">No filtered chats</string>
|
||||||
|
<string name="no_chats_in_list">No chats in list %s.</string>
|
||||||
|
<string name="no_unread_chats">No unread chats</string>
|
||||||
|
<string name="no_chats">No chats</string>
|
||||||
|
<string name="no_chats_found">No chats found</string>
|
||||||
<string name="contact_tap_to_connect">Tap to Connect</string>
|
<string name="contact_tap_to_connect">Tap to Connect</string>
|
||||||
<string name="connect_with_contact_name_question">Connect with %1$s?</string>
|
<string name="connect_with_contact_name_question">Connect with %1$s?</string>
|
||||||
<string name="search_or_paste_simplex_link">Search or paste SimpleX link</string>
|
<string name="search_or_paste_simplex_link">Search or paste SimpleX link</string>
|
||||||
|
@ -409,6 +417,12 @@
|
||||||
<string name="forward_files_missing_desc">%1$d file(s) were deleted.</string>
|
<string name="forward_files_missing_desc">%1$d file(s) were deleted.</string>
|
||||||
<string name="forward_files_not_accepted_receive_files">Download</string>
|
<string name="forward_files_not_accepted_receive_files">Download</string>
|
||||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s messages not forwarded</string>
|
<string name="forward_files_messages_deleted_after_selection_title">%1$s messages not forwarded</string>
|
||||||
|
<string name="chat_list_favorites">Favorites</string>
|
||||||
|
<string name="chat_list_contacts">Contacts</string>
|
||||||
|
<string name="chat_list_groups">Groups</string>
|
||||||
|
<string name="chat_list_businesses">Businesses</string>
|
||||||
|
<string name="chat_list_all">All</string>
|
||||||
|
<string name="chat_list_add_list">Add list</string>
|
||||||
|
|
||||||
<!-- ShareListView.kt -->
|
<!-- ShareListView.kt -->
|
||||||
<string name="share_message">Share message…</string>
|
<string name="share_message">Share message…</string>
|
||||||
|
@ -482,6 +496,7 @@
|
||||||
<string name="loading_remote_file_desc">Please, wait while the file is being loaded from the linked mobile</string>
|
<string name="loading_remote_file_desc">Please, wait while the file is being loaded from the linked mobile</string>
|
||||||
<string name="file_error">File error</string>
|
<string name="file_error">File error</string>
|
||||||
<string name="temporary_file_error">Temporary file error</string>
|
<string name="temporary_file_error">Temporary file error</string>
|
||||||
|
<string name="open_with_app">Open with %s</string>
|
||||||
|
|
||||||
<!-- Voice messages -->
|
<!-- Voice messages -->
|
||||||
<string name="voice_message">Voice message</string>
|
<string name="voice_message">Voice message</string>
|
||||||
|
@ -626,6 +641,16 @@
|
||||||
<string name="favorite_chat">Favorite</string>
|
<string name="favorite_chat">Favorite</string>
|
||||||
<string name="unfavorite_chat">Unfavorite</string>
|
<string name="unfavorite_chat">Unfavorite</string>
|
||||||
|
|
||||||
|
<!-- Tags - ChatListNavLinkView.kt -->
|
||||||
|
<string name="create_list">Create list</string>
|
||||||
|
<string name="add_to_list">Add to list</string>
|
||||||
|
<string name="save_list">Save list</string>
|
||||||
|
<string name="list_name_field_placeholder">List name...</string>
|
||||||
|
<string name="duplicated_list_error">List name and emoji should be different for all lists.</string>
|
||||||
|
<string name="delete_chat_list_menu_action">Delete</string>
|
||||||
|
<string name="delete_chat_list_question">Delete list?</string>
|
||||||
|
<string name="delete_chat_list_warning">All chats will be removed from the list %s, and the list deleted</string>
|
||||||
|
<string name="edit_chat_list_menu_action">Edit</string>
|
||||||
|
|
||||||
<!-- Pending contact connection alert dialogues -->
|
<!-- Pending contact connection alert dialogues -->
|
||||||
<string name="you_invited_a_contact">You invited a contact</string>
|
<string name="you_invited_a_contact">You invited a contact</string>
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M170-368v-75h620v75H170Zm0-150v-75h620v75H170Z"/></svg>
|
After Width: | Height: | Size: 171 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M54.5-279q0-33 16.75-60.25T116.5-381q61-30 123.5-45.25t126-15.25q63.5 0 125.75 15.25T615-381q28.5 14.5 45.25 41.75T677-279v31q0 31-22 53t-53 22H129.5q-31 0-53-22t-22-53v-31Zm677 106q10-17 15.25-36t5.25-39v-35q0-43.5-22.5-83.75T663-434.5q48.5 6 91.25 19.75t80.25 34.25Q869-362 887.25-338t18.25 52v38q0 31-22 53t-53 22h-99ZM366-479q-64 0-109-45t-45-109q0-64 45-109t109-45q64 0 109 45t45 109q0 64-45 109t-109 45Zm382-154.5q0 63.5-45 108.75T594-479.5q-9.5 0-25.25-2.25T543-487q26.5-30.5 40.75-68T598-633.5q0-40.5-14.25-78.25T543-780q12.5-4.5 25.5-5.75T594-787q64 0 109 45t45 108.5Z"/></svg>
|
After Width: | Height: | Size: 683 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="m859-406.5-304 305Q545.93-93 535.21-89q-10.71 4-21.71 4t-21.25-4.25Q482-93.5 473.5-101.5L102.5-474q-8-7.5-12.75-17.97Q85-502.44 85-514v-303.5q0-23.72 16.89-40.61T142.5-875h305q11.41 0 22.11 4.4 10.71 4.39 18.89 12.6L859-488q8.91 8.92 13.21 19.52 4.29 10.6 4.29 21.21 0 11.27-4.5 22.27t-13 18.5Zm-343 266L820-446 447.49-817.5H142.5v301.77L516-140.5ZM246.75-664q20.5 0 35.63-15.04 15.12-15.03 15.12-35.37 0-20.34-15.06-35.47Q267.38-765 247-765q-20.75 0-35.62 15.04-14.88 15.03-14.88 35.37 0 20.34 14.88 35.46Q226.25-664 246.75-664ZM481.5-479Z"/></svg>
|
After Width: | Height: | Size: 646 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="m859-406.5-304 305Q545.93-93 535.21-89q-10.71 4-21.71 4t-21.25-4.25Q482-93.5 473.5-101.5L102.5-474q-8-7.5-12.75-17.97Q85-502.44 85-514v-303.5q0-23.72 16.89-40.61T142.5-875h305q11.41 0 22.11 4.4 10.71 4.39 18.89 12.6L859-488q8.91 8.92 13.21 19.52 4.29 10.6 4.29 21.21 0 11.27-4.5 22.27t-13 18.5ZM246.75-664q20.5 0 35.63-15.04 15.12-15.03 15.12-35.37 0-20.34-15.06-35.47Q267.38-765 247-765q-20.75 0-35.62 15.04-14.88 15.03-14.88 35.37 0 20.34 14.88 35.46Q226.25-664 246.75-664Z"/></svg>
|
After Width: | Height: | Size: 581 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M480-479q-64.5 0-109.75-45.25T325-634q0-64.5 45.25-109.75T480-789q64.5 0 109.75 45.25T635-634q0 64.5-45.25 109.75T480-479ZM169-248v-31.03q0-32.97 16.75-60.22t45.27-41.76Q292-411 354.25-426.25 416.5-441.5 480-441.5t125.75 15.25Q668-411 728.98-381.01q28.52 14.51 45.27 41.76Q791-312 791-279.03V-248q0 30.94-22.03 52.97Q746.94-173 716-173H244q-30.94 0-52.97-22.03Q169-217.06 169-248Z"/></svg>
|
After Width: | Height: | Size: 486 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M165-130q-30.94 0-52.97-22.03Q90-174.06 90-205v-440q0-30.94 22.03-52.97Q134.06-720 165-720h161v-75.04Q326-826 348.03-848T401-870h158q30.94 0 52.97 22.03Q634-825.94 634-795v75h161q30.94 0 52.97 22.03Q870-675.94 870-645v440q0 30.94-22.03 52.97Q825.94-130 795-130H165Zm236-590h158v-75H401v75Z"/></svg>
|
After Width: | Height: | Size: 395 B |
|
@ -0,0 +1,18 @@
|
||||||
|
package chat.simplex.common.views.chat.item
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import chat.simplex.common.model.CryptoFile
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun SaveOrOpenFileMenu(
|
||||||
|
showMenu: MutableState<Boolean>,
|
||||||
|
encrypted: Boolean,
|
||||||
|
ext: String?,
|
||||||
|
encryptedUri: URI,
|
||||||
|
fileSource: CryptoFile,
|
||||||
|
saveFile: () -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
|
@ -4,8 +4,7 @@ import SectionDivider
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.interaction.InteractionSource
|
import androidx.compose.foundation.interaction.InteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.Divider
|
import androidx.compose.material.*
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
package chat.simplex.common.views.chatlist
|
||||||
|
|
||||||
|
import SectionItemView
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import chat.simplex.common.ui.theme.*
|
||||||
|
import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex
|
||||||
|
import chat.simplex.common.views.chat.item.isHeartEmoji
|
||||||
|
import chat.simplex.common.views.chat.item.isShortEmoji
|
||||||
|
import chat.simplex.common.views.helpers.toDp
|
||||||
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun ChatTagInput(name: MutableState<String>, showError: State<Boolean>, emoji: MutableState<String?>) {
|
||||||
|
SectionItemView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||||
|
SingleEmojiInput(emoji)
|
||||||
|
TagListNameTextField(name, showError = showError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SingleEmojiInput(
|
||||||
|
emoji: MutableState<String?>
|
||||||
|
) {
|
||||||
|
val state = remember { mutableStateOf(TextFieldValue(emoji.value ?: "")) }
|
||||||
|
val colors = TextFieldDefaults.textFieldColors(
|
||||||
|
textColor = if (isHeartEmoji(emoji.value ?: "")) Color(0xffD63C31) else MaterialTheme.colors.onPrimary,
|
||||||
|
backgroundColor = Color.Unspecified,
|
||||||
|
focusedIndicatorColor = MaterialTheme.colors.secondary.copy(alpha = 0.6f),
|
||||||
|
unfocusedIndicatorColor = CurrentColors.value.colors.secondary.copy(alpha = 0.3f),
|
||||||
|
cursorColor = MaterialTheme.colors.secondary,
|
||||||
|
)
|
||||||
|
TextField(
|
||||||
|
value = state.value,
|
||||||
|
onValueChange = { newValue ->
|
||||||
|
if (newValue.text == emoji.value) {
|
||||||
|
state.value = newValue
|
||||||
|
return@TextField
|
||||||
|
}
|
||||||
|
val newValueClamped = newValue.text.replace(emoji.value ?: "", "")
|
||||||
|
val isEmoji = isShortEmoji(newValueClamped)
|
||||||
|
emoji.value = if (isEmoji) newValueClamped else null
|
||||||
|
state.value = if (isEmoji) newValue else TextFieldValue()
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(width = TextFieldDefaults.MinHeight.value.sp.toDp(), height = TextFieldDefaults.MinHeight),
|
||||||
|
textStyle = LocalTextStyle.current.copy(fontFamily = EmojiFont, textAlign = TextAlign.Center),
|
||||||
|
placeholder = {
|
||||||
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(MR.images.ic_add_reaction),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = colors,
|
||||||
|
)
|
||||||
|
}
|
|
@ -24,11 +24,11 @@ android.nonTransitiveRClass=true
|
||||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||||
kotlin.jvm.target=11
|
kotlin.jvm.target=11
|
||||||
|
|
||||||
android.version_name=6.2.1
|
android.version_name=6.3-beta.0
|
||||||
android.version_code=261
|
android.version_code=267
|
||||||
|
|
||||||
desktop.version_name=6.2.1
|
desktop.version_name=6.3-beta.0
|
||||||
desktop.version_code=83
|
desktop.version_code=86
|
||||||
|
|
||||||
kotlin.version=1.9.23
|
kotlin.version=1.9.23
|
||||||
gradle.plugin.version=8.2.0
|
gradle.plugin.version=8.2.0
|
||||||
|
|
|
@ -72,7 +72,7 @@ This is a small but important change - you can now see who reacted to your messa
|
||||||
|
|
||||||
### Improving notifications in iOS app
|
### Improving notifications in iOS app
|
||||||
|
|
||||||
iOS notifications in a decentralized network is a complex problems. We [support iOS notifications](./20220404-simplex-chat-instant-notifications.md#ios-notifications-require-a-server) from early versions of the app, focussing on preserving privacy as much as possible. But the reliability of notifications was not good enough.
|
iOS notifications in a decentralized network is a complex problem. We [support iOS notifications](./20220404-simplex-chat-instant-notifications.md#ios-notifications-require-a-server) from early versions of the app, focussing on preserving privacy as much as possible. But the reliability of notifications was not good enough.
|
||||||
|
|
||||||
We solved several problems of notification delivery in this release:
|
We solved several problems of notification delivery in this release:
|
||||||
- messaging servers no longer lose notifications while notification servers are restarted.
|
- messaging servers no longer lose notifications while notification servers are restarted.
|
||||||
|
|
|
@ -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: f5cef2556b05cf384db284644c112f822407b361
|
tag: 184a95cd2a2806fb3d318508ca1f6528201a8114
|
||||||
|
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
|
|
187
package.yaml
187
package.yaml
|
@ -1,187 +0,0 @@
|
||||||
name: simplex-chat
|
|
||||||
version: 6.2.0.7
|
|
||||||
#synopsis:
|
|
||||||
#description:
|
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
|
||||||
license: AGPL-3
|
|
||||||
author: simplex.chat
|
|
||||||
maintainer: chat@simplex.chat
|
|
||||||
copyright: 2020-22 simplex.chat
|
|
||||||
category: Web, System, Services, Cryptography
|
|
||||||
extra-source-files:
|
|
||||||
- README.md
|
|
||||||
- PRIVACY.md
|
|
||||||
- cabal.project
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
- aeson == 2.2.*
|
|
||||||
- ansi-terminal >= 0.10 && < 0.12
|
|
||||||
- async == 2.2.*
|
|
||||||
- attoparsec == 0.14.*
|
|
||||||
- base >= 4.7 && < 5
|
|
||||||
- base64-bytestring >= 1.0 && < 1.3
|
|
||||||
- composition == 1.0.*
|
|
||||||
- constraints >= 0.12 && < 0.14
|
|
||||||
- containers == 0.6.*
|
|
||||||
- crypton == 0.34.*
|
|
||||||
- data-default >= 0.7 && < 0.8
|
|
||||||
- directory == 1.3.*
|
|
||||||
- direct-sqlcipher == 2.3.*
|
|
||||||
- email-validate == 2.3.*
|
|
||||||
- exceptions == 0.10.*
|
|
||||||
- filepath == 1.4.*
|
|
||||||
- file-embed == 0.0.15.*
|
|
||||||
- http-types == 0.12.*
|
|
||||||
- http2 >= 4.2.2 && < 4.3
|
|
||||||
- memory == 0.18.*
|
|
||||||
- mtl >= 2.3.1 && < 3.0
|
|
||||||
- network >= 3.1.2.7 && < 3.2
|
|
||||||
- network-transport == 0.5.6
|
|
||||||
- optparse-applicative >= 0.15 && < 0.17
|
|
||||||
- random >= 1.1 && < 1.3
|
|
||||||
- record-hasfield == 1.0.*
|
|
||||||
- scientific ==0.3.7.*
|
|
||||||
- simple-logger == 0.1.*
|
|
||||||
- simplexmq >= 5.0
|
|
||||||
- socks == 0.6.*
|
|
||||||
- sqlcipher-simple == 0.4.*
|
|
||||||
- stm == 2.5.*
|
|
||||||
- terminal == 0.2.*
|
|
||||||
- time == 1.12.*
|
|
||||||
- tls >= 1.9.0 && < 1.10
|
|
||||||
- unliftio == 0.2.*
|
|
||||||
- unliftio-core == 0.2.*
|
|
||||||
- uuid == 1.3.*
|
|
||||||
- zip == 2.0.*
|
|
||||||
|
|
||||||
flags:
|
|
||||||
swift:
|
|
||||||
description: Enable swift JSON format
|
|
||||||
manual: True
|
|
||||||
default: False
|
|
||||||
|
|
||||||
when:
|
|
||||||
- condition: flag(swift)
|
|
||||||
cpp-options:
|
|
||||||
- -DswiftJSON
|
|
||||||
- condition: impl(ghc >= 9.6.2)
|
|
||||||
dependencies:
|
|
||||||
- bytestring == 0.11.*
|
|
||||||
- process == 1.6.*
|
|
||||||
- template-haskell == 2.20.*
|
|
||||||
- text >= 2.0.1 && < 2.2
|
|
||||||
- condition: impl(ghc < 9.6.2)
|
|
||||||
dependencies:
|
|
||||||
- bytestring == 0.10.*
|
|
||||||
- process >= 1.6 && < 1.6.18
|
|
||||||
- template-haskell == 2.16.*
|
|
||||||
- text >= 1.2.4.0 && < 1.3
|
|
||||||
|
|
||||||
library:
|
|
||||||
source-dirs: src
|
|
||||||
|
|
||||||
executables:
|
|
||||||
simplex-chat:
|
|
||||||
source-dirs: apps/simplex-chat
|
|
||||||
main: Main.hs
|
|
||||||
dependencies:
|
|
||||||
- simplex-chat
|
|
||||||
- network == 3.1.*
|
|
||||||
- websockets == 0.12.*
|
|
||||||
ghc-options:
|
|
||||||
- -threaded
|
|
||||||
|
|
||||||
simplex-bot:
|
|
||||||
source-dirs: apps/simplex-bot
|
|
||||||
main: Main.hs
|
|
||||||
dependencies:
|
|
||||||
- simplex-chat
|
|
||||||
ghc-options:
|
|
||||||
- -threaded
|
|
||||||
|
|
||||||
simplex-bot-advanced:
|
|
||||||
source-dirs: apps/simplex-bot-advanced
|
|
||||||
main: Main.hs
|
|
||||||
dependencies:
|
|
||||||
- simplex-chat
|
|
||||||
ghc-options:
|
|
||||||
- -threaded
|
|
||||||
|
|
||||||
simplex-broadcast-bot:
|
|
||||||
source-dirs:
|
|
||||||
- apps/simplex-broadcast-bot
|
|
||||||
- apps/simplex-broadcast-bot/src
|
|
||||||
main: Main.hs
|
|
||||||
dependencies:
|
|
||||||
- simplex-chat
|
|
||||||
ghc-options:
|
|
||||||
- -threaded
|
|
||||||
|
|
||||||
simplex-directory-service:
|
|
||||||
source-dirs:
|
|
||||||
- apps/simplex-directory-service
|
|
||||||
- apps/simplex-directory-service/src
|
|
||||||
main: Main.hs
|
|
||||||
dependencies:
|
|
||||||
- simplex-chat
|
|
||||||
ghc-options:
|
|
||||||
- -threaded
|
|
||||||
|
|
||||||
tests:
|
|
||||||
simplex-chat-test:
|
|
||||||
source-dirs:
|
|
||||||
- tests
|
|
||||||
- apps/simplex-broadcast-bot/src
|
|
||||||
- apps/simplex-directory-service/src
|
|
||||||
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:
|
|
||||||
- QuickCheck == 2.14.*
|
|
||||||
- simplex-chat
|
|
||||||
- async == 2.2.*
|
|
||||||
- deepseq == 1.4.*
|
|
||||||
- generic-random == 1.5.*
|
|
||||||
- network == 3.1.*
|
|
||||||
- silently == 1.2.*
|
|
||||||
- stm == 2.5.*
|
|
||||||
ghc-options:
|
|
||||||
- -threaded
|
|
||||||
|
|
||||||
ghc-options:
|
|
||||||
# - -haddock
|
|
||||||
- -O2
|
|
||||||
- -Weverything
|
|
||||||
- -Wno-missing-exported-signatures
|
|
||||||
- -Wno-missing-import-lists
|
|
||||||
- -Wno-missed-specialisations
|
|
||||||
- -Wno-all-missed-specialisations
|
|
||||||
- -Wno-unsafe
|
|
||||||
- -Wno-safe
|
|
||||||
- -Wno-missing-local-signatures
|
|
||||||
- -Wno-missing-kind-signatures
|
|
||||||
- -Wno-missing-deriving-strategies
|
|
||||||
- -Wno-monomorphism-restriction
|
|
||||||
- -Wno-prepositive-qualified-module
|
|
||||||
- -Wno-unused-packages
|
|
||||||
- -Wno-implicit-prelude
|
|
||||||
- -Wno-missing-safe-haskell-mode
|
|
||||||
- -Wno-missing-export-lists
|
|
||||||
- -Wno-partial-fields
|
|
||||||
- -Wcompat
|
|
||||||
- -Werror=incomplete-record-updates
|
|
||||||
- -Werror=incomplete-patterns
|
|
||||||
- -Werror=missing-methods
|
|
||||||
- -Werror=incomplete-uni-patterns
|
|
||||||
- -Werror=tabs
|
|
||||||
- -Wredundant-constraints
|
|
||||||
- -Wincomplete-record-updates
|
|
||||||
- -Wunused-type-patterns
|
|
||||||
|
|
||||||
default-extensions:
|
|
||||||
- StrictData
|
|
|
@ -38,6 +38,44 @@
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="6.2.3" date="2024-12-26">
|
||||||
|
<url type="details">https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html</url>
|
||||||
|
<description>
|
||||||
|
<p>New in v6.2.1-3:</p>
|
||||||
|
<ul>
|
||||||
|
<li>important fixes</li>
|
||||||
|
<li>offer to "fix" encryption when calling or making direct connection with member.</li>
|
||||||
|
<li>broken layout.</li>
|
||||||
|
<li>option to enable debug logs (disabled by default).</li>
|
||||||
|
<li>show who reacted in direct chats.</li>
|
||||||
|
</ul>
|
||||||
|
<p>New in v6.2:</p>
|
||||||
|
<ul>
|
||||||
|
<li>SimpleX Chat and Flux made an agreement to include servers operated by Flux into the app – to improve metadata privacy.</li>
|
||||||
|
<li>Business chats – your customers privacy.</li>
|
||||||
|
<li>Improved user experience in chats: open on the first unread, jump to quoted messages, see who reacted.</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="6.2.2" date="2024-12-25">
|
||||||
|
<url type="details">https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html</url>
|
||||||
|
<description>
|
||||||
|
<p>New in v6.2.1-2:</p>
|
||||||
|
<ul>
|
||||||
|
<li>important fixes</li>
|
||||||
|
<li>offer to "fix" encryption when calling or making direct connection with member.</li>
|
||||||
|
<li>broken layout.</li>
|
||||||
|
<li>option to enable debug logs (disabled by default).</li>
|
||||||
|
<li>show who reacted in direct chats.</li>
|
||||||
|
</ul>
|
||||||
|
<p>New in v6.2:</p>
|
||||||
|
<ul>
|
||||||
|
<li>SimpleX Chat and Flux made an agreement to include servers operated by Flux into the app – to improve metadata privacy.</li>
|
||||||
|
<li>Business chats – your customers privacy.</li>
|
||||||
|
<li>Improved user experience in chats: open on the first unread, jump to quoted messages, see who reacted.</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="6.2.1" date="2024-12-12">
|
<release version="6.2.1" date="2024-12-12">
|
||||||
<url type="details">https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html</url>
|
<url type="details">https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html</url>
|
||||||
<description>
|
<description>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"https://github.com/simplex-chat/simplexmq.git"."f5cef2556b05cf384db284644c112f822407b361" = "14w0fji1fdr63w4pjkhlrdiz93p2axghzkb1ykvnrw111by87g8l";
|
"https://github.com/simplex-chat/simplexmq.git"."184a95cd2a2806fb3d318508ca1f6528201a8114" = "0zlxi2dfv86sph9ndnazlvqgy0kq5rd1j6pjh41pswhaz8diwcag";
|
||||||
"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";
|
||||||
|
|
|
@ -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: 6.2.0.7
|
version: 6.3.0.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
|
||||||
|
@ -239,7 +239,7 @@ library
|
||||||
, record-hasfield ==1.0.*
|
, record-hasfield ==1.0.*
|
||||||
, scientific ==0.3.7.*
|
, scientific ==0.3.7.*
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
, simplexmq >=5.0
|
, simplexmq >=6.3
|
||||||
, socks ==0.6.*
|
, socks ==0.6.*
|
||||||
, sqlcipher-simple ==0.4.*
|
, sqlcipher-simple ==0.4.*
|
||||||
, stm ==2.5.*
|
, stm ==2.5.*
|
||||||
|
@ -276,61 +276,10 @@ executable simplex-bot
|
||||||
StrictData
|
StrictData
|
||||||
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
||||||
build-depends:
|
build-depends:
|
||||||
aeson ==2.2.*
|
base >=4.7 && <5
|
||||||
, ansi-terminal >=0.10 && <0.12
|
|
||||||
, async ==2.2.*
|
|
||||||
, attoparsec ==0.14.*
|
|
||||||
, base >=4.7 && <5
|
|
||||||
, base64-bytestring >=1.0 && <1.3
|
|
||||||
, composition ==1.0.*
|
|
||||||
, constraints >=0.12 && <0.14
|
|
||||||
, containers ==0.6.*
|
|
||||||
, crypton ==0.34.*
|
|
||||||
, data-default ==0.7.*
|
|
||||||
, direct-sqlcipher ==2.3.*
|
|
||||||
, directory ==1.3.*
|
, directory ==1.3.*
|
||||||
, email-validate ==2.3.*
|
|
||||||
, exceptions ==0.10.*
|
|
||||||
, file-embed ==0.0.15.*
|
|
||||||
, filepath ==1.4.*
|
|
||||||
, http-types ==0.12.*
|
|
||||||
, http2 >=4.2.2 && <4.3
|
|
||||||
, memory ==0.18.*
|
|
||||||
, mtl >=2.3.1 && <3.0
|
|
||||||
, network >=3.1.2.7 && <3.2
|
|
||||||
, network-transport ==0.5.6
|
|
||||||
, optparse-applicative >=0.15 && <0.17
|
|
||||||
, random >=1.1 && <1.3
|
|
||||||
, record-hasfield ==1.0.*
|
|
||||||
, scientific ==0.3.7.*
|
|
||||||
, simple-logger ==0.1.*
|
|
||||||
, simplex-chat
|
, simplex-chat
|
||||||
, simplexmq >=5.0
|
|
||||||
, socks ==0.6.*
|
|
||||||
, sqlcipher-simple ==0.4.*
|
|
||||||
, stm ==2.5.*
|
|
||||||
, terminal ==0.2.*
|
|
||||||
, time ==1.12.*
|
|
||||||
, tls >=1.9.0 && <1.10
|
|
||||||
, unliftio ==0.2.*
|
|
||||||
, unliftio-core ==0.2.*
|
|
||||||
, uuid ==1.3.*
|
|
||||||
, zip ==2.0.*
|
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
if flag(swift)
|
|
||||||
cpp-options: -DswiftJSON
|
|
||||||
if impl(ghc >= 9.6.2)
|
|
||||||
build-depends:
|
|
||||||
bytestring ==0.11.*
|
|
||||||
, process ==1.6.*
|
|
||||||
, template-haskell ==2.20.*
|
|
||||||
, text >=2.0.1 && <2.2
|
|
||||||
if impl(ghc < 9.6.2)
|
|
||||||
build-depends:
|
|
||||||
bytestring ==0.10.*
|
|
||||||
, process >=1.6 && <1.6.18
|
|
||||||
, template-haskell ==2.16.*
|
|
||||||
, text >=1.2.4.0 && <1.3
|
|
||||||
|
|
||||||
executable simplex-bot-advanced
|
executable simplex-bot-advanced
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
|
@ -342,61 +291,19 @@ executable simplex-bot-advanced
|
||||||
StrictData
|
StrictData
|
||||||
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
||||||
build-depends:
|
build-depends:
|
||||||
aeson ==2.2.*
|
async ==2.2.*
|
||||||
, ansi-terminal >=0.10 && <0.12
|
|
||||||
, async ==2.2.*
|
|
||||||
, attoparsec ==0.14.*
|
|
||||||
, base >=4.7 && <5
|
, base >=4.7 && <5
|
||||||
, base64-bytestring >=1.0 && <1.3
|
|
||||||
, composition ==1.0.*
|
|
||||||
, constraints >=0.12 && <0.14
|
|
||||||
, containers ==0.6.*
|
|
||||||
, crypton ==0.34.*
|
|
||||||
, data-default ==0.7.*
|
|
||||||
, direct-sqlcipher ==2.3.*
|
|
||||||
, directory ==1.3.*
|
, directory ==1.3.*
|
||||||
, email-validate ==2.3.*
|
|
||||||
, exceptions ==0.10.*
|
|
||||||
, file-embed ==0.0.15.*
|
|
||||||
, filepath ==1.4.*
|
|
||||||
, http-types ==0.12.*
|
|
||||||
, http2 >=4.2.2 && <4.3
|
|
||||||
, memory ==0.18.*
|
|
||||||
, mtl >=2.3.1 && <3.0
|
|
||||||
, network >=3.1.2.7 && <3.2
|
|
||||||
, network-transport ==0.5.6
|
|
||||||
, optparse-applicative >=0.15 && <0.17
|
|
||||||
, random >=1.1 && <1.3
|
|
||||||
, record-hasfield ==1.0.*
|
|
||||||
, scientific ==0.3.7.*
|
|
||||||
, simple-logger ==0.1.*
|
|
||||||
, simplex-chat
|
, simplex-chat
|
||||||
, simplexmq >=5.0
|
, simplexmq >=6.3
|
||||||
, socks ==0.6.*
|
|
||||||
, sqlcipher-simple ==0.4.*
|
|
||||||
, stm ==2.5.*
|
, stm ==2.5.*
|
||||||
, terminal ==0.2.*
|
|
||||||
, time ==1.12.*
|
|
||||||
, tls >=1.9.0 && <1.10
|
|
||||||
, unliftio ==0.2.*
|
|
||||||
, unliftio-core ==0.2.*
|
|
||||||
, uuid ==1.3.*
|
|
||||||
, zip ==2.0.*
|
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
if flag(swift)
|
|
||||||
cpp-options: -DswiftJSON
|
|
||||||
if impl(ghc >= 9.6.2)
|
if impl(ghc >= 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.11.*
|
text >=2.0.1 && <2.2
|
||||||
, process ==1.6.*
|
|
||||||
, template-haskell ==2.20.*
|
|
||||||
, 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.*
|
text >=1.2.4.0 && <1.3
|
||||||
, process >=1.6 && <1.6.18
|
|
||||||
, template-haskell ==2.16.*
|
|
||||||
, text >=1.2.4.0 && <1.3
|
|
||||||
|
|
||||||
executable simplex-broadcast-bot
|
executable simplex-broadcast-bot
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
|
@ -411,61 +318,20 @@ executable simplex-broadcast-bot
|
||||||
Paths_simplex_chat
|
Paths_simplex_chat
|
||||||
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
||||||
build-depends:
|
build-depends:
|
||||||
aeson ==2.2.*
|
async ==2.2.*
|
||||||
, ansi-terminal >=0.10 && <0.12
|
|
||||||
, async ==2.2.*
|
|
||||||
, attoparsec ==0.14.*
|
|
||||||
, base >=4.7 && <5
|
, base >=4.7 && <5
|
||||||
, base64-bytestring >=1.0 && <1.3
|
|
||||||
, composition ==1.0.*
|
|
||||||
, constraints >=0.12 && <0.14
|
|
||||||
, containers ==0.6.*
|
|
||||||
, crypton ==0.34.*
|
|
||||||
, data-default ==0.7.*
|
|
||||||
, direct-sqlcipher ==2.3.*
|
|
||||||
, directory ==1.3.*
|
, directory ==1.3.*
|
||||||
, email-validate ==2.3.*
|
|
||||||
, exceptions ==0.10.*
|
|
||||||
, file-embed ==0.0.15.*
|
|
||||||
, filepath ==1.4.*
|
|
||||||
, http-types ==0.12.*
|
|
||||||
, http2 >=4.2.2 && <4.3
|
|
||||||
, memory ==0.18.*
|
|
||||||
, mtl >=2.3.1 && <3.0
|
|
||||||
, network >=3.1.2.7 && <3.2
|
|
||||||
, network-transport ==0.5.6
|
|
||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
, random >=1.1 && <1.3
|
|
||||||
, record-hasfield ==1.0.*
|
|
||||||
, scientific ==0.3.7.*
|
|
||||||
, simple-logger ==0.1.*
|
|
||||||
, simplex-chat
|
, simplex-chat
|
||||||
, simplexmq >=5.0
|
, simplexmq >=6.3
|
||||||
, socks ==0.6.*
|
|
||||||
, sqlcipher-simple ==0.4.*
|
|
||||||
, stm ==2.5.*
|
, stm ==2.5.*
|
||||||
, terminal ==0.2.*
|
|
||||||
, time ==1.12.*
|
|
||||||
, tls >=1.9.0 && <1.10
|
|
||||||
, unliftio ==0.2.*
|
|
||||||
, unliftio-core ==0.2.*
|
|
||||||
, uuid ==1.3.*
|
|
||||||
, zip ==2.0.*
|
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
if flag(swift)
|
|
||||||
cpp-options: -DswiftJSON
|
|
||||||
if impl(ghc >= 9.6.2)
|
if impl(ghc >= 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.11.*
|
text >=2.0.1 && <2.2
|
||||||
, process ==1.6.*
|
|
||||||
, template-haskell ==2.20.*
|
|
||||||
, 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.*
|
text >=1.2.4.0 && <1.3
|
||||||
, process >=1.6 && <1.6.18
|
|
||||||
, template-haskell ==2.16.*
|
|
||||||
, text >=1.2.4.0 && <1.3
|
|
||||||
|
|
||||||
executable simplex-chat
|
executable simplex-chat
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
|
@ -479,61 +345,22 @@ executable simplex-chat
|
||||||
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
||||||
build-depends:
|
build-depends:
|
||||||
aeson ==2.2.*
|
aeson ==2.2.*
|
||||||
, ansi-terminal >=0.10 && <0.12
|
|
||||||
, async ==2.2.*
|
|
||||||
, attoparsec ==0.14.*
|
|
||||||
, base >=4.7 && <5
|
, base >=4.7 && <5
|
||||||
, base64-bytestring >=1.0 && <1.3
|
|
||||||
, composition ==1.0.*
|
|
||||||
, constraints >=0.12 && <0.14
|
|
||||||
, containers ==0.6.*
|
|
||||||
, crypton ==0.34.*
|
|
||||||
, data-default ==0.7.*
|
|
||||||
, direct-sqlcipher ==2.3.*
|
|
||||||
, directory ==1.3.*
|
, directory ==1.3.*
|
||||||
, email-validate ==2.3.*
|
|
||||||
, exceptions ==0.10.*
|
|
||||||
, file-embed ==0.0.15.*
|
|
||||||
, filepath ==1.4.*
|
|
||||||
, http-types ==0.12.*
|
|
||||||
, http2 >=4.2.2 && <4.3
|
|
||||||
, memory ==0.18.*
|
|
||||||
, mtl >=2.3.1 && <3.0
|
, mtl >=2.3.1 && <3.0
|
||||||
, network ==3.1.*
|
, network ==3.1.*
|
||||||
, network-transport ==0.5.6
|
|
||||||
, optparse-applicative >=0.15 && <0.17
|
|
||||||
, random >=1.1 && <1.3
|
|
||||||
, record-hasfield ==1.0.*
|
|
||||||
, scientific ==0.3.7.*
|
|
||||||
, simple-logger ==0.1.*
|
|
||||||
, simplex-chat
|
, simplex-chat
|
||||||
, simplexmq >=5.0
|
, simplexmq >=6.3
|
||||||
, socks ==0.6.*
|
|
||||||
, sqlcipher-simple ==0.4.*
|
|
||||||
, stm ==2.5.*
|
, stm ==2.5.*
|
||||||
, terminal ==0.2.*
|
|
||||||
, time ==1.12.*
|
|
||||||
, tls >=1.9.0 && <1.10
|
|
||||||
, unliftio ==0.2.*
|
, unliftio ==0.2.*
|
||||||
, unliftio-core ==0.2.*
|
|
||||||
, uuid ==1.3.*
|
|
||||||
, websockets ==0.12.*
|
, websockets ==0.12.*
|
||||||
, zip ==2.0.*
|
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
if flag(swift)
|
|
||||||
cpp-options: -DswiftJSON
|
|
||||||
if impl(ghc >= 9.6.2)
|
if impl(ghc >= 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.11.*
|
text >=2.0.1 && <2.2
|
||||||
, process ==1.6.*
|
|
||||||
, template-haskell ==2.20.*
|
|
||||||
, 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.*
|
text >=1.2.4.0 && <1.3
|
||||||
, process >=1.6 && <1.6.18
|
|
||||||
, template-haskell ==2.16.*
|
|
||||||
, text >=1.2.4.0 && <1.3
|
|
||||||
|
|
||||||
executable simplex-directory-service
|
executable simplex-directory-service
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
|
@ -551,60 +378,26 @@ executable simplex-directory-service
|
||||||
Paths_simplex_chat
|
Paths_simplex_chat
|
||||||
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded
|
||||||
build-depends:
|
build-depends:
|
||||||
aeson ==2.2.*
|
async ==2.2.*
|
||||||
, ansi-terminal >=0.10 && <0.12
|
|
||||||
, async ==2.2.*
|
|
||||||
, attoparsec ==0.14.*
|
, attoparsec ==0.14.*
|
||||||
, base >=4.7 && <5
|
, base >=4.7 && <5
|
||||||
, base64-bytestring >=1.0 && <1.3
|
|
||||||
, composition ==1.0.*
|
, composition ==1.0.*
|
||||||
, constraints >=0.12 && <0.14
|
|
||||||
, containers ==0.6.*
|
, containers ==0.6.*
|
||||||
, crypton ==0.34.*
|
|
||||||
, data-default ==0.7.*
|
|
||||||
, direct-sqlcipher ==2.3.*
|
|
||||||
, directory ==1.3.*
|
, directory ==1.3.*
|
||||||
, email-validate ==2.3.*
|
|
||||||
, exceptions ==0.10.*
|
|
||||||
, file-embed ==0.0.15.*
|
|
||||||
, filepath ==1.4.*
|
|
||||||
, http-types ==0.12.*
|
|
||||||
, http2 >=4.2.2 && <4.3
|
|
||||||
, memory ==0.18.*
|
|
||||||
, mtl >=2.3.1 && <3.0
|
|
||||||
, network >=3.1.2.7 && <3.2
|
|
||||||
, network-transport ==0.5.6
|
|
||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
, random >=1.1 && <1.3
|
|
||||||
, record-hasfield ==1.0.*
|
|
||||||
, scientific ==0.3.7.*
|
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
, simplex-chat
|
, simplex-chat
|
||||||
, simplexmq >=5.0
|
, simplexmq >=6.3
|
||||||
, socks ==0.6.*
|
|
||||||
, sqlcipher-simple ==0.4.*
|
|
||||||
, stm ==2.5.*
|
, stm ==2.5.*
|
||||||
, terminal ==0.2.*
|
|
||||||
, time ==1.12.*
|
, time ==1.12.*
|
||||||
, tls >=1.9.0 && <1.10
|
|
||||||
, unliftio ==0.2.*
|
|
||||||
, unliftio-core ==0.2.*
|
|
||||||
, uuid ==1.3.*
|
|
||||||
, zip ==2.0.*
|
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
if flag(swift)
|
|
||||||
cpp-options: -DswiftJSON
|
|
||||||
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.*
|
|
||||||
, 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.*
|
|
||||||
, text >=1.2.4.0 && <1.3
|
, text >=1.2.4.0 && <1.3
|
||||||
|
|
||||||
test-suite simplex-chat-test
|
test-suite simplex-chat-test
|
||||||
|
@ -659,60 +452,36 @@ test-suite simplex-chat-test
|
||||||
, base >=4.7 && <5
|
, base >=4.7 && <5
|
||||||
, base64-bytestring >=1.0 && <1.3
|
, base64-bytestring >=1.0 && <1.3
|
||||||
, composition ==1.0.*
|
, composition ==1.0.*
|
||||||
, constraints >=0.12 && <0.14
|
|
||||||
, containers ==0.6.*
|
, containers ==0.6.*
|
||||||
, crypton ==0.34.*
|
, crypton ==0.34.*
|
||||||
, data-default ==0.7.*
|
|
||||||
, deepseq ==1.4.*
|
, deepseq ==1.4.*
|
||||||
, direct-sqlcipher ==2.3.*
|
|
||||||
, directory ==1.3.*
|
, directory ==1.3.*
|
||||||
, email-validate ==2.3.*
|
|
||||||
, exceptions ==0.10.*
|
|
||||||
, file-embed ==0.0.15.*
|
|
||||||
, filepath ==1.4.*
|
, filepath ==1.4.*
|
||||||
, generic-random ==1.5.*
|
, generic-random ==1.5.*
|
||||||
, http-types ==0.12.*
|
, http-types ==0.12.*
|
||||||
, http2 >=4.2.2 && <4.3
|
|
||||||
, memory ==0.18.*
|
, memory ==0.18.*
|
||||||
, mtl >=2.3.1 && <3.0
|
, mtl >=2.3.1 && <3.0
|
||||||
, network ==3.1.*
|
, network ==3.1.*
|
||||||
, network-transport ==0.5.6
|
|
||||||
, optparse-applicative >=0.15 && <0.17
|
, optparse-applicative >=0.15 && <0.17
|
||||||
, random >=1.1 && <1.3
|
|
||||||
, record-hasfield ==1.0.*
|
|
||||||
, scientific ==0.3.7.*
|
|
||||||
, silently ==1.2.*
|
, silently ==1.2.*
|
||||||
, simple-logger ==0.1.*
|
, simple-logger ==0.1.*
|
||||||
, simplex-chat
|
, simplex-chat
|
||||||
, simplexmq >=5.0
|
, simplexmq >=6.3
|
||||||
, socks ==0.6.*
|
|
||||||
, sqlcipher-simple ==0.4.*
|
, sqlcipher-simple ==0.4.*
|
||||||
, stm ==2.5.*
|
, stm ==2.5.*
|
||||||
, terminal ==0.2.*
|
, terminal ==0.2.*
|
||||||
, time ==1.12.*
|
, time ==1.12.*
|
||||||
, tls >=1.9.0 && <1.10
|
|
||||||
, unliftio ==0.2.*
|
, unliftio ==0.2.*
|
||||||
, unliftio-core ==0.2.*
|
|
||||||
, uuid ==1.3.*
|
|
||||||
, zip ==2.0.*
|
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
if flag(swift)
|
|
||||||
cpp-options: -DswiftJSON
|
|
||||||
if impl(ghc >= 9.6.2)
|
if impl(ghc >= 9.6.2)
|
||||||
build-depends:
|
build-depends:
|
||||||
bytestring ==0.11.*
|
bytestring ==0.11.*
|
||||||
|
, hspec ==2.11.*
|
||||||
, process ==1.6.*
|
, process ==1.6.*
|
||||||
, 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.*
|
||||||
|
, hspec ==2.7.*
|
||||||
, process >=1.6 && <1.6.18
|
, process >=1.6 && <1.6.18
|
||||||
, template-haskell ==2.16.*
|
|
||||||
, text >=1.2.4.0 && <1.3
|
, text >=1.2.4.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.*
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ import Simplex.Chat.Protocol
|
||||||
import Simplex.Chat.Store
|
import Simplex.Chat.Store
|
||||||
import Simplex.Chat.Store.Profiles
|
import Simplex.Chat.Store.Profiles
|
||||||
import Simplex.Chat.Types
|
import Simplex.Chat.Types
|
||||||
import Simplex.Chat.Util (shuffle)
|
import Simplex.Chat.Util (encryptFile, liftIOEither, shuffle)
|
||||||
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
|
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
|
||||||
import Simplex.Messaging.Agent as Agent
|
import Simplex.Messaging.Agent as Agent
|
||||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), ServerRoles (..), allRoles, createAgentStore, defaultAgentConfig, presetServerCfg)
|
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), ServerRoles (..), allRoles, createAgentStore, defaultAgentConfig, presetServerCfg)
|
||||||
|
|
|
@ -73,11 +73,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis
|
||||||
|
|
||||||
-- when acting as host
|
-- when acting as host
|
||||||
minRemoteCtrlVersion :: AppVersion
|
minRemoteCtrlVersion :: AppVersion
|
||||||
minRemoteCtrlVersion = AppVersion [6, 2, 0, 7]
|
minRemoteCtrlVersion = AppVersion [6, 3, 0, 0]
|
||||||
|
|
||||||
-- when acting as controller
|
-- when acting as controller
|
||||||
minRemoteHostVersion :: AppVersion
|
minRemoteHostVersion :: AppVersion
|
||||||
minRemoteHostVersion = AppVersion [6, 2, 0, 7]
|
minRemoteHostVersion = AppVersion [6, 3, 0, 0]
|
||||||
|
|
||||||
currentAppVersion :: AppVersion
|
currentAppVersion :: AppVersion
|
||||||
currentAppVersion = AppVersion SC.version
|
currentAppVersion = AppVersion SC.version
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue