mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 12:19:54 +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 {
|
||||
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
|
||||
}
|
||||
} 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 {
|
||||
if let count = unreadTags[tag] {
|
||||
unreadTags[tag] = max(0, count - 1)
|
||||
|
|
|
@ -748,7 +748,11 @@ private func setChatTag(tagId: Int64?, chat: Chat, closeSheet: @escaping () -> V
|
|||
|
||||
await MainActor.run {
|
||||
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 {
|
||||
contact.chatTags = chatTags
|
||||
m.updateContact(contact)
|
||||
|
@ -756,6 +760,7 @@ private func setChatTag(tagId: Int64?, chat: Chat, closeSheet: @escaping () -> V
|
|||
group.chatTags = chatTags
|
||||
m.updateGroup(group)
|
||||
}
|
||||
ChatTagsModel.shared.updateChatTagRead(chat, wasUnread: false)
|
||||
closeSheet()
|
||||
}
|
||||
} catch let error {
|
||||
|
|
|
@ -53,7 +53,7 @@ struct OperatorView: View {
|
|||
ServersErrorView(errStr: errStr)
|
||||
} else {
|
||||
switch (userServers[operatorIndex].operator_.conditionsAcceptance) {
|
||||
case let .accepted(acceptedAt):
|
||||
case let .accepted(acceptedAt, _):
|
||||
if let acceptedAt = acceptedAt {
|
||||
Text("Conditions accepted on: \(conditionsTimestamp(acceptedAt)).")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
|
|
|
@ -167,9 +167,9 @@
|
|||
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; };
|
||||
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -673,9 +673,9 @@
|
|||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
649B28E12CFE07CF00536B68 /* libgmp.a 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 */,
|
||||
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 */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -756,8 +756,8 @@
|
|||
649B28D82CFE07CF00536B68 /* libffi.a */,
|
||||
649B28DC2CFE07CF00536B68 /* libgmp.a */,
|
||||
649B28DA2CFE07CF00536B68 /* libgmpxx.a */,
|
||||
649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW-ghc9.6.3.a */,
|
||||
649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.2.0.7-1UFyc6WJuaw6s2eEIvrMnW.a */,
|
||||
649B28D92CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t-ghc9.6.3.a */,
|
||||
649B28DB2CFE07CF00536B68 /* libHSsimplex-chat-6.3.0.0-asZoMipGotBmPgAUW0U4t.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1935,7 +1935,7 @@
|
|||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 258;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
|
@ -1960,7 +1960,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES_THIN;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -1984,7 +1984,7 @@
|
|||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 258;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
|
@ -2009,7 +2009,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2025,11 +2025,11 @@
|
|||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 258;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2045,11 +2045,11 @@
|
|||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 258;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2070,7 +2070,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 258;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
|
@ -2085,7 +2085,7 @@
|
|||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -2107,7 +2107,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 258;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
|
@ -2122,7 +2122,7 @@
|
|||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -2144,7 +2144,7 @@
|
|||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 258;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
|
@ -2170,7 +2170,7 @@
|
|||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2195,7 +2195,7 @@
|
|||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 258;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
|
@ -2221,7 +2221,7 @@
|
|||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2246,7 +2246,7 @@
|
|||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 258;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
|
@ -2261,7 +2261,7 @@
|
|||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2280,7 +2280,7 @@
|
|||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 255;
|
||||
CURRENT_PROJECT_VERSION = 258;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
|
@ -2295,7 +2295,7 @@
|
|||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.2.1;
|
||||
MARKETING_VERSION = 6.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
|
|
@ -1324,7 +1324,7 @@ public struct ServerOperatorConditions: Decodable {
|
|||
}
|
||||
|
||||
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.
|
||||
// No deadline indicates it's required to accept conditions for the operator to start using it.
|
||||
case required(deadline: Date?)
|
||||
|
@ -1398,7 +1398,7 @@ public struct ServerOperator: Identifiable, Equatable, Codable {
|
|||
tradeName: "SimpleX Chat",
|
||||
legalName: "SimpleX Chat Ltd",
|
||||
serverDomains: ["simplex.im"],
|
||||
conditionsAcceptance: .accepted(acceptedAt: nil),
|
||||
conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false),
|
||||
enabled: true,
|
||||
smpRoles: ServerRoles(storage: true, proxy: true),
|
||||
xftpRoles: ServerRoles(storage: true, proxy: true)
|
||||
|
@ -1431,7 +1431,7 @@ public struct UserOperatorServers: Identifiable, Equatable, Codable {
|
|||
tradeName: "",
|
||||
legalName: "",
|
||||
serverDomains: [],
|
||||
conditionsAcceptance: .accepted(acceptedAt: nil),
|
||||
conditionsAcceptance: .accepted(acceptedAt: nil, autoAccepted: false),
|
||||
enabled: false,
|
||||
smpRoles: 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_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
|
||||
android:name="SimplexApp"
|
||||
android:allowBackup="false"
|
||||
|
|
|
@ -470,53 +470,65 @@ class SimplexService: Service() {
|
|||
)
|
||||
}
|
||||
|
||||
private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode, showOffAlert: Boolean) = AlertManager.shared.showAlert {
|
||||
val ignoreOptimization = {
|
||||
AlertManager.shared.hideAlert()
|
||||
askAboutIgnoringBatteryOptimization()
|
||||
private var showingIgnoreNotification = false
|
||||
private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode, showOffAlert: Boolean) {
|
||||
// that's workaround for situation when the app receives onPause/onResume events multiple times
|
||||
// (for example, after showing system alert for enabling notifications) which triggers showing that alert multiple times
|
||||
if (showingIgnoreNotification) {
|
||||
return
|
||||
}
|
||||
val disableNotifications = {
|
||||
AlertManager.shared.hideAlert()
|
||||
disableNotifications(mode, showOffAlert)
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = disableNotifications,
|
||||
title = {
|
||||
Row {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_bolt),
|
||||
contentDescription =
|
||||
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
|
||||
)
|
||||
Text(
|
||||
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications) else stringResource(MR.strings.periodic_notifications),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
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),
|
||||
Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
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)
|
||||
showingIgnoreNotification = true
|
||||
AlertManager.shared.showAlert {
|
||||
val ignoreOptimization = {
|
||||
AlertManager.shared.hideAlert()
|
||||
showingIgnoreNotification = false
|
||||
askAboutIgnoringBatteryOptimization()
|
||||
}
|
||||
val disableNotifications = {
|
||||
AlertManager.shared.hideAlert()
|
||||
showingIgnoreNotification = false
|
||||
disableNotifications(mode, showOffAlert)
|
||||
}
|
||||
AlertDialog(
|
||||
onDismissRequest = disableNotifications,
|
||||
title = {
|
||||
Row {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_bolt),
|
||||
contentDescription =
|
||||
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications),
|
||||
)
|
||||
Text(
|
||||
if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications) else stringResource(MR.strings.periodic_notifications),
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
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))
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
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),
|
||||
Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
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 {
|
||||
|
|
|
@ -87,6 +87,9 @@ kotlin {
|
|||
implementation("io.coil-kt:coil-compose: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")
|
||||
|
||||
val cameraXVersion = "1.3.4"
|
||||
|
|
|
@ -3,19 +3,30 @@ package chat.simplex.common.platform
|
|||
import android.Manifest
|
||||
import android.content.*
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
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.UriHandler
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import chat.simplex.common.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import chat.simplex.res.MR
|
||||
import java.net.URI
|
||||
import kotlin.math.min
|
||||
|
||||
data class OpenDefaultApp(
|
||||
val name: String,
|
||||
val icon: ImageBitmap,
|
||||
val isSystemChooser: Boolean
|
||||
)
|
||||
|
||||
actual fun ClipboardManager.shareText(text: String) {
|
||||
var text = text
|
||||
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 tmpFile = File(tmpDir, fileSource.filePath)
|
||||
tmpFile.deleteOnExit()
|
||||
|
@ -67,9 +78,35 @@ fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean) {
|
|||
type = mimeType
|
||||
}
|
||||
}
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||
androidAppContext.startActivity(shareIntent)
|
||||
if (useChooser) {
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
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) {
|
||||
|
|
|
@ -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 androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import chat.simplex.common.platform.ntfManager
|
||||
import com.google.accompanist.permissions.PermissionStatus
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
import com.google.accompanist.permissions.*
|
||||
|
||||
@Composable
|
||||
actual fun SetNotificationsModeAdditions() {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
val notificationsPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
|
||||
LaunchedEffect(notificationsPermissionState.status == PermissionStatus.Granted) {
|
||||
if (notificationsPermissionState.status == PermissionStatus.Granted) {
|
||||
ntfManager.androidCreateNtfChannelsMaybeShowAlert()
|
||||
val canAsk = appPrefs.canAskToEnableNotifications.get()
|
||||
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 {
|
||||
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 {
|
||||
|
|
|
@ -13,6 +13,7 @@ import chat.simplex.common.platform.*
|
|||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.migration.MigrationToDeviceState
|
||||
import chat.simplex.common.views.migration.MigrationToState
|
||||
|
@ -81,6 +82,12 @@ object ChatModel {
|
|||
val groupMembers = mutableStateListOf<GroupMember>()
|
||||
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.
|
||||
// Used for deciding to add terminal items on main thread or not. Floating means appPrefs.terminalAlwaysVisible
|
||||
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
|
||||
fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null
|
||||
// TODO pass rhId?
|
||||
|
@ -280,6 +397,7 @@ object ChatModel {
|
|||
updateChatInfo(rhId, cInfo)
|
||||
} else if (addMissing) {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf()))
|
||||
addPresetChatTags(cInfo)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -329,6 +447,7 @@ object ChatModel {
|
|||
}
|
||||
else -> cItem
|
||||
}
|
||||
val wasUnread = chat.unreadTag
|
||||
chats[i] = chat.copy(
|
||||
chatItems = arrayListOf(newPreviewItem),
|
||||
chatStats =
|
||||
|
@ -339,6 +458,8 @@ object ChatModel {
|
|||
else
|
||||
chat.chatStats
|
||||
)
|
||||
updateChatTagRead(chats[i], wasUnread)
|
||||
|
||||
if (appPlatform.isDesktop && cItem.chatDir.sent) {
|
||||
reorderChat(chats[i], 0)
|
||||
} else {
|
||||
|
@ -455,6 +576,7 @@ object ChatModel {
|
|||
if (i >= 0) {
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount)
|
||||
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
|
||||
markChatTagRead(chats[i])
|
||||
}
|
||||
// clear current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
|
@ -522,11 +644,13 @@ object ChatModel {
|
|||
val chat = chats[chatIdx]
|
||||
val lastId = chat.chatItems.lastOrNull()?.id
|
||||
if (lastId != null) {
|
||||
val wasUnread = chat.unreadTag
|
||||
val unreadCount = if (itemIds != null) chat.chatStats.unreadCount - markedRead else 0
|
||||
decreaseUnreadCounter(remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIdx] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(unreadCount = unreadCount)
|
||||
)
|
||||
updateChatTagRead(chats[chatIdx], wasUnread)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -537,16 +661,29 @@ object ChatModel {
|
|||
|
||||
val chat = chats[chatIndex]
|
||||
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
|
||||
val wasUnread = chat.unreadTag
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIndex] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCount,
|
||||
)
|
||||
)
|
||||
updateChatTagRead(chats[chatIndex], wasUnread)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -977,6 +1114,8 @@ data class Chat(
|
|||
else -> false
|
||||
}
|
||||
|
||||
val unreadTag: Boolean get() = chatInfo.ntfsEnabled && (chatStats.unreadCount > 0 || chatStats.unreadChat)
|
||||
|
||||
val id: String get() = chatInfo.id
|
||||
|
||||
fun groupFeatureEnabled(feature: GroupFeature): Boolean =
|
||||
|
@ -1189,6 +1328,12 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||
else -> false
|
||||
}
|
||||
|
||||
val chatTags: List<Long>?
|
||||
get() = when (this) {
|
||||
is Direct -> contact.chatTags
|
||||
is Group -> groupInfo.chatTags
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
@ -1232,6 +1377,7 @@ data class Contact(
|
|||
val chatTs: Instant?,
|
||||
val contactGroupMemberId: Long? = null,
|
||||
val contactGrpInvSent: Boolean,
|
||||
val chatTags: List<Long>,
|
||||
override val chatDeleted: Boolean,
|
||||
val uiThemes: ThemeModeOverrides? = null,
|
||||
): SomeChat, NamedChat {
|
||||
|
@ -1315,6 +1461,7 @@ data class Contact(
|
|||
contactGrpInvSent = false,
|
||||
chatDeleted = false,
|
||||
uiThemes = null,
|
||||
chatTags = emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1476,6 +1623,7 @@ data class GroupInfo (
|
|||
override val updatedAt: Instant,
|
||||
val chatTs: Instant?,
|
||||
val uiThemes: ThemeModeOverrides? = null,
|
||||
val chatTags: List<Long>
|
||||
): SomeChat, NamedChat {
|
||||
override val chatType get() = ChatType.Group
|
||||
override val id get() = "#$groupId"
|
||||
|
@ -1520,6 +1668,7 @@ data class GroupInfo (
|
|||
updatedAt = Clock.System.now(),
|
||||
chatTs = Clock.System.now(),
|
||||
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
|
||||
class ChatItemInfo(
|
||||
val itemVersions: List<ChatItemVersion>,
|
||||
|
|
|
@ -80,6 +80,7 @@ class AppPreferences {
|
|||
if (!runServiceInBackground.get()) NotificationsMode.OFF else NotificationsMode.default
|
||||
) { NotificationsMode.values().firstOrNull { it.name == this } }
|
||||
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 backgroundServiceBatteryNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false)
|
||||
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_NOTIFICATIONS_MODE = "NotificationsMode"
|
||||
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_BATTERY_NOTICE_SHOWN = "BackgroundServiceBatteryNoticeShown"
|
||||
private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay"
|
||||
|
@ -624,6 +626,9 @@ object ChatController {
|
|||
val chats = apiGetChats(rhId)
|
||||
updateChats(chats)
|
||||
}
|
||||
chatModel.userTags.value = apiGetChatTags(rhId).takeIf { hasUser } ?: emptyList()
|
||||
chatModel.activeChatTagFilter.value = null
|
||||
chatModel.updateChatTags(rhId)
|
||||
}
|
||||
|
||||
private fun startReceiver() {
|
||||
|
@ -879,6 +884,16 @@ object ChatController {
|
|||
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>? {
|
||||
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
|
||||
|
@ -891,6 +906,28 @@ object ChatController {
|
|||
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>? {
|
||||
val cmd = CC.ApiSendMessages(type, id, live, ttl, composedMessages)
|
||||
return processSendMessageCmd(rh, cmd)
|
||||
|
@ -3152,10 +3189,16 @@ sealed class CC {
|
|||
class TestStorageEncryption(val key: String): CC()
|
||||
class ApiSaveSettings(val settings: AppSettings): CC()
|
||||
class ApiGetSettings(val settings: AppSettings): CC()
|
||||
class ApiGetChatTags(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 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 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 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()
|
||||
|
@ -3307,6 +3350,7 @@ sealed class CC {
|
|||
is TestStorageEncryption -> "/db test key $key"
|
||||
is ApiSaveSettings -> "/_save 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 ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
|
||||
|
@ -3315,6 +3359,11 @@ sealed class CC {
|
|||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||
"/_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 -> {
|
||||
val msgs = json.encodeToString(composedMessages)
|
||||
"/_create *$noteFolderId json $msgs"
|
||||
|
@ -3471,10 +3520,16 @@ sealed class CC {
|
|||
is TestStorageEncryption -> "testStorageEncryption"
|
||||
is ApiSaveSettings -> "apiSaveSettings"
|
||||
is ApiGetSettings -> "apiGetSettings"
|
||||
is ApiGetChatTags -> "apiGetChatTags"
|
||||
is ApiGetChats -> "apiGetChats"
|
||||
is ApiGetChat -> "apiGetChat"
|
||||
is ApiGetChatItemInfo -> "apiGetChatItemInfo"
|
||||
is ApiSendMessages -> "apiSendMessages"
|
||||
is ApiCreateChatTag -> "apiCreateChatTag"
|
||||
is ApiSetChatTags -> "apiSetChatTags"
|
||||
is ApiDeleteChatTag -> "apiDeleteChatTag"
|
||||
is ApiUpdateChatTag -> "apiUpdateChatTag"
|
||||
is ApiReorderChatTags -> "apiReorderChatTags"
|
||||
is ApiCreateChatItems -> "apiCreateChatItems"
|
||||
is ApiUpdateChatItem -> "apiUpdateChatItem"
|
||||
is ApiDeleteChatItem -> "apiDeleteChatItem"
|
||||
|
@ -3657,6 +3712,9 @@ sealed class ChatPagination {
|
|||
@Serializable
|
||||
class ComposedMessage(val fileSource: CryptoFile?, val quotedItemId: Long?, val msgContent: MsgContent)
|
||||
|
||||
@Serializable
|
||||
class ChatTagData(val emoji: String?, val text: String)
|
||||
|
||||
@Serializable
|
||||
class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null)
|
||||
|
||||
|
@ -3757,7 +3815,7 @@ data class ServerOperatorConditionsDetail(
|
|||
|
||||
@Serializable()
|
||||
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()
|
||||
|
||||
val conditionsAccepted: Boolean
|
||||
|
@ -3801,7 +3859,7 @@ data class ServerOperator(
|
|||
tradeName = "SimpleX Chat",
|
||||
legalName = "SimpleX Chat Ltd",
|
||||
serverDomains = listOf("simplex.im"),
|
||||
conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null),
|
||||
conditionsAcceptance = ConditionsAcceptance.Accepted(acceptedAt = null, autoAccepted = false),
|
||||
enabled = true,
|
||||
smpRoles = ServerRoles(storage = true, proxy = true),
|
||||
xftpRoles = ServerRoles(storage = true, proxy = true)
|
||||
|
@ -3883,7 +3941,7 @@ data class UserOperatorServers(
|
|||
tradeName = "",
|
||||
legalName = null,
|
||||
serverDomains = emptyList(),
|
||||
conditionsAcceptance = ConditionsAcceptance.Accepted(null),
|
||||
conditionsAcceptance = ConditionsAcceptance.Accepted(null, autoAccepted = false),
|
||||
enabled = false,
|
||||
smpRoles = 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("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("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("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): 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("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("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("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()
|
||||
|
@ -5574,6 +5634,7 @@ sealed class CR {
|
|||
is ChatStopped -> "chatStopped"
|
||||
is ApiChats -> "apiChats"
|
||||
is ApiChat -> "apiChat"
|
||||
is ChatTags -> "chatTags"
|
||||
is ApiChatItemInfo -> "chatItemInfo"
|
||||
is ServerTestResult -> "serverTestResult"
|
||||
is ServerOperatorConditions -> "serverOperatorConditions"
|
||||
|
@ -5600,6 +5661,7 @@ sealed class CR {
|
|||
is ContactCode -> "contactCode"
|
||||
is GroupMemberCode -> "groupMemberCode"
|
||||
is ConnectionVerified -> "connectionVerified"
|
||||
is TagsUpdated -> "tagsUpdated"
|
||||
is Invitation -> "invitation"
|
||||
is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated"
|
||||
is ConnectionUserChanged -> "ConnectionUserChanged"
|
||||
|
@ -5748,6 +5810,7 @@ sealed class CR {
|
|||
is ChatStopped -> noDetails()
|
||||
is ApiChats -> withUser(user, json.encodeToString(chats))
|
||||
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 ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
|
||||
is ServerOperatorConditions -> "conditions: ${json.encodeToString(conditions)}"
|
||||
|
@ -5774,6 +5837,7 @@ sealed class CR {
|
|||
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 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 ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
|
||||
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)
|
||||
}
|
||||
)
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val copyNameToClipboard = {
|
||||
clipboard.setText(AnnotatedString(contact.profile.displayName))
|
||||
showToast(generalGetString(MR.strings.copied))
|
||||
}
|
||||
Text(
|
||||
text,
|
||||
inlineContent = inlineContent,
|
||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
textAlign = TextAlign.Center,
|
||||
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) {
|
||||
Text(
|
||||
|
@ -711,7 +717,8 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
|
|||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
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.ChatModel.activeCall
|
||||
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.ui.theme.*
|
||||
import chat.simplex.common.views.call.*
|
||||
|
@ -2106,6 +2107,7 @@ private fun markUnreadChatAsRead(chatId: String) {
|
|||
if (success) {
|
||||
withChats {
|
||||
replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
||||
markChatTagRead(chat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import SectionSpacer
|
|||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.material.*
|
||||
|
@ -17,6 +18,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
@ -446,12 +449,18 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
|
|||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
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(
|
||||
cInfo.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 4,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||
)
|
||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) {
|
||||
Text(
|
||||
|
@ -459,7 +468,8 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo) {
|
|||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
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 SectionView
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
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)
|
||||
}
|
||||
)
|
||||
val clipboard = LocalClipboardManager.current
|
||||
val copyNameToClipboard = {
|
||||
clipboard.setText(AnnotatedString(member.displayName))
|
||||
showToast(generalGetString(MR.strings.copied))
|
||||
}
|
||||
Text(
|
||||
text,
|
||||
inlineContent = inlineContent,
|
||||
style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.combinedClickable(onClick = copyNameToClipboard, onLongClick = copyNameToClipboard).onRightClick(copyNameToClipboard)
|
||||
)
|
||||
if (member.fullName != "" && member.fullName != member.displayName) {
|
||||
Text(
|
||||
|
@ -551,7 +558,8 @@ fun GroupMemberInfoHeader(member: GroupMember) {
|
|||
color = MaterialTheme.colors.onBackground,
|
||||
textAlign = TextAlign.Center,
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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(
|
||||
Modifier
|
||||
.combinedClickable(
|
||||
onClick = { fileAction() },
|
||||
onClick = {
|
||||
if (appPlatform.isAndroid && loadedFilePath != null) {
|
||||
showOpenSaveMenu.value = true
|
||||
} else {
|
||||
fileAction()
|
||||
}
|
||||
},
|
||||
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())),
|
||||
//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,
|
||||
horizontalArrangement = Arrangement.spacedBy(2.sp.toDp())
|
||||
) {
|
||||
|
@ -223,6 +235,16 @@ fun CIFileView(
|
|||
|
||||
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
|
||||
fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher =
|
||||
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
|
||||
fun ItemAction(
|
||||
text: String,
|
||||
|
|
|
@ -4,28 +4,34 @@ import SectionItemView
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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.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.TextStyle
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
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.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chat.group.deleteGroupDialog
|
||||
import chat.simplex.common.views.chat.group.leaveGroupDialog
|
||||
import chat.simplex.common.views.chat.group.*
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.contacts.onRequestAccepted
|
||||
import chat.simplex.common.views.helpers.*
|
||||
|
@ -33,7 +39,6 @@ import chat.simplex.common.views.newchat.*
|
|||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.math.min
|
||||
|
||||
@Composable
|
||||
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)
|
||||
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
|
||||
TagListAction(chat, showMenu)
|
||||
ClearChatAction(chat, showMenu)
|
||||
}
|
||||
DeleteContactAction(chat, chatModel, showMenu)
|
||||
|
@ -291,6 +297,7 @@ fun GroupMenuItems(
|
|||
}
|
||||
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
|
||||
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
|
||||
TagListAction(chat, showMenu)
|
||||
ClearChatAction(chat, showMenu)
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
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
|
||||
fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolean, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
|
@ -557,6 +586,7 @@ fun markChatRead(c: Chat, chatModel: ChatModel) {
|
|||
if (success) {
|
||||
withChats {
|
||||
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
|
||||
|
||||
withApi {
|
||||
val wasUnread = chat.unreadTag
|
||||
val success = chatModel.controller.apiChatUnread(
|
||||
chat.remoteHostId,
|
||||
chat.chatInfo.chatType,
|
||||
|
@ -577,6 +608,7 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
|
|||
if (success) {
|
||||
withChats {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
updateChatInfo(remoteHostId, newChatInfo)
|
||||
}
|
||||
if (chatSettings.enableNtfs != MsgFilter.All) {
|
||||
ntfManager.cancelNotificationsForChat(chatInfo.id)
|
||||
}
|
||||
val updatedChat = chatModel.getChat(chatInfo.id)
|
||||
if (updatedChat != null) {
|
||||
chatModel.updateChatTagRead(updatedChat, wasUnread)
|
||||
}
|
||||
val current = currentState?.value
|
||||
if (current != null) {
|
||||
currentState.value = !current
|
||||
|
|
|
@ -16,11 +16,13 @@ import androidx.compose.ui.focus.*
|
|||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
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.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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 chat.simplex.common.AppLock
|
||||
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.platform.*
|
||||
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.mkValidName
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.common.views.onboarding.*
|
||||
import chat.simplex.common.views.showInvalidNameAlert
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.ConditionsLinkButton
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.UsageConditionsView
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.serialization.json.Json
|
||||
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>) {
|
||||
ModalManager.start.closeModals()
|
||||
ModalManager.end.closeModals()
|
||||
|
@ -187,6 +197,12 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate
|
|||
}
|
||||
}
|
||||
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 = {}) {
|
||||
UserPicker(
|
||||
chatModel = chatModel,
|
||||
|
@ -557,17 +573,24 @@ private fun BoxScope.unreadBadge(text: String? = "") {
|
|||
|
||||
@Composable
|
||||
private fun ToggleFilterEnabledButton() {
|
||||
val pref = remember { ChatController.appPrefs.showUnreadAndFavorites }
|
||||
IconButton(onClick = { pref.set(!pref.get()) }) {
|
||||
val showUnread = remember { chatModel.activeChatTagFilter }.value == ActiveFilter.Unread
|
||||
|
||||
IconButton(onClick = {
|
||||
if (showUnread) {
|
||||
chatModel.activeChatTagFilter.value = null
|
||||
} else {
|
||||
chatModel.activeChatTagFilter.value = ActiveFilter.Unread
|
||||
}
|
||||
}) {
|
||||
val sp16 = with(LocalDensity.current) { 16.sp.toDp() }
|
||||
Icon(
|
||||
painterResource(MR.images.ic_filter_list),
|
||||
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
|
||||
.padding(3.dp)
|
||||
.background(color = if (pref.state.value) 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))
|
||||
.background(color = if (showUnread) 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)
|
||||
.size(sp16)
|
||||
)
|
||||
|
@ -731,6 +754,7 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
|||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state }
|
||||
val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state }
|
||||
val activeFilter = remember { chatModel.activeChatTagFilter }
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||
val currentIndex = listState.firstVisibleItemIndex
|
||||
|
@ -753,14 +777,13 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
|||
DisposableEffect(Unit) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value
|
||||
val allChats = remember { chatModel.chats }
|
||||
// In some not always reproducible situations this code produce IndexOutOfBoundsException on Compose's side
|
||||
// which is related to [derivedStateOf]. Using safe alternative instead
|
||||
// val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
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 blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent
|
||||
LazyColumnWithScrollBar(
|
||||
|
@ -791,11 +814,15 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
|
|||
) {
|
||||
if (oneHandUI.value) {
|
||||
Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) {
|
||||
Divider()
|
||||
TagsView()
|
||||
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
|
||||
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
|
||||
}
|
||||
} else {
|
||||
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()) {
|
||||
Box(Modifier.fillMaxSize().imePadding(), contentAlignment = Alignment.Center) {
|
||||
Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary)
|
||||
Box(Modifier.fillMaxSize().imePadding().padding(horizontal = DEFAULT_PADDING), contentAlignment = Alignment.Center) {
|
||||
NoChatsView(searchText = searchText)
|
||||
}
|
||||
}
|
||||
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
|
||||
|
@ -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(
|
||||
showUnreadAndFavorites: Boolean,
|
||||
searchShowingSimplexLink: State<Boolean>,
|
||||
searchChatFilteredBySimplexLink: State<String?>,
|
||||
searchText: String,
|
||||
chats: List<Chat>
|
||||
chats: List<Chat>,
|
||||
activeFilter: ActiveFilter? = null,
|
||||
): List<Chat> {
|
||||
val linkChatId = searchChatFilteredBySimplexLink.value
|
||||
return if (linkChatId != null) {
|
||||
chats.filter { it.id == linkChatId }
|
||||
} else {
|
||||
val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase()
|
||||
if (s.isEmpty() && !showUnreadAndFavorites)
|
||||
chats.filter { chat -> !chat.chatInfo.chatDeleted && chatContactType(chat) != ContactType.CARD }
|
||||
if (s.isEmpty())
|
||||
chats.filter { chat -> !chat.chatInfo.chatDeleted && chatContactType(chat) != ContactType.CARD && filtered(chat, activeFilter) }
|
||||
else {
|
||||
chats.filter { chat ->
|
||||
when (val cInfo = chat.chatInfo) {
|
||||
is ChatInfo.Direct -> chatContactType(chat) != ContactType.CARD && !chat.chatInfo.chatDeleted && (
|
||||
if (s.isEmpty()) {
|
||||
chat.id == chatModel.chatId.value || filtered(chat)
|
||||
chat.id == chatModel.chatId.value || filtered(chat, activeFilter)
|
||||
} else {
|
||||
cInfo.anyNameContains(s)
|
||||
})
|
||||
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 {
|
||||
cInfo.anyNameContains(s)
|
||||
}
|
||||
|
@ -898,10 +1230,41 @@ fun filteredChats(
|
|||
}
|
||||
}
|
||||
|
||||
private fun filtered(chat: Chat): Boolean =
|
||||
(chat.chatInfo.chatSettings?.favorite ?: false) ||
|
||||
chat.chatStats.unreadChat ||
|
||||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
|
||||
private fun filtered(chat: Chat, activeFilter: ActiveFilter?): Boolean =
|
||||
when (activeFilter) {
|
||||
is ActiveFilter.PresetTag -> presetTagMatchesChat(activeFilter.tag, chat.chatInfo)
|
||||
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) {
|
||||
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) {
|
||||
derivedStateOf {
|
||||
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)
|
||||
|
|
|
@ -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="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="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 -->
|
||||
<string name="icon_descr_instant_notifications">Instant notifications</string>
|
||||
|
@ -361,6 +364,7 @@
|
|||
<string name="revoke_file__confirm">Revoke</string>
|
||||
<string name="forward_chat_item">Forward</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_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="loading_chats">Loading 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="connect_with_contact_name_question">Connect with %1$s?</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_not_accepted_receive_files">Download</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 -->
|
||||
<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="file_error">File error</string>
|
||||
<string name="temporary_file_error">Temporary file error</string>
|
||||
<string name="open_with_app">Open with %s</string>
|
||||
|
||||
<!-- Voice messages -->
|
||||
<string name="voice_message">Voice message</string>
|
||||
|
@ -626,6 +641,16 @@
|
|||
<string name="favorite_chat">Favorite</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 -->
|
||||
<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.interaction.InteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
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.jvm.target=11
|
||||
|
||||
android.version_name=6.2.1
|
||||
android.version_code=261
|
||||
android.version_name=6.3-beta.0
|
||||
android.version_code=267
|
||||
|
||||
desktop.version_name=6.2.1
|
||||
desktop.version_code=83
|
||||
desktop.version_name=6.3-beta.0
|
||||
desktop.version_code=86
|
||||
|
||||
kotlin.version=1.9.23
|
||||
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
|
||||
|
||||
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:
|
||||
- messaging servers no longer lose notifications while notification servers are restarted.
|
||||
|
|
|
@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: f5cef2556b05cf384db284644c112f822407b361
|
||||
tag: 184a95cd2a2806fb3d318508ca1f6528201a8114
|
||||
|
||||
source-repository-package
|
||||
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>
|
||||
|
||||
<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">
|
||||
<url type="details">https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html</url>
|
||||
<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/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
|
|
|
@ -5,7 +5,7 @@ cabal-version: 1.12
|
|||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 6.2.0.7
|
||||
version: 6.3.0.0
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
@ -239,7 +239,7 @@ library
|
|||
, record-hasfield ==1.0.*
|
||||
, scientific ==0.3.7.*
|
||||
, simple-logger ==0.1.*
|
||||
, simplexmq >=5.0
|
||||
, simplexmq >=6.3
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, stm ==2.5.*
|
||||
|
@ -276,61 +276,10 @@ executable simplex-bot
|
|||
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
|
||||
build-depends:
|
||||
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.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
base >=4.7 && <5
|
||||
, 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
|
||||
, 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
|
||||
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
|
||||
main-is: Main.hs
|
||||
|
@ -342,61 +291,19 @@ executable simplex-bot-advanced
|
|||
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
|
||||
build-depends:
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
async ==2.2.*
|
||||
, 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.*
|
||||
, 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
|
||||
, simplexmq >=5.0
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, simplexmq >=6.3
|
||||
, 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
|
||||
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
|
||||
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
|
||||
text >=1.2.4.0 && <1.3
|
||||
|
||||
executable simplex-broadcast-bot
|
||||
main-is: Main.hs
|
||||
|
@ -411,61 +318,20 @@ executable simplex-broadcast-bot
|
|||
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
|
||||
build-depends:
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, async ==2.2.*
|
||||
, attoparsec ==0.14.*
|
||||
async ==2.2.*
|
||||
, 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.*
|
||||
, 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
|
||||
, simplexmq >=5.0
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, simplexmq >=6.3
|
||||
, 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
|
||||
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
|
||||
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
|
||||
text >=1.2.4.0 && <1.3
|
||||
|
||||
executable simplex-chat
|
||||
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
|
||||
build-depends:
|
||||
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.*
|
||||
, direct-sqlcipher ==2.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.*
|
||||
, 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
|
||||
, simplexmq >=5.0
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, simplexmq >=6.3
|
||||
, 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.*
|
||||
, websockets ==0.12.*
|
||||
, zip ==2.0.*
|
||||
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
|
||||
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
|
||||
text >=1.2.4.0 && <1.3
|
||||
|
||||
executable simplex-directory-service
|
||||
main-is: Main.hs
|
||||
|
@ -551,60 +378,26 @@ executable simplex-directory-service
|
|||
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
|
||||
build-depends:
|
||||
aeson ==2.2.*
|
||||
, ansi-terminal >=0.10 && <0.12
|
||||
, async ==2.2.*
|
||||
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.*
|
||||
, 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
|
||||
, simplexmq >=5.0
|
||||
, socks ==0.6.*
|
||||
, sqlcipher-simple ==0.4.*
|
||||
, simplexmq >=6.3
|
||||
, 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
|
||||
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
|
||||
|
||||
test-suite simplex-chat-test
|
||||
|
@ -659,60 +452,36 @@ test-suite simplex-chat-test
|
|||
, 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.*
|
||||
, deepseq ==1.4.*
|
||||
, direct-sqlcipher ==2.3.*
|
||||
, directory ==1.3.*
|
||||
, email-validate ==2.3.*
|
||||
, exceptions ==0.10.*
|
||||
, file-embed ==0.0.15.*
|
||||
, filepath ==1.4.*
|
||||
, generic-random ==1.5.*
|
||||
, http-types ==0.12.*
|
||||
, http2 >=4.2.2 && <4.3
|
||||
, memory ==0.18.*
|
||||
, mtl >=2.3.1 && <3.0
|
||||
, 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.*
|
||||
, silently ==1.2.*
|
||||
, simple-logger ==0.1.*
|
||||
, simplex-chat
|
||||
, simplexmq >=5.0
|
||||
, socks ==0.6.*
|
||||
, simplexmq >=6.3
|
||||
, 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
|
||||
if flag(swift)
|
||||
cpp-options: -DswiftJSON
|
||||
if impl(ghc >= 9.6.2)
|
||||
build-depends:
|
||||
bytestring ==0.11.*
|
||||
, hspec ==2.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.*
|
||||
, hspec ==2.7.*
|
||||
, process >=1.6 && <1.6.18
|
||||
, template-haskell ==2.16.*
|
||||
, 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.Profiles
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (shuffle)
|
||||
import Simplex.Chat.Util (encryptFile, liftIOEither, shuffle)
|
||||
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
|
||||
import Simplex.Messaging.Agent as Agent
|
||||
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
|
||||
minRemoteCtrlVersion :: AppVersion
|
||||
minRemoteCtrlVersion = AppVersion [6, 2, 0, 7]
|
||||
minRemoteCtrlVersion = AppVersion [6, 3, 0, 0]
|
||||
|
||||
-- when acting as controller
|
||||
minRemoteHostVersion :: AppVersion
|
||||
minRemoteHostVersion = AppVersion [6, 2, 0, 7]
|
||||
minRemoteHostVersion = AppVersion [6, 3, 0, 0]
|
||||
|
||||
currentAppVersion :: AppVersion
|
||||
currentAppVersion = AppVersion SC.version
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue