mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-14 07:10:19 +00:00
Merge branch 'master' into master-android
This commit is contained in:
@@ -77,6 +77,24 @@ struct CIMsgError: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct RcvMsgErrorItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
var rcvMsgError: RcvMsgError
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
CIMsgError(chat: chat, chatItem: chatItem) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Message error",
|
||||
message: switch rcvMsgError {
|
||||
case let .dropped(attempts): "The app removed this message after \(attempts) attempts to receive it."
|
||||
case let .parseError(parseError): "\(parseError)"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IntegrityErrorItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
IntegrityErrorItemView(chat: Chat.sampleData, msgError: .msgBadHash, chatItem: ChatItem.getIntegrityErrorSample())
|
||||
|
||||
@@ -145,6 +145,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
} else {
|
||||
ZStack {}
|
||||
}
|
||||
case let .rcvMsgError(rcvMsgError): RcvMsgErrorItemView(chat: chat, rcvMsgError: rcvMsgError, chatItem: chatItem)
|
||||
case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(chat: chat, msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem)
|
||||
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
||||
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
||||
@@ -195,7 +196,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
}
|
||||
|
||||
private func pendingReviewEventItemText() -> Text {
|
||||
Text(chatItem.content.text)
|
||||
Text(chatItem.content.text(isChannel: chat.chatInfo.isChannel))
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.fontWeight(.bold)
|
||||
@@ -209,9 +210,9 @@ struct ChatItemContentView<Content: View>: View {
|
||||
.font(.caption)
|
||||
.foregroundColor(secondaryColor)
|
||||
.fontWeight(.light)
|
||||
+ chatEventText(chatItem, secondaryColor)
|
||||
+ chatEventText(chatItem, secondaryColor, isChannel: chat.chatInfo.isChannel)
|
||||
} else {
|
||||
return chatEventText(chatItem, secondaryColor)
|
||||
return chatEventText(chatItem, secondaryColor, isChannel: chat.chatInfo.isChannel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +235,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
return if count <= 1 {
|
||||
nil
|
||||
} else if ns.count == 0 {
|
||||
Text("\(count) group events")
|
||||
chat.chatInfo.isChannel ? Text("\(count) channel events") : Text("\(count) group events")
|
||||
} else if count > ns.count {
|
||||
Text(members) + textSpace + Text("and \(count - ns.count) other events")
|
||||
} else {
|
||||
@@ -275,8 +276,8 @@ func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text, _ secondaryColor
|
||||
chatEventText(Text(eventText) + textSpace + ts, secondaryColor)
|
||||
}
|
||||
|
||||
func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text {
|
||||
chatEventText("\(ci.content.text)", ci.timestampText, secondaryColor)
|
||||
func chatEventText(_ ci: ChatItem, _ secondaryColor: Color, isChannel: Bool = false) -> Text {
|
||||
chatEventText("\(ci.content.text(isChannel: isChannel))", ci.timestampText, secondaryColor)
|
||||
}
|
||||
|
||||
struct ChatItemView_Previews: PreviewProvider {
|
||||
|
||||
@@ -296,7 +296,7 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
|
||||
func chatItemPreview(_ cItem: ChatItem) -> (Text, Bool) {
|
||||
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText()
|
||||
let itemText = cItem.meta.itemDeleted == nil ? cItem.text(isChannel: chat.chatInfo.isChannel) : markedDeletedText()
|
||||
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
|
||||
let r = messageText(itemText, itemFormattedText, sender: cItem.meta.showGroupAsSender ? nil : cItem.memberDisplayName, preview: true, mentions: cItem.mentions, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, showSecrets: nil, backgroundColor: UIColor(theme.colors.background), prefix: prefix())
|
||||
return (Text(AttributedString(r.string)), r.hasSecrets)
|
||||
|
||||
@@ -37,6 +37,7 @@ struct ChatItemClipped: ViewModifier {
|
||||
.rcvMsgContent,
|
||||
.rcvDecryptionError,
|
||||
.rcvIntegrityError,
|
||||
.rcvMsgError,
|
||||
.invalidJSON:
|
||||
let tail = if let mc = ci.content.msgContent, mc.isImageOrVideo && mc.text.isEmpty {
|
||||
false
|
||||
|
||||
@@ -187,8 +187,8 @@
|
||||
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
|
||||
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; };
|
||||
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; };
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk.a */; };
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.14-357Qkjfr6Ry4Z1G22pOLpT-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.14-357Qkjfr6Ry4Z1G22pOLpT-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.14-357Qkjfr6Ry4Z1G22pOLpT.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.14-357Qkjfr6Ry4Z1G22pOLpT.a */; };
|
||||
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; };
|
||||
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
|
||||
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
|
||||
@@ -563,8 +563,8 @@
|
||||
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
|
||||
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk.a"; sourceTree = "<group>"; };
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.14-357Qkjfr6Ry4Z1G22pOLpT-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.14-357Qkjfr6Ry4Z1G22pOLpT-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.14-357Qkjfr6Ry4Z1G22pOLpT.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.14-357Qkjfr6Ry4Z1G22pOLpT.a"; sourceTree = "<group>"; };
|
||||
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
|
||||
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
|
||||
@@ -726,8 +726,8 @@
|
||||
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
|
||||
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
|
||||
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.14-357Qkjfr6Ry4Z1G22pOLpT-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.14-357Qkjfr6Ry4Z1G22pOLpT.a in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -813,8 +813,8 @@
|
||||
64C829992D54AEEE006B9E89 /* libffi.a */,
|
||||
64C829982D54AEED006B9E89 /* libgmp.a */,
|
||||
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.12-ERy6t9H0AqxJf9JR5ehJBk.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.14-357Qkjfr6Ry4Z1G22pOLpT-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.14-357Qkjfr6Ry4Z1G22pOLpT.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -2029,7 +2029,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 324;
|
||||
CURRENT_PROJECT_VERSION = 325;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2079,7 +2079,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 324;
|
||||
CURRENT_PROJECT_VERSION = 325;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2121,7 +2121,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 324;
|
||||
CURRENT_PROJECT_VERSION = 325;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
@@ -2141,7 +2141,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 324;
|
||||
CURRENT_PROJECT_VERSION = 325;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
@@ -2166,7 +2166,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 324;
|
||||
CURRENT_PROJECT_VERSION = 325;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -2203,7 +2203,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 324;
|
||||
CURRENT_PROJECT_VERSION = 325;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -2240,7 +2240,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 324;
|
||||
CURRENT_PROJECT_VERSION = 325;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2291,7 +2291,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 324;
|
||||
CURRENT_PROJECT_VERSION = 325;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2342,7 +2342,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 324;
|
||||
CURRENT_PROJECT_VERSION = 325;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2376,7 +2376,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 324;
|
||||
CURRENT_PROJECT_VERSION = 325;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
|
||||
@@ -1652,6 +1652,10 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public var isChannel: Bool {
|
||||
groupInfo?.useRelays == true
|
||||
}
|
||||
|
||||
// this works for features that are common for contacts and groups
|
||||
public func featureEnabled(_ feature: ChatFeature) -> Bool {
|
||||
switch self {
|
||||
@@ -3191,11 +3195,13 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
|
||||
public var timestampText: Text { meta.timestampText }
|
||||
|
||||
public var text: String {
|
||||
switch (content.text, content.msgContent, file) {
|
||||
public var text: String { text(isChannel: false) }
|
||||
|
||||
public func text(isChannel: Bool) -> String {
|
||||
switch (content.text(isChannel: isChannel), content.msgContent, file) {
|
||||
case let ("", .some(.voice(_, duration)), _): return "Voice message (\(durationText(duration)))"
|
||||
case let ("", _, .some(file)): return file.fileName
|
||||
default: return content.text
|
||||
default: return content.text(isChannel: isChannel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3265,6 +3271,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
case .rcvCall: return false // notification is shown on .callInvitation instead
|
||||
case .rcvIntegrityError: return false
|
||||
case .rcvDecryptionError: return false
|
||||
case .rcvMsgError: return false
|
||||
case .rcvGroupInvitation: return true
|
||||
case .sndGroupInvitation: return false
|
||||
case .rcvDirectEvent(rcvDirectEvent: let rcvDirectEvent):
|
||||
@@ -4022,6 +4029,7 @@ public enum CIContent: Decodable, ItemContent, Hashable {
|
||||
case rcvCall(status: CICallStatus, duration: Int)
|
||||
case rcvIntegrityError(msgError: MsgErrorType)
|
||||
case rcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt32)
|
||||
case rcvMsgError(rcvMsgError: RcvMsgError)
|
||||
case rcvGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole)
|
||||
case sndGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole)
|
||||
case rcvDirectEvent(rcvDirectEvent: RcvDirectEvent)
|
||||
@@ -4047,42 +4055,43 @@ public enum CIContent: Decodable, ItemContent, Hashable {
|
||||
case chatBanner
|
||||
case invalidJSON(json: Data?)
|
||||
|
||||
public var text: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .sndMsgContent(mc): return mc.text
|
||||
case let .rcvMsgContent(mc): return mc.text
|
||||
case .sndDeleted: return NSLocalizedString("deleted", comment: "deleted chat item")
|
||||
case .rcvDeleted: return NSLocalizedString("deleted", comment: "deleted chat item")
|
||||
case let .sndCall(status, duration): return status.text(duration)
|
||||
case let .rcvCall(status, duration): return status.text(duration)
|
||||
case let .rcvIntegrityError(msgError): return msgError.text
|
||||
case let .rcvDecryptionError(msgDecryptError, _): return msgDecryptError.text
|
||||
case let .rcvGroupInvitation(groupInvitation, _): return groupInvitation.text
|
||||
case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text
|
||||
case let .rcvDirectEvent(rcvDirectEvent): return rcvDirectEvent.text
|
||||
case let .rcvGroupEvent(rcvGroupEvent): return rcvGroupEvent.text
|
||||
case let .sndGroupEvent(sndGroupEvent): return sndGroupEvent.text
|
||||
case let .rcvConnEvent(rcvConnEvent): return rcvConnEvent.text
|
||||
case let .sndConnEvent(sndConnEvent): return sndConnEvent.text
|
||||
case let .rcvChatFeature(feature, enabled, param): return CIContent.featureText(feature, enabled.text, param)
|
||||
case let .sndChatFeature(feature, enabled, param): return CIContent.featureText(feature, enabled.text, param)
|
||||
case let .rcvChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param)
|
||||
case let .sndChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param)
|
||||
case let .rcvGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role)
|
||||
case let .sndGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role)
|
||||
case let .rcvChatFeatureRejected(feature): return String.localizedStringWithFormat("%@: received, prohibited", feature.text)
|
||||
case let .rcvGroupFeatureRejected(groupFeature): return String.localizedStringWithFormat("%@: received, prohibited", groupFeature.text)
|
||||
case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
|
||||
case .rcvModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
|
||||
case .rcvBlocked: return NSLocalizedString("blocked by admin", comment: "blocked chat item")
|
||||
case let .sndDirectE2EEInfo(e2eeInfo): return directE2EEInfoStr(e2eeInfo)
|
||||
case let .rcvDirectE2EEInfo(e2eeInfo): return directE2EEInfoStr(e2eeInfo)
|
||||
case .sndGroupE2EEInfo: return e2eeInfoNoPQStr
|
||||
case .rcvGroupE2EEInfo: return e2eeInfoNoPQStr
|
||||
case .chatBanner: return ""
|
||||
case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item")
|
||||
}
|
||||
public var text: String { text(isChannel: false) }
|
||||
|
||||
public func text(isChannel: Bool) -> String {
|
||||
switch self {
|
||||
case let .sndMsgContent(mc): return mc.text
|
||||
case let .rcvMsgContent(mc): return mc.text
|
||||
case .sndDeleted: return NSLocalizedString("deleted", comment: "deleted chat item")
|
||||
case .rcvDeleted: return NSLocalizedString("deleted", comment: "deleted chat item")
|
||||
case let .sndCall(status, duration): return status.text(duration)
|
||||
case let .rcvCall(status, duration): return status.text(duration)
|
||||
case let .rcvIntegrityError(msgError): return msgError.text
|
||||
case let .rcvDecryptionError(msgDecryptError, _): return msgDecryptError.text
|
||||
case let .rcvMsgError(rcvMsgError): return rcvMsgError.text
|
||||
case let .rcvGroupInvitation(groupInvitation, _): return groupInvitation.text
|
||||
case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text
|
||||
case let .rcvDirectEvent(rcvDirectEvent): return rcvDirectEvent.text
|
||||
case let .rcvGroupEvent(rcvGroupEvent): return rcvGroupEvent.text(isChannel: isChannel)
|
||||
case let .sndGroupEvent(sndGroupEvent): return sndGroupEvent.text(isChannel: isChannel)
|
||||
case let .rcvConnEvent(rcvConnEvent): return rcvConnEvent.text
|
||||
case let .sndConnEvent(sndConnEvent): return sndConnEvent.text
|
||||
case let .rcvChatFeature(feature, enabled, param): return CIContent.featureText(feature, enabled.text, param)
|
||||
case let .sndChatFeature(feature, enabled, param): return CIContent.featureText(feature, enabled.text, param)
|
||||
case let .rcvChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param)
|
||||
case let .sndChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param)
|
||||
case let .rcvGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role)
|
||||
case let .sndGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role)
|
||||
case let .rcvChatFeatureRejected(feature): return String.localizedStringWithFormat("%@: received, prohibited", feature.text)
|
||||
case let .rcvGroupFeatureRejected(groupFeature): return String.localizedStringWithFormat("%@: received, prohibited", groupFeature.text)
|
||||
case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
|
||||
case .rcvModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
|
||||
case .rcvBlocked: return NSLocalizedString("blocked by admin", comment: "blocked chat item")
|
||||
case let .sndDirectE2EEInfo(e2eeInfo): return directE2EEInfoStr(e2eeInfo)
|
||||
case let .rcvDirectE2EEInfo(e2eeInfo): return directE2EEInfoStr(e2eeInfo)
|
||||
case .sndGroupE2EEInfo: return e2eeInfoNoPQStr
|
||||
case .rcvGroupE2EEInfo: return e2eeInfoNoPQStr
|
||||
case .chatBanner: return ""
|
||||
case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4150,6 +4159,7 @@ public enum CIContent: Decodable, ItemContent, Hashable {
|
||||
case .rcvCall: return true
|
||||
case .rcvIntegrityError: return true
|
||||
case .rcvDecryptionError: return true
|
||||
case .rcvMsgError: return true
|
||||
case .rcvGroupInvitation: return true
|
||||
case .rcvModerated: return true
|
||||
case .rcvBlocked: return true
|
||||
@@ -5076,6 +5086,20 @@ public enum MsgErrorType: Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RcvMsgError: Decodable, Hashable {
|
||||
case dropped(attempts: Int)
|
||||
case parseError(parseError: String)
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case let .dropped(attempts):
|
||||
String.localizedStringWithFormat(NSLocalizedString("removed (%d attempts)", comment: "receive error chat item"), attempts)
|
||||
case let .parseError(parseError):
|
||||
String.localizedStringWithFormat(NSLocalizedString("error: %@", comment: "receive error chat item"), parseError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct CIGroupInvitation: Decodable, Hashable {
|
||||
public var groupId: Int64
|
||||
public var groupMemberId: Int64
|
||||
@@ -5153,7 +5177,9 @@ public enum RcvGroupEvent: Decodable, Hashable {
|
||||
case memberProfileUpdated(fromProfile: Profile, toProfile: Profile)
|
||||
case newMemberPendingReview
|
||||
|
||||
var text: String {
|
||||
var text: String { text(isChannel: false) }
|
||||
|
||||
func text(isChannel: Bool) -> String {
|
||||
switch self {
|
||||
case let .memberAdded(_, profile):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("invited %@", comment: "rcv group event chat item"), profile.profileViewName)
|
||||
@@ -5175,8 +5201,12 @@ public enum RcvGroupEvent: Decodable, Hashable {
|
||||
case let .memberDeleted(_, profile):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.profileViewName)
|
||||
case .userDeleted: return NSLocalizedString("removed you", comment: "rcv group event chat item")
|
||||
case .groupDeleted: return NSLocalizedString("deleted group", comment: "rcv group event chat item")
|
||||
case .groupUpdated: return NSLocalizedString("updated group profile", comment: "rcv group event chat item")
|
||||
case .groupDeleted: return isChannel
|
||||
? NSLocalizedString("deleted channel", comment: "rcv group event chat item")
|
||||
: NSLocalizedString("deleted group", comment: "rcv group event chat item")
|
||||
case .groupUpdated: return isChannel
|
||||
? NSLocalizedString("updated channel profile", comment: "rcv group event chat item")
|
||||
: NSLocalizedString("updated group profile", comment: "rcv group event chat item")
|
||||
case .invitedViaGroupLink: return NSLocalizedString("invited via your group link", comment: "rcv group event chat item")
|
||||
case .memberCreatedContact: return NSLocalizedString("requested connection", comment: "rcv group event chat item")
|
||||
case let .memberProfileUpdated(fromProfile, toProfile): return profileUpdatedText(fromProfile, toProfile)
|
||||
@@ -5208,7 +5238,9 @@ public enum SndGroupEvent: Decodable, Hashable {
|
||||
case memberAccepted(groupMemberId: Int64, profile: Profile)
|
||||
case userPendingReview
|
||||
|
||||
var text: String {
|
||||
var text: String { text(isChannel: false) }
|
||||
|
||||
func text(isChannel: Bool) -> String {
|
||||
switch self {
|
||||
case let .memberRole(_, profile, role):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("you changed role of %@ to %@", comment: "snd group event chat item"), profile.profileViewName, role.text)
|
||||
@@ -5223,7 +5255,9 @@ public enum SndGroupEvent: Decodable, Hashable {
|
||||
case let .memberDeleted(_, profile):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("you removed %@", comment: "snd group event chat item"), profile.profileViewName)
|
||||
case .userLeft: return NSLocalizedString("you left", comment: "snd group event chat item")
|
||||
case .groupUpdated: return NSLocalizedString("group profile updated", comment: "snd group event chat item")
|
||||
case .groupUpdated: return isChannel
|
||||
? NSLocalizedString("channel profile updated", comment: "snd group event chat item")
|
||||
: NSLocalizedString("group profile updated", comment: "snd group event chat item")
|
||||
case .memberAccepted: return NSLocalizedString("you accepted this member", comment: "snd group event chat item")
|
||||
case .userPendingReview:
|
||||
return NSLocalizedString("Please wait for group moderators to review your request to join the group.", comment: "snd group event chat item")
|
||||
|
||||
@@ -74,7 +74,7 @@ public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _
|
||||
return createNotification(
|
||||
categoryIdentifier: ntfCategoryMessageReceived,
|
||||
title: title,
|
||||
body: previewMode == .message ? hideSecrets(cItem) : NSLocalizedString("new message", comment: "notification"),
|
||||
body: previewMode == .message ? hideSecrets(cItem, isChannel: cInfo.isChannel) : NSLocalizedString("new message", comment: "notification"),
|
||||
targetContentIdentifier: cInfo.id,
|
||||
userInfo: ["userId": user.userId],
|
||||
// userInfo: ["chatId": cInfo.id, "chatItemId": cItem.id]
|
||||
@@ -197,7 +197,7 @@ public func createNotification(
|
||||
}
|
||||
|
||||
// Spec: spec/services/notifications.md#hideSecrets
|
||||
func hideSecrets(_ cItem: ChatItem) -> String {
|
||||
func hideSecrets(_ cItem: ChatItem, isChannel: Bool = false) -> String {
|
||||
if let md = cItem.formattedText {
|
||||
var res = ""
|
||||
for ft in md {
|
||||
@@ -213,7 +213,7 @@ func hideSecrets(_ cItem: ChatItem) -> String {
|
||||
if case let .report(text, reason) = mc {
|
||||
return String.localizedStringWithFormat(NSLocalizedString("Report: %@", comment: "report in notification"), text.isEmpty ? reason.text : text)
|
||||
} else {
|
||||
return cItem.text
|
||||
return cItem.text(isChannel: isChannel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,8 +118,8 @@ kotlin {
|
||||
implementation("org.slf4j:slf4j-simple:2.0.12")
|
||||
implementation("uk.co.caprica:vlcj:4.8.3")
|
||||
implementation("net.java.dev.jna:jna:5.14.0")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
}
|
||||
}
|
||||
|
||||
+38
-12
@@ -1748,6 +1748,9 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
is Group -> groupInfo
|
||||
else -> null
|
||||
}
|
||||
|
||||
val isChannel: Boolean
|
||||
get() = groupInfo_?.useRelays == true
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -2891,12 +2894,14 @@ data class ChatItem (
|
||||
val id: Long get() = meta.itemId
|
||||
val timestampText: String get() = meta.timestampText
|
||||
|
||||
val text: String get() {
|
||||
val text: String get() = text(isChannel = false)
|
||||
|
||||
fun text(isChannel: Boolean): String {
|
||||
val mc = content.msgContent
|
||||
return when {
|
||||
content.text == "" && file != null && mc is MsgContent.MCVoice -> String.format(generalGetString(MR.strings.voice_message_with_duration), durationText(mc.duration))
|
||||
content.text == "" && file != null -> file.fileName
|
||||
else -> content.text
|
||||
content.text(isChannel) == "" && file != null && mc is MsgContent.MCVoice -> String.format(generalGetString(MR.strings.voice_message_with_duration), durationText(mc.duration))
|
||||
content.text(isChannel) == "" && file != null -> file.fileName
|
||||
else -> content.text(isChannel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3035,6 +3040,7 @@ data class ChatItem (
|
||||
is CIContent.RcvCall -> false // notification is shown on CallInvitation instead
|
||||
is CIContent.RcvIntegrityError -> false
|
||||
is CIContent.RcvDecryptionError -> false
|
||||
is CIContent.RcvMsgErrorContent -> false
|
||||
is CIContent.RcvGroupInvitation -> true
|
||||
is CIContent.SndGroupInvitation -> false
|
||||
is CIContent.RcvDirectEventContent -> when (content.rcvDirectEvent) {
|
||||
@@ -3729,6 +3735,7 @@ sealed class CIContent: ItemContent {
|
||||
@Serializable @SerialName("rcvCall") class RcvCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvIntegrityError") class RcvIntegrityError(val msgError: MsgErrorType): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvDecryptionError") class RcvDecryptionError(val msgDecryptError: MsgDecryptError, val msgCount: UInt): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvMsgError") class RcvMsgErrorContent(val rcvMsgError: RcvMsgError): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvGroupInvitation") class RcvGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvDirectEvent") class RcvDirectEventContent(val rcvDirectEvent: RcvDirectEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@@ -3754,7 +3761,9 @@ sealed class CIContent: ItemContent {
|
||||
@Serializable @SerialName("chatBanner") object ChatBanner: CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
|
||||
override val text: String get() = when (this) {
|
||||
override val text: String get() = text(isChannel = false)
|
||||
|
||||
fun text(isChannel: Boolean): String = when (this) {
|
||||
is SndMsgContent -> msgContent.text
|
||||
is RcvMsgContent -> msgContent.text
|
||||
is SndDeleted -> generalGetString(MR.strings.deleted_description)
|
||||
@@ -3763,11 +3772,12 @@ sealed class CIContent: ItemContent {
|
||||
is RcvCall -> status.text(duration)
|
||||
is RcvIntegrityError -> msgError.text
|
||||
is RcvDecryptionError -> msgDecryptError.text
|
||||
is RcvMsgErrorContent -> rcvMsgError.text
|
||||
is RcvGroupInvitation -> groupInvitation.text
|
||||
is SndGroupInvitation -> groupInvitation.text
|
||||
is RcvDirectEventContent -> rcvDirectEvent.text
|
||||
is RcvGroupEventContent -> rcvGroupEvent.text
|
||||
is SndGroupEventContent -> sndGroupEvent.text
|
||||
is RcvGroupEventContent -> rcvGroupEvent.text(isChannel)
|
||||
is SndGroupEventContent -> sndGroupEvent.text(isChannel)
|
||||
is RcvConnEventContent -> rcvConnEvent.text
|
||||
is SndConnEventContent -> sndConnEvent.text
|
||||
is RcvChatFeature -> featureText(feature, enabled.text, param)
|
||||
@@ -3803,6 +3813,7 @@ sealed class CIContent: ItemContent {
|
||||
is RcvCall -> true
|
||||
is RcvIntegrityError -> true
|
||||
is RcvDecryptionError -> true
|
||||
is RcvMsgErrorContent -> true
|
||||
is RcvGroupInvitation -> true
|
||||
is RcvModerated -> true
|
||||
is RcvBlocked -> true
|
||||
@@ -4714,6 +4725,17 @@ sealed class MsgErrorType() {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class RcvMsgError() {
|
||||
@Serializable @SerialName("dropped") class Dropped(val attempts: Int): RcvMsgError()
|
||||
@Serializable @SerialName("parseError") class ParseError(val parseError: String): RcvMsgError()
|
||||
|
||||
val text: String get() = when (this) {
|
||||
is Dropped -> String.format(generalGetString(MR.strings.rcv_msg_error_dropped), attempts)
|
||||
is ParseError -> String.format(generalGetString(MR.strings.rcv_msg_error_parse), parseError)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class RcvDirectEvent() {
|
||||
@Serializable @SerialName("contactDeleted") class ContactDeleted(): RcvDirectEvent()
|
||||
@@ -4764,7 +4786,9 @@ sealed class RcvGroupEvent() {
|
||||
@Serializable @SerialName("memberProfileUpdated") class MemberProfileUpdated(val fromProfile: Profile, val toProfile: Profile): RcvGroupEvent()
|
||||
@Serializable @SerialName("newMemberPendingReview") class NewMemberPendingReview(): RcvGroupEvent()
|
||||
|
||||
val text: String get() = when (this) {
|
||||
val text: String get() = text(isChannel = false)
|
||||
|
||||
fun text(isChannel: Boolean): String = when (this) {
|
||||
is MemberAdded -> String.format(generalGetString(MR.strings.rcv_group_event_member_added), profile.profileViewName)
|
||||
is MemberConnected -> generalGetString(MR.strings.rcv_group_event_member_connected)
|
||||
is MemberAccepted -> String.format(generalGetString(MR.strings.rcv_group_event_member_accepted), profile.profileViewName)
|
||||
@@ -4779,8 +4803,8 @@ sealed class RcvGroupEvent() {
|
||||
is UserRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_your_role), role.text)
|
||||
is MemberDeleted -> String.format(generalGetString(MR.strings.rcv_group_event_member_deleted), profile.profileViewName)
|
||||
is UserDeleted -> generalGetString(MR.strings.rcv_group_event_user_deleted)
|
||||
is GroupDeleted -> generalGetString(MR.strings.rcv_group_event_group_deleted)
|
||||
is GroupUpdated -> generalGetString(MR.strings.rcv_group_event_updated_group_profile)
|
||||
is GroupDeleted -> generalGetString(if (isChannel) MR.strings.rcv_channel_event_channel_deleted else MR.strings.rcv_group_event_group_deleted)
|
||||
is GroupUpdated -> generalGetString(if (isChannel) MR.strings.rcv_channel_event_updated_channel_profile else MR.strings.rcv_group_event_updated_group_profile)
|
||||
is InvitedViaGroupLink -> generalGetString(MR.strings.rcv_group_event_invited_via_your_group_link)
|
||||
is MemberCreatedContact -> generalGetString(MR.strings.rcv_group_event_member_created_contact)
|
||||
is MemberProfileUpdated -> profileUpdatedText(fromProfile, toProfile)
|
||||
@@ -4812,7 +4836,9 @@ sealed class SndGroupEvent() {
|
||||
@Serializable @SerialName("memberAccepted") class MemberAccepted(val groupMemberId: Long, val profile: Profile): SndGroupEvent()
|
||||
@Serializable @SerialName("userPendingReview") class UserPendingReview(): SndGroupEvent()
|
||||
|
||||
val text: String get() = when (this) {
|
||||
val text: String get() = text(isChannel = false)
|
||||
|
||||
fun text(isChannel: Boolean): String = when (this) {
|
||||
is MemberRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_member_role), profile.profileViewName, role.text)
|
||||
is UserRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_role_for_yourself), role.text)
|
||||
is MemberBlocked -> if (blocked) {
|
||||
@@ -4822,7 +4848,7 @@ sealed class SndGroupEvent() {
|
||||
}
|
||||
is MemberDeleted -> String.format(generalGetString(MR.strings.snd_group_event_member_deleted), profile.profileViewName)
|
||||
is UserLeft -> generalGetString(MR.strings.snd_group_event_user_left)
|
||||
is GroupUpdated -> generalGetString(MR.strings.snd_group_event_group_profile_updated)
|
||||
is GroupUpdated -> generalGetString(if (isChannel) MR.strings.snd_channel_event_channel_profile_updated else MR.strings.snd_group_event_group_profile_updated)
|
||||
is MemberAccepted -> generalGetString(MR.strings.snd_group_event_member_accepted)
|
||||
is UserPendingReview -> generalGetString(MR.strings.snd_group_event_user_pending_review)
|
||||
}
|
||||
|
||||
+4
-4
@@ -44,7 +44,7 @@ abstract class NtfManager {
|
||||
chatModel.chatId.value != cInfo.id ||
|
||||
chatModel.remoteHostId() != rhId)
|
||||
) {
|
||||
displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
|
||||
displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem, cInfo.isChannel))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ abstract class NtfManager {
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideSecrets(cItem: ChatItem): String {
|
||||
private fun hideSecrets(cItem: ChatItem, isChannel: Boolean = false): String {
|
||||
val md = cItem.formattedText
|
||||
return if (md != null) {
|
||||
var res = ""
|
||||
@@ -130,9 +130,9 @@ abstract class NtfManager {
|
||||
} else {
|
||||
val mc = cItem.content.msgContent
|
||||
if (mc is MsgContent.MCReport) {
|
||||
generalGetString(MR.strings.notification_group_report).format(cItem.text.ifEmpty { mc.reason.text })
|
||||
generalGetString(MR.strings.notification_group_report).format(cItem.text(isChannel).ifEmpty { mc.reason.text })
|
||||
} else {
|
||||
cItem.text
|
||||
cItem.text(isChannel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -312,7 +312,6 @@ fun BoxScope.SelectionHandler(
|
||||
manager.listState = listState
|
||||
manager.onCopySelection = {
|
||||
clipboard.setText(AnnotatedString(manager.getSelectedCopiedText(mergedItems.value.items, linkMode)))
|
||||
manager.clearSelection()
|
||||
showToast(generalGetString(MR.strings.copied))
|
||||
}
|
||||
|
||||
@@ -509,7 +508,10 @@ fun SelectionCopyButton() {
|
||||
.background(MaterialTheme.colors.surface, RoundedCornerShape(20.dp))
|
||||
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(20.dp))
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.clickable { manager.onCopySelection?.invoke() }
|
||||
.clickable {
|
||||
manager.onCopySelection?.invoke()
|
||||
manager.clearSelection()
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
+10
-6
@@ -48,8 +48,8 @@ private val msgTailMaxHeightDp = msgTailWidthDp * 1.732f // 60deg
|
||||
|
||||
val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = CurrentColors.value.colors.secondary)
|
||||
|
||||
fun chatEventText(ci: ChatItem): AnnotatedString =
|
||||
chatEventText(ci.content.text, ci.timestampText)
|
||||
fun chatEventText(ci: ChatItem, isChannel: Boolean = false): AnnotatedString =
|
||||
chatEventText(ci.content.text(isChannel), ci.timestampText)
|
||||
|
||||
fun chatEventText(eventText: String, ts: String): AnnotatedString =
|
||||
buildAnnotatedString {
|
||||
@@ -612,7 +612,7 @@ fun ChatItemView(
|
||||
return if (count <= 1) {
|
||||
null
|
||||
} else if (ns.isEmpty()) {
|
||||
generalGetString(MR.strings.rcv_group_events_count).format(count)
|
||||
generalGetString(if (cInfo.isChannel) MR.strings.rcv_channel_events_count else MR.strings.rcv_group_events_count).format(count)
|
||||
} else if (count > ns.size) {
|
||||
members + " " + generalGetString(MR.strings.rcv_group_and_other_events).format(count - ns.size)
|
||||
} else {
|
||||
@@ -629,9 +629,9 @@ fun ChatItemView(
|
||||
buildAnnotatedString {
|
||||
withStyle(chatEventStyle) { append(memberDisplayName) }
|
||||
append(" ")
|
||||
}.plus(chatEventText(cItem))
|
||||
}.plus(chatEventText(cItem, cInfo.isChannel))
|
||||
} else {
|
||||
chatEventText(cItem)
|
||||
chatEventText(cItem, cInfo.isChannel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,7 +643,7 @@ fun ChatItemView(
|
||||
@Composable fun PendingReviewEventItemView() {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(chatEventStyle.copy(fontWeight = FontWeight.Bold)) { append(cItem.content.text) }
|
||||
withStyle(chatEventStyle.copy(fontWeight = FontWeight.Bold)) { append(cItem.content.text(cInfo.isChannel)) }
|
||||
},
|
||||
Modifier.padding(horizontal = 6.dp, vertical = 6.dp)
|
||||
)
|
||||
@@ -714,6 +714,9 @@ fun ChatItemView(
|
||||
} else {
|
||||
Box(Modifier.size(0.dp)) {}
|
||||
}
|
||||
is CIContent.RcvMsgErrorContent -> {
|
||||
RcvMsgErrorItemView(c.rcvMsgError, cItem, showTimestamp, cInfo.timedMessagesTTL)
|
||||
}
|
||||
is CIContent.RcvDecryptionError -> {
|
||||
CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember)
|
||||
DeleteItemMenu()
|
||||
@@ -1310,6 +1313,7 @@ fun shapeStyleWithTail(chatItem: ChatItem? = null, tailEnabled: Boolean, tailVis
|
||||
is CIContent.SndMsgContent,
|
||||
is CIContent.RcvMsgContent,
|
||||
is CIContent.RcvDecryptionError,
|
||||
is CIContent.RcvMsgErrorContent,
|
||||
is CIContent.SndDeleted,
|
||||
is CIContent.RcvDeleted,
|
||||
is CIContent.RcvIntegrityError,
|
||||
|
||||
+14
@@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.model.MsgErrorType
|
||||
import chat.simplex.common.model.RcvMsgError
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.helpers.AlertManager
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
@@ -73,6 +74,19 @@ fun CIMsgError(ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?, onC
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RcvMsgErrorItemView(rcvMsgError: RcvMsgError, ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?) {
|
||||
CIMsgError(ci, showTimestamp, timedMessagesTTL) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.alert_title_msg_error),
|
||||
text = when (rcvMsgError) {
|
||||
is RcvMsgError.Dropped -> String.format(generalGetString(MR.strings.alert_text_msg_reception_error), rcvMsgError.attempts)
|
||||
is RcvMsgError.ParseError -> rcvMsgError.parseError
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview/*(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
name = "Dark Mode"
|
||||
|
||||
+3
-3
@@ -299,7 +299,7 @@ fun MarkdownText (
|
||||
}
|
||||
val clampedRange = selectionRange?.let { it.first .. minOf(it.last, selectableEnd) }
|
||||
if ((hasLinks && uriHandler != null) || hasSecrets || (hasCommands && sendCommandMsg != null)) {
|
||||
val icon = remember { mutableStateOf(PointerIcon.Default) }
|
||||
val icon = remember { mutableStateOf(PointerIcon.Text) }
|
||||
ClickableText(annotatedText, style = style, selectionRange = clampedRange, modifier = modifier.pointerHoverIcon(icon.value), maxLines = maxLines, overflow = overflow,
|
||||
onLongClick = { offset ->
|
||||
if (hasLinks) {
|
||||
@@ -336,7 +336,7 @@ fun MarkdownText (
|
||||
if (hasAnnotation("WEB_URL") || hasAnnotation("SIMPLEX_URL") || hasAnnotation("OTHER_URL") || hasAnnotation("SECRET") || hasAnnotation("COMMAND")) {
|
||||
PointerIcon.Hand
|
||||
} else {
|
||||
PointerIcon.Default
|
||||
PointerIcon.Text
|
||||
}
|
||||
},
|
||||
shouldConsumeEvent = { offset ->
|
||||
@@ -431,7 +431,7 @@ private fun SelectableText(
|
||||
|
||||
BasicText(
|
||||
text = text,
|
||||
modifier = modifier.then(selectionHighlight(selectionRange, text.length, layoutResult)),
|
||||
modifier = modifier.pointerHoverIcon(PointerIcon.Text).then(selectionHighlight(selectionRange, text.length, layoutResult)),
|
||||
style = style,
|
||||
maxLines = maxLines,
|
||||
overflow = overflow,
|
||||
|
||||
+1
-1
@@ -241,7 +241,7 @@ fun ChatPreviewView(
|
||||
Text(previewText.first, color = previewText.second)
|
||||
} else if (ci != null && showChatPreviews) {
|
||||
val (text: CharSequence, inlineTextContent) = when {
|
||||
ci.meta.itemDeleted == null -> ci.text to null
|
||||
ci.meta.itemDeleted == null -> ci.text(chat.chatInfo.isChannel) to null
|
||||
else -> markedDeletedText(ci, chat.chatInfo) to null
|
||||
}
|
||||
val formattedText = when {
|
||||
|
||||
@@ -1377,6 +1377,10 @@
|
||||
<string name="integrity_msg_bad_hash">bad message hash</string>
|
||||
<string name="integrity_msg_bad_id">bad message ID</string>
|
||||
<string name="integrity_msg_duplicate">duplicate message</string>
|
||||
<string name="rcv_msg_error_dropped">dropped (%1$d attempts)</string>
|
||||
<string name="rcv_msg_error_parse">error: %s</string>
|
||||
<string name="alert_title_msg_error">Message error</string>
|
||||
<string name="alert_text_msg_reception_error">The app removed this message after %1$d attempts to receive it.</string>
|
||||
<string name="alert_title_skipped_messages">Skipped messages</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised.</string>
|
||||
<string name="alert_title_msg_bad_hash">Bad message hash</string>
|
||||
@@ -1707,7 +1711,9 @@
|
||||
<string name="rcv_group_event_member_deleted">removed %1$s</string>
|
||||
<string name="rcv_group_event_user_deleted">removed you</string>
|
||||
<string name="rcv_group_event_group_deleted">deleted group</string>
|
||||
<string name="rcv_channel_event_channel_deleted">deleted channel</string>
|
||||
<string name="rcv_group_event_updated_group_profile">updated group profile</string>
|
||||
<string name="rcv_channel_event_updated_channel_profile">updated channel profile</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">invited via your group link</string>
|
||||
<string name="rcv_group_event_member_created_contact">requested connection</string>
|
||||
<string name="rcv_group_event_new_member_pending_review">New member wants to join the group.</string>
|
||||
@@ -1718,6 +1724,7 @@
|
||||
<string name="snd_group_event_member_deleted">you removed %1$s</string>
|
||||
<string name="snd_group_event_user_left">you left</string>
|
||||
<string name="snd_group_event_group_profile_updated">group profile updated</string>
|
||||
<string name="snd_channel_event_channel_profile_updated">channel profile updated</string>
|
||||
<string name="snd_group_event_member_accepted">you accepted this member</string>
|
||||
<string name="snd_group_event_user_pending_review">Please wait for group moderators to review your request to join the group.</string>
|
||||
|
||||
@@ -1726,6 +1733,7 @@
|
||||
<string name="rcv_group_event_3_members_connected">%s, %s and %s connected</string>
|
||||
<string name="rcv_group_event_n_members_connected">%s, %s and %d other members connected</string>
|
||||
<string name="rcv_group_events_count">%d group events</string>
|
||||
<string name="rcv_channel_events_count">%d channel events</string>
|
||||
<string name="rcv_group_and_other_events">and %d other events</string>
|
||||
<string name="group_members_2">%s and %s</string>
|
||||
<string name="group_members_n">%s, %s and %d members</string>
|
||||
|
||||
@@ -24,13 +24,13 @@ android.nonTransitiveRClass=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=6.5-beta.7
|
||||
android.version_code=339
|
||||
android.version_name=6.5-beta.8
|
||||
android.version_code=340
|
||||
|
||||
android.bundle=false
|
||||
|
||||
desktop.version_name=6.5-beta.7
|
||||
desktop.version_code=134
|
||||
desktop.version_name=6.5-beta.8
|
||||
desktop.version_code=135
|
||||
|
||||
kotlin.version=2.1.20
|
||||
gradle.plugin.version=8.7.0
|
||||
|
||||
@@ -51,6 +51,7 @@ This file is generated automatically.
|
||||
- [Color](#color)
|
||||
- [CommandError](#commanderror)
|
||||
- [CommandErrorType](#commanderrortype)
|
||||
- [CommentsGroupPreference](#commentsgrouppreference)
|
||||
- [ComposedMessage](#composedmessage)
|
||||
- [ConnStatus](#connstatus)
|
||||
- [ConnType](#conntype)
|
||||
@@ -69,6 +70,7 @@ This file is generated automatically.
|
||||
- [CreatedConnLink](#createdconnlink)
|
||||
- [CryptoFile](#cryptofile)
|
||||
- [CryptoFileArgs](#cryptofileargs)
|
||||
- [DroppedMsg](#droppedmsg)
|
||||
- [E2EInfo](#e2einfo)
|
||||
- [ErrorType](#errortype)
|
||||
- [FeatureAllowed](#featureallowed)
|
||||
@@ -148,6 +150,7 @@ This file is generated automatically.
|
||||
- [RcvFileStatus](#rcvfilestatus)
|
||||
- [RcvFileTransfer](#rcvfiletransfer)
|
||||
- [RcvGroupEvent](#rcvgroupevent)
|
||||
- [RcvMsgError](#rcvmsgerror)
|
||||
- [RelayProfile](#relayprofile)
|
||||
- [RelayStatus](#relaystatus)
|
||||
- [ReportReason](#reportreason)
|
||||
@@ -453,6 +456,10 @@ RcvDecryptionError:
|
||||
- msgDecryptError: [MsgDecryptError](#msgdecrypterror)
|
||||
- msgCount: word32
|
||||
|
||||
RcvMsgError:
|
||||
- type: "rcvMsgError"
|
||||
- rcvMsgError: [RcvMsgError](#rcvmsgerror)
|
||||
|
||||
RcvGroupInvitation:
|
||||
- type: "rcvGroupInvitation"
|
||||
- groupInvitation: [CIGroupInvitation](#cigroupinvitation)
|
||||
@@ -1480,6 +1487,15 @@ LARGE:
|
||||
- type: "LARGE"
|
||||
|
||||
|
||||
---
|
||||
|
||||
## CommentsGroupPreference
|
||||
|
||||
**Record type**:
|
||||
- enable: [GroupFeatureEnabled](#groupfeatureenabled)
|
||||
- duration: int?
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ComposedMessage
|
||||
@@ -1803,6 +1819,15 @@ connFullLink + ((' ' + connShortLink) if connShortLink is not None else '') # Py
|
||||
- fileNonce: string
|
||||
|
||||
|
||||
---
|
||||
|
||||
## DroppedMsg
|
||||
|
||||
**Record type**:
|
||||
- brokerTs: UTCTime
|
||||
- attempts: int
|
||||
|
||||
|
||||
---
|
||||
|
||||
## E2EInfo
|
||||
@@ -2070,6 +2095,7 @@ Phone:
|
||||
- reports: [GroupPreference](#grouppreference)
|
||||
- history: [GroupPreference](#grouppreference)
|
||||
- sessions: [RoleGroupPreference](#rolegrouppreference)
|
||||
- comments: [CommentsGroupPreference](#commentsgrouppreference)
|
||||
- commands: [[ChatBotCommand](#chatbotcommand)]
|
||||
|
||||
|
||||
@@ -2160,6 +2186,7 @@ MemberSupport:
|
||||
- "reports"
|
||||
- "history"
|
||||
- "sessions"
|
||||
- "comments"
|
||||
|
||||
|
||||
---
|
||||
@@ -2376,6 +2403,7 @@ Known:
|
||||
- reports: [GroupPreference](#grouppreference)?
|
||||
- history: [GroupPreference](#grouppreference)?
|
||||
- sessions: [RoleGroupPreference](#rolegrouppreference)?
|
||||
- comments: [CommentsGroupPreference](#commentsgrouppreference)?
|
||||
- commands: [[ChatBotCommand](#chatbotcommand)]?
|
||||
|
||||
|
||||
@@ -3179,6 +3207,21 @@ MsgBadSignature:
|
||||
- type: "msgBadSignature"
|
||||
|
||||
|
||||
---
|
||||
|
||||
## RcvMsgError
|
||||
|
||||
**Discriminated union type**:
|
||||
|
||||
Dropped:
|
||||
- type: "dropped"
|
||||
- attempts: int
|
||||
|
||||
ParseError:
|
||||
- type: "parseError"
|
||||
- parseError: string
|
||||
|
||||
|
||||
---
|
||||
|
||||
## RelayProfile
|
||||
@@ -3248,6 +3291,7 @@ A_CRYPTO:
|
||||
|
||||
A_DUPLICATE:
|
||||
- type: "A_DUPLICATE"
|
||||
- droppedMsg_: [DroppedMsg](#droppedmsg)?
|
||||
|
||||
A_QUEUE:
|
||||
- type: "A_QUEUE"
|
||||
|
||||
@@ -237,6 +237,7 @@ chatTypesDocsData =
|
||||
(sti @Color, STEnum, "", [], "", ""),
|
||||
(sti @CommandError, STUnion, "", [], "", ""),
|
||||
(sti @CommandErrorType, STUnion, "", [], "", ""),
|
||||
(sti @CommentsGroupPreference, STRecord, "", [], "", ""),
|
||||
(sti @ComposedMessage, STRecord, "", [], "", ""),
|
||||
(sti @Connection, STRecord, "", [], "", ""),
|
||||
(sti @ConnectionEntity, STUnion, "", [], "", ""),
|
||||
@@ -252,6 +253,7 @@ chatTypesDocsData =
|
||||
(sti @ContactUserPreferences, STRecord, "", [], "", ""),
|
||||
(sti @CryptoFile, STRecord, "", [], "", ""),
|
||||
(sti @CryptoFileArgs, STRecord, "", [], "", ""),
|
||||
(sti @DroppedMsg, STRecord, "", [], "", ""),
|
||||
(sti @E2EInfo, STRecord, "", [], "", ""),
|
||||
(sti @ErrorType, STUnion, "", [], "", ""),
|
||||
(sti @FeatureAllowed, STEnum, "FA", [], "", ""),
|
||||
@@ -331,6 +333,7 @@ chatTypesDocsData =
|
||||
(sti @RcvFileStatus, STUnion, "RFS", [], "", ""),
|
||||
(sti @RcvFileTransfer, STRecord, "", [], "", ""),
|
||||
(sti @RcvGroupEvent, STUnion, "RGE", [], "", ""),
|
||||
(sti @RcvMsgError, STUnion, "RME", [], "", ""),
|
||||
(sti @RelayProfile, STRecord, "", [], "", ""),
|
||||
(sti @RelayStatus, STEnum, "RS", [], "", ""),
|
||||
(sti @ReportReason, STEnum' (dropPfxSfx "RR" ""), "", ["RRUnknown"], "", ""),
|
||||
@@ -435,6 +438,7 @@ deriving instance Generic ClientNotice
|
||||
deriving instance Generic Color
|
||||
deriving instance Generic CommandError
|
||||
deriving instance Generic CommandErrorType
|
||||
deriving instance Generic CommentsGroupPreference
|
||||
deriving instance Generic ComposedMessage
|
||||
deriving instance Generic Connection
|
||||
deriving instance Generic ConnectionEntity
|
||||
@@ -450,6 +454,7 @@ deriving instance Generic ContactStatus
|
||||
deriving instance Generic ContactUserPreferences
|
||||
deriving instance Generic CryptoFile
|
||||
deriving instance Generic CryptoFileArgs
|
||||
deriving instance Generic DroppedMsg
|
||||
deriving instance Generic E2EInfo
|
||||
deriving instance Generic ErrorType
|
||||
deriving instance Generic FeatureAllowed
|
||||
@@ -535,6 +540,7 @@ deriving instance Generic RcvFileDescr
|
||||
deriving instance Generic RcvFileStatus
|
||||
deriving instance Generic RcvFileTransfer
|
||||
deriving instance Generic RcvGroupEvent
|
||||
deriving instance Generic RcvMsgError
|
||||
deriving instance Generic RelayProfile
|
||||
deriving instance Generic RelayStatus
|
||||
deriving instance Generic ReportReason
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 97802a30fce1dfeea90f0b465e21fc8eca937abb
|
||||
tag: bc5ea42bec3a63e46b191e4150dd79957f114e01
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -278,6 +278,7 @@ export type CIContent =
|
||||
| CIContent.RcvCall
|
||||
| CIContent.RcvIntegrityError
|
||||
| CIContent.RcvDecryptionError
|
||||
| CIContent.RcvMsgError
|
||||
| CIContent.RcvGroupInvitation
|
||||
| CIContent.SndGroupInvitation
|
||||
| CIContent.RcvDirectEvent
|
||||
@@ -312,6 +313,7 @@ export namespace CIContent {
|
||||
| "rcvCall"
|
||||
| "rcvIntegrityError"
|
||||
| "rcvDecryptionError"
|
||||
| "rcvMsgError"
|
||||
| "rcvGroupInvitation"
|
||||
| "sndGroupInvitation"
|
||||
| "rcvDirectEvent"
|
||||
@@ -383,6 +385,11 @@ export namespace CIContent {
|
||||
msgCount: number // word32
|
||||
}
|
||||
|
||||
export interface RcvMsgError extends Interface {
|
||||
type: "rcvMsgError"
|
||||
rcvMsgError: RcvMsgError
|
||||
}
|
||||
|
||||
export interface RcvGroupInvitation extends Interface {
|
||||
type: "rcvGroupInvitation"
|
||||
groupInvitation: CIGroupInvitation
|
||||
@@ -1714,6 +1721,11 @@ export namespace CommandErrorType {
|
||||
}
|
||||
}
|
||||
|
||||
export interface CommentsGroupPreference {
|
||||
enable: GroupFeatureEnabled
|
||||
duration?: number // int
|
||||
}
|
||||
|
||||
export interface ComposedMessage {
|
||||
fileSource?: CryptoFile
|
||||
quotedItemId?: number // int64
|
||||
@@ -2070,6 +2082,11 @@ export interface CryptoFileArgs {
|
||||
fileNonce: string
|
||||
}
|
||||
|
||||
export interface DroppedMsg {
|
||||
brokerTs: string // ISO-8601 timestamp
|
||||
attempts: number // int
|
||||
}
|
||||
|
||||
export interface E2EInfo {
|
||||
pqEnabled?: boolean
|
||||
}
|
||||
@@ -2420,6 +2437,7 @@ export interface FullGroupPreferences {
|
||||
reports: GroupPreference
|
||||
history: GroupPreference
|
||||
sessions: RoleGroupPreference
|
||||
comments: CommentsGroupPreference
|
||||
commands: ChatBotCommand[]
|
||||
}
|
||||
|
||||
@@ -2492,6 +2510,7 @@ export enum GroupFeature {
|
||||
Reports = "reports",
|
||||
History = "history",
|
||||
Sessions = "sessions",
|
||||
Comments = "comments",
|
||||
}
|
||||
|
||||
export enum GroupFeatureEnabled {
|
||||
@@ -2669,6 +2688,7 @@ export interface GroupPreferences {
|
||||
reports?: GroupPreference
|
||||
history?: GroupPreference
|
||||
sessions?: RoleGroupPreference
|
||||
comments?: CommentsGroupPreference
|
||||
commands?: ChatBotCommand[]
|
||||
}
|
||||
|
||||
@@ -3598,6 +3618,26 @@ export namespace RcvGroupEvent {
|
||||
}
|
||||
}
|
||||
|
||||
export type RcvMsgError = RcvMsgError.Dropped | RcvMsgError.ParseError
|
||||
|
||||
export namespace RcvMsgError {
|
||||
export type Tag = "dropped" | "parseError"
|
||||
|
||||
interface Interface {
|
||||
type: Tag
|
||||
}
|
||||
|
||||
export interface Dropped extends Interface {
|
||||
type: "dropped"
|
||||
attempts: number // int
|
||||
}
|
||||
|
||||
export interface ParseError extends Interface {
|
||||
type: "parseError"
|
||||
parseError: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface RelayProfile {
|
||||
displayName: string
|
||||
fullName: string
|
||||
@@ -3673,6 +3713,7 @@ export namespace SMPAgentError {
|
||||
|
||||
export interface A_DUPLICATE extends Interface {
|
||||
type: "A_DUPLICATE"
|
||||
droppedMsg_?: DroppedMsg
|
||||
}
|
||||
|
||||
export interface A_QUEUE extends Interface {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"docs": "typedoc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@simplex-chat/types": "^0.3.0",
|
||||
"@simplex-chat/types": "^0.4.0",
|
||||
"extract-zip": "^2.0.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"node-addon-api": "^8.5.0"
|
||||
|
||||
@@ -872,4 +872,34 @@ export class ChatApi {
|
||||
const r = await this.sendChatCmd(CC.APISetContactPrefs.cmdString({contactId, preferences}))
|
||||
if (r.type !== "contactPrefsUpdated") throw new ChatCommandError("error setting contact prefs", r)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a direct message contact with a group member.
|
||||
* Returns the created contact.
|
||||
* Network usage: interactive.
|
||||
*/
|
||||
async apiCreateMemberContact(groupId: number, groupMemberId: number): Promise<T.Contact> {
|
||||
const r: any = await this.sendChatCmd(`/_create member contact #${groupId} ${groupMemberId}`)
|
||||
if (r.type === "newMemberContact") return r.contact
|
||||
throw new ChatCommandError("error creating member contact", r)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a direct message invitation to a group member contact.
|
||||
* The contact must have been created with {@link apiCreateMemberContact}.
|
||||
* Network usage: interactive.
|
||||
*/
|
||||
async apiSendMemberContactInvitation(contactId: number, message?: T.MsgContent | string): Promise<T.Contact> {
|
||||
let cmd = `/_invite member contact @${contactId}`
|
||||
if (message !== undefined) {
|
||||
if (typeof message === "string") {
|
||||
cmd += ` text ${message}`
|
||||
} else {
|
||||
cmd += ` json ${JSON.stringify(message)}`
|
||||
}
|
||||
}
|
||||
const r: any = await this.sendChatCmd(cmd)
|
||||
if (r.type === "newMemberContactSentInv") return r.contact
|
||||
throw new ChatCommandError("error sending member contact invitation", r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,4 +64,89 @@ describe("API tests (use preset servers)", () => {
|
||||
expect(servers[0] !== servers[1]).toBe(true)
|
||||
expect(eventCount > 0).toBe(true)
|
||||
}, 30000)
|
||||
|
||||
it("should create member contact and send invitation", async () => {
|
||||
// create 3 users and start chat controllers
|
||||
const alice = await api.ChatApi.init(alicePath)
|
||||
const bob = await api.ChatApi.init(bobPath)
|
||||
const carolPath = path.join(tmpDir, "carol")
|
||||
const carol = await api.ChatApi.init(carolPath)
|
||||
const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""})
|
||||
await bob.apiCreateActiveUser({displayName: "bob", fullName: ""})
|
||||
await carol.apiCreateActiveUser({displayName: "carol", fullName: ""})
|
||||
await alice.startChat()
|
||||
await bob.startChat()
|
||||
await carol.startChat()
|
||||
// connect alice <-> bob
|
||||
const aliceLink1 = await alice.apiCreateLink(aliceUser.userId)
|
||||
await expect(bob.apiConnectActiveUser(aliceLink1)).resolves.toBe(api.ConnReqType.Invitation)
|
||||
const [bobContact] = await Promise.all([
|
||||
(await alice.wait("contactConnected")).contact,
|
||||
(await bob.wait("contactConnected")).contact
|
||||
])
|
||||
// connect alice <-> carol
|
||||
const aliceLink2 = await alice.apiCreateLink(aliceUser.userId)
|
||||
await expect(carol.apiConnectActiveUser(aliceLink2)).resolves.toBe(api.ConnReqType.Invitation)
|
||||
const [carolContact] = await Promise.all([
|
||||
(await alice.wait("contactConnected")).contact,
|
||||
(await carol.wait("contactConnected")).contact
|
||||
])
|
||||
// create group with direct messages enabled
|
||||
const group = await alice.apiNewGroup(aliceUser.userId, {
|
||||
displayName: "test-group",
|
||||
fullName: "",
|
||||
groupPreferences: {
|
||||
directMessages: {enable: T.GroupFeatureEnabled.On},
|
||||
},
|
||||
})
|
||||
const groupId = group.groupId
|
||||
// add bob to the group
|
||||
const bobInvP = bob.wait("receivedGroupInvitation", 15000)
|
||||
await alice.apiAddMember(groupId, bobContact.contactId, T.GroupMemberRole.Member)
|
||||
const bobInvEvt = await bobInvP
|
||||
expect(bobInvEvt).toBeDefined()
|
||||
const aliceBobConnP = alice.wait("connectedToGroupMember", 15000)
|
||||
const bobAliceConnP = bob.wait("connectedToGroupMember", 15000)
|
||||
await bob.apiJoinGroup(bobInvEvt!.groupInfo.groupId)
|
||||
await Promise.all([aliceBobConnP, bobAliceConnP])
|
||||
// add carol to the group
|
||||
const carolInvP = carol.wait("receivedGroupInvitation", 30000)
|
||||
await alice.apiAddMember(groupId, carolContact.contactId, T.GroupMemberRole.Member)
|
||||
const carolInvEvt = await carolInvP
|
||||
expect(carolInvEvt).toBeDefined()
|
||||
// wait for carol to connect to both alice and bob (and vice versa)
|
||||
const bobCarolConnP = bob.wait("connectedToGroupMember",
|
||||
(evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "carol", 30000)
|
||||
const carolAliceConnP = carol.wait("connectedToGroupMember",
|
||||
(evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "alice", 30000)
|
||||
const carolBobConnP = carol.wait("connectedToGroupMember",
|
||||
(evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "bob", 30000)
|
||||
const aliceCarolConnP = alice.wait("connectedToGroupMember",
|
||||
(evt: CEvt.ConnectedToGroupMember) => evt.member.memberProfile.displayName === "carol", 30000)
|
||||
await carol.apiJoinGroup(carolInvEvt!.groupInfo.groupId)
|
||||
await Promise.all([bobCarolConnP, carolAliceConnP, carolBobConnP, aliceCarolConnP])
|
||||
// find carol's memberId from bob's perspective
|
||||
const members = await bob.apiListMembers(groupId)
|
||||
const carolMember = members.find(m => m.memberProfile.displayName === "carol")
|
||||
expect(carolMember).toBeDefined()
|
||||
// test apiCreateMemberContact
|
||||
const dmContact = await bob.apiCreateMemberContact(groupId, carolMember!.groupMemberId)
|
||||
expect(dmContact).toBeDefined()
|
||||
expect(dmContact.contactId).toBeDefined()
|
||||
// test apiSendMemberContactInvitation
|
||||
const carolDmP = carol.wait("newMemberContactReceivedInv" as CEvt.Tag, 30000)
|
||||
const invContact = await bob.apiSendMemberContactInvitation(dmContact.contactId, "hello from bob")
|
||||
expect(invContact).toBeDefined()
|
||||
// carol should receive the member contact invitation
|
||||
const carolDmEvt = await carolDmP
|
||||
expect(carolDmEvt).toBeDefined()
|
||||
expect((carolDmEvt as any).contact).toBeDefined()
|
||||
// cleanup
|
||||
await alice.stopChat()
|
||||
await bob.stopChat()
|
||||
await carol.stopChat()
|
||||
await alice.close()
|
||||
await bob.close()
|
||||
await carol.close()
|
||||
}, 90000)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."97802a30fce1dfeea90f0b465e21fc8eca937abb" = "1pgrfzir7www4f2h1byvzlihs6vird85m1mhwiinl0xaqsycha1m";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."bc5ea42bec3a63e46b191e4150dd79957f114e01" = "0lswj7crfnwzh4g28kgxxl7g4i2a9pn03pxj7sqfqy3vs83m3bax";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 6.5.0.14
|
||||
version: 6.5.0.15
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
||||
@@ -493,7 +493,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
Left e -> do
|
||||
atomically $ modifyTVar' tags ("error" :)
|
||||
logInfo $ "contact msg=error " <> eInfo <> " " <> tshow e
|
||||
eToView (ChatError . CEException $ "error parsing chat message: " <> e)
|
||||
createInternalChatItem user (CDDirectRcv ct') (CIRcvMsgError $ RMEParseError $ T.pack e) Nothing
|
||||
`catchAllErrors` \_ -> pure ()
|
||||
withRcpt <- checkSendRcpt ct' $ rights aChatMsgs -- not crucial to use ct'' from processEvent
|
||||
pure (withRcpt, False)
|
||||
where
|
||||
@@ -687,6 +688,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
-- error cannot be AUTH error here
|
||||
updateDirectItemsStatusMsgs ct conn (L.toList msgIds) (CISSndError $ agentSndError err)
|
||||
eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity)
|
||||
ERR (AGENT (A_DUPLICATE (Just DroppedMsg {brokerTs, attempts}))) ->
|
||||
createInternalChatItem user (CDDirectRcv ct) (CIRcvMsgError $ RMEDropped attempts) (Just brokerTs)
|
||||
ERR err -> do
|
||||
eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity)
|
||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||
@@ -962,7 +965,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
Left e -> do
|
||||
atomically $ modifyTVar' tags ("error" :)
|
||||
logInfo $ "group msg=error " <> eInfo <> " " <> tshow e
|
||||
eToView (ChatError . CEException $ "error parsing chat message: " <> e)
|
||||
if isRelay membership
|
||||
then
|
||||
eToView (ChatError . CEException $ "error parsing chat message: " <> e)
|
||||
else
|
||||
createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m') (CIRcvMsgError $ RMEParseError $ T.pack e) Nothing
|
||||
`catchAllErrors` \_ -> pure ()
|
||||
pure newDeliveryTasks
|
||||
processEvent :: forall e. MsgEncodingI e => GroupInfo -> GroupMember -> VerifiedMsg e -> CM (Maybe NewMessageDeliveryTask)
|
||||
processEvent gInfo' m' verifiedMsg = do
|
||||
@@ -1178,6 +1186,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
withStore' $ \db -> forM_ msgIds $ \msgId ->
|
||||
updateGroupItemsErrorStatus db msgId (groupMemberId' m) newStatus `catchAll_` pure ()
|
||||
eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity)
|
||||
ERR err@(AGENT (A_DUPLICATE (Just DroppedMsg {brokerTs, attempts})))
|
||||
| isRelay membership ->
|
||||
eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity)
|
||||
| otherwise -> do
|
||||
(gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m
|
||||
createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m') (CIRcvMsgError $ RMEDropped attempts) (Just brokerTs)
|
||||
ERR err -> do
|
||||
eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity)
|
||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||
|
||||
@@ -145,6 +145,7 @@ data CIContent (d :: MsgDirection) where
|
||||
CIRcvCall :: CICallStatus -> Int -> CIContent 'MDRcv
|
||||
CIRcvIntegrityError :: MsgErrorType -> CIContent 'MDRcv
|
||||
CIRcvDecryptionError :: MsgDecryptError -> Word32 -> CIContent 'MDRcv
|
||||
CIRcvMsgError :: RcvMsgError -> CIContent 'MDRcv
|
||||
CIRcvGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDRcv
|
||||
CISndGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDSnd
|
||||
CIRcvDirectEvent :: RcvDirectEvent -> CIContent 'MDRcv
|
||||
@@ -196,6 +197,11 @@ data MsgDecryptError
|
||||
| MDERatchetSync
|
||||
deriving (Eq, Show)
|
||||
|
||||
data RcvMsgError
|
||||
= RMEDropped {attempts :: Int}
|
||||
| RMEParseError {parseError :: Text}
|
||||
deriving (Eq, Show)
|
||||
|
||||
ciRequiresAttention :: forall d. MsgDirectionI d => CIContent d -> Bool
|
||||
ciRequiresAttention content = case msgDirection @d of
|
||||
SMDSnd -> True
|
||||
@@ -205,6 +211,7 @@ ciRequiresAttention content = case msgDirection @d of
|
||||
CIRcvCall {} -> True
|
||||
CIRcvIntegrityError _ -> True
|
||||
CIRcvDecryptionError {} -> True
|
||||
CIRcvMsgError _ -> False
|
||||
CIRcvGroupInvitation {} -> True
|
||||
CIRcvDirectEvent rde -> case rde of
|
||||
RDEContactDeleted -> False
|
||||
@@ -275,6 +282,7 @@ ciContentToText = \case
|
||||
CIRcvCall status duration -> "incoming call: " <> ciCallInfoText status duration
|
||||
CIRcvIntegrityError err -> msgIntegrityError err
|
||||
CIRcvDecryptionError err n -> msgDecryptErrorText err n
|
||||
CIRcvMsgError err -> rcvMsgErrorText err
|
||||
CIRcvGroupInvitation groupInvitation memberRole -> "received " <> ciGroupInvitationToText groupInvitation memberRole
|
||||
CISndGroupInvitation groupInvitation memberRole -> "sent " <> ciGroupInvitationToText groupInvitation memberRole
|
||||
CIRcvDirectEvent event -> rcvDirectEventToText event
|
||||
@@ -421,6 +429,11 @@ msgIntegrityError = \case
|
||||
MsgBadHash -> "incorrect message hash"
|
||||
MsgDuplicate -> "duplicate message ID"
|
||||
|
||||
rcvMsgErrorText :: RcvMsgError -> Text
|
||||
rcvMsgErrorText = \case
|
||||
RMEDropped {attempts} -> "message removed after " <> tshow attempts <> " attempts"
|
||||
RMEParseError {parseError} -> "message error: " <> parseError
|
||||
|
||||
msgDecryptErrorText :: MsgDecryptError -> Word32 -> Text
|
||||
msgDecryptErrorText err n =
|
||||
"decryption error, possibly due to the device change"
|
||||
@@ -457,6 +470,7 @@ data JSONCIContent
|
||||
| JCIRcvCall {status :: CICallStatus, duration :: Int}
|
||||
| JCIRcvIntegrityError {msgError :: MsgErrorType}
|
||||
| JCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32}
|
||||
| JCIRcvMsgError {rcvMsgError :: RcvMsgError}
|
||||
| JCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
||||
| JCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
||||
| JCIRcvDirectEvent {rcvDirectEvent :: RcvDirectEvent}
|
||||
@@ -492,6 +506,7 @@ jsonCIContent = \case
|
||||
CIRcvCall status duration -> JCIRcvCall {status, duration}
|
||||
CIRcvIntegrityError err -> JCIRcvIntegrityError err
|
||||
CIRcvDecryptionError err n -> JCIRcvDecryptionError err n
|
||||
CIRcvMsgError err -> JCIRcvMsgError err
|
||||
CIRcvGroupInvitation groupInvitation memberRole -> JCIRcvGroupInvitation {groupInvitation, memberRole}
|
||||
CISndGroupInvitation groupInvitation memberRole -> JCISndGroupInvitation {groupInvitation, memberRole}
|
||||
CIRcvDirectEvent rcvDirectEvent -> JCIRcvDirectEvent {rcvDirectEvent}
|
||||
@@ -527,6 +542,7 @@ aciContentJSON = \case
|
||||
JCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration
|
||||
JCIRcvIntegrityError err -> ACIContent SMDRcv $ CIRcvIntegrityError err
|
||||
JCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n
|
||||
JCIRcvMsgError err -> ACIContent SMDRcv $ CIRcvMsgError err
|
||||
JCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole
|
||||
JCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole
|
||||
JCIRcvDirectEvent {rcvDirectEvent} -> ACIContent SMDRcv $ CIRcvDirectEvent rcvDirectEvent
|
||||
@@ -563,6 +579,7 @@ data DBJSONCIContent
|
||||
| DBJCIRcvCall {status :: CICallStatus, duration :: Int}
|
||||
| DBJCIRcvIntegrityError {msgError :: DBMsgErrorType}
|
||||
| DBJCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32}
|
||||
| DBJCIRcvMsgError {rcvMsgError :: RcvMsgError}
|
||||
| DBJCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
||||
| DBJCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
||||
| DBJCIRcvDirectEvent {rcvDirectEvent :: DBRcvDirectEvent}
|
||||
@@ -598,6 +615,7 @@ dbJsonCIContent = \case
|
||||
CIRcvCall status duration -> DBJCIRcvCall {status, duration}
|
||||
CIRcvIntegrityError err -> DBJCIRcvIntegrityError $ DBME err
|
||||
CIRcvDecryptionError err n -> DBJCIRcvDecryptionError err n
|
||||
CIRcvMsgError err -> DBJCIRcvMsgError err
|
||||
CIRcvGroupInvitation groupInvitation memberRole -> DBJCIRcvGroupInvitation {groupInvitation, memberRole}
|
||||
CISndGroupInvitation groupInvitation memberRole -> DBJCISndGroupInvitation {groupInvitation, memberRole}
|
||||
CIRcvDirectEvent rde -> DBJCIRcvDirectEvent $ RDE rde
|
||||
@@ -633,6 +651,7 @@ aciContentDBJSON = \case
|
||||
DBJCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration
|
||||
DBJCIRcvIntegrityError (DBME err) -> ACIContent SMDRcv $ CIRcvIntegrityError err
|
||||
DBJCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n
|
||||
DBJCIRcvMsgError err -> ACIContent SMDRcv $ CIRcvMsgError err
|
||||
DBJCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole
|
||||
DBJCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole
|
||||
DBJCIRcvDirectEvent (RDE rde) -> ACIContent SMDRcv $ CIRcvDirectEvent rde
|
||||
@@ -693,6 +712,8 @@ $(JQ.deriveJSON defaultJSON ''E2EInfo)
|
||||
|
||||
$(JQ.deriveJSON (enumJSON $ dropPrefix "MDE") ''MsgDecryptError)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RME") ''RcvMsgError)
|
||||
|
||||
$(JQ.deriveJSON (enumJSON $ dropPrefix "CIGIS") ''CIGroupInvitationStatus)
|
||||
|
||||
$(JQ.deriveJSON defaultJSON ''CIGroupInvitation)
|
||||
@@ -751,6 +772,7 @@ toCIContentTag ciContent = case ciContent of
|
||||
CIRcvCall {} -> "rcvCall"
|
||||
CIRcvIntegrityError _ -> "rcvIntegrityError"
|
||||
CIRcvDecryptionError {} -> "rcvDecryptionError"
|
||||
CIRcvMsgError _ -> "rcvMsgError"
|
||||
CIRcvGroupInvitation {} -> "rcvGroupInvitation"
|
||||
CISndGroupInvitation {} -> "sndGroupInvitation"
|
||||
CIRcvDirectEvent _ -> "rcvDirectEvent"
|
||||
|
||||
@@ -56,7 +56,7 @@ import Simplex.Chat.Types.Preferences
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion)
|
||||
import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder, fromTextField_)
|
||||
import Simplex.Messaging.Compression (Compressed, compress1, decompress1)
|
||||
import Simplex.Messaging.Compression (Compressed, compress1, decompress1, decompressedSize)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
@@ -799,7 +799,11 @@ parseChatMessages msg = case B.head msg of
|
||||
decodeCompressed :: ByteString -> [Either String AParsedMsg]
|
||||
decodeCompressed s = case smpDecode s of
|
||||
Left e -> [Left e]
|
||||
Right (compressed :: L.NonEmpty Compressed) -> concatMap (either (\e -> [Left e]) parseUncompressed' . decompress1 maxDecompressedMsgLength) compressed
|
||||
Right (compressed :: L.NonEmpty Compressed) -> case traverse decompressedSize compressed of
|
||||
Nothing -> [Left "compressed size not specified"]
|
||||
Just sizes
|
||||
| sum sizes > maxDecompressedMsgLength -> [Left "decompressed size exceeds limit"]
|
||||
| otherwise -> concatMap (either (\e -> [Left e]) parseUncompressed' . decompress1) compressed
|
||||
parseUncompressed' "" = [Left "empty string"]
|
||||
parseUncompressed' s = parseUncompressed (B.head s) s
|
||||
-- Binary batch format: '=' <count:1> (<len:2> <body>)*
|
||||
|
||||
@@ -177,6 +177,7 @@ data GroupFeature
|
||||
| GFReports
|
||||
| GFHistory
|
||||
| GFSessions
|
||||
| GFComments
|
||||
deriving (Show)
|
||||
|
||||
data SGroupFeature (f :: GroupFeature) where
|
||||
@@ -190,6 +191,7 @@ data SGroupFeature (f :: GroupFeature) where
|
||||
SGFReports :: SGroupFeature 'GFReports
|
||||
SGFHistory :: SGroupFeature 'GFHistory
|
||||
SGFSessions :: SGroupFeature 'GFSessions
|
||||
SGFComments :: SGroupFeature 'GFComments
|
||||
|
||||
deriving instance Show (SGroupFeature f)
|
||||
|
||||
@@ -217,6 +219,7 @@ groupFeatureNameText = \case
|
||||
GFReports -> "Member reports"
|
||||
GFHistory -> "Recent history"
|
||||
GFSessions -> "Chat sessions"
|
||||
GFComments -> "Comments"
|
||||
|
||||
groupFeatureNameText' :: SGroupFeature f -> Text
|
||||
groupFeatureNameText' = groupFeatureNameText . toGroupFeature
|
||||
@@ -230,6 +233,11 @@ groupFeatureMemberAllowed' feature role prefs =
|
||||
let pref = getGroupPreference feature prefs
|
||||
in getField @"enable" pref == FEOn && maybe True (role >=) (getField @"role" pref)
|
||||
|
||||
-- TODO: some preferences are channel-only (e.g., comments) and should not generate
|
||||
-- UI items or be configurable in regular groups. Currently they are simply excluded
|
||||
-- from this list. When more channel-only or group-only preferences are added,
|
||||
-- consider adding a scope property to GroupFeatureI (e.g., GFScopeAll | GFScopeChannel | GFScopeGroup)
|
||||
-- and filtering at the call sites in createGroupFeatureItems_ / createGroupFeatureChangedItems.
|
||||
allGroupFeatures :: [AGroupFeature]
|
||||
allGroupFeatures =
|
||||
[ AGF SGFTimedMessages,
|
||||
@@ -244,7 +252,7 @@ allGroupFeatures =
|
||||
]
|
||||
|
||||
groupPrefSel :: SGroupFeature f -> GroupPreferences -> Maybe (GroupFeaturePreference f)
|
||||
groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, sessions} = case f of
|
||||
groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, sessions, comments} = case f of
|
||||
SGFTimedMessages -> timedMessages
|
||||
SGFDirectMessages -> directMessages
|
||||
SGFFullDelete -> fullDelete
|
||||
@@ -255,6 +263,7 @@ groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reac
|
||||
SGFReports -> reports
|
||||
SGFHistory -> history
|
||||
SGFSessions -> sessions
|
||||
SGFComments -> comments
|
||||
|
||||
toGroupFeature :: SGroupFeature f -> GroupFeature
|
||||
toGroupFeature = \case
|
||||
@@ -268,6 +277,7 @@ toGroupFeature = \case
|
||||
SGFReports -> GFReports
|
||||
SGFHistory -> GFHistory
|
||||
SGFSessions -> GFSessions
|
||||
SGFComments -> GFComments
|
||||
|
||||
class GroupPreferenceI p where
|
||||
getGroupPreference :: SGroupFeature f -> p -> GroupFeaturePreference f
|
||||
@@ -279,7 +289,7 @@ instance GroupPreferenceI (Maybe GroupPreferences) where
|
||||
getGroupPreference pt prefs = fromMaybe (getGroupPreference pt defaultGroupPrefs) (groupPrefSel pt =<< prefs)
|
||||
|
||||
instance GroupPreferenceI FullGroupPreferences where
|
||||
getGroupPreference f FullGroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, sessions} = case f of
|
||||
getGroupPreference f FullGroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, reports, history, sessions, comments} = case f of
|
||||
SGFTimedMessages -> timedMessages
|
||||
SGFDirectMessages -> directMessages
|
||||
SGFFullDelete -> fullDelete
|
||||
@@ -290,6 +300,7 @@ instance GroupPreferenceI FullGroupPreferences where
|
||||
SGFReports -> reports
|
||||
SGFHistory -> history
|
||||
SGFSessions -> sessions
|
||||
SGFComments -> comments
|
||||
{-# INLINE getGroupPreference #-}
|
||||
|
||||
-- collection of optional group preferences
|
||||
@@ -304,6 +315,7 @@ data GroupPreferences = GroupPreferences
|
||||
reports :: Maybe ReportsGroupPreference,
|
||||
history :: Maybe HistoryGroupPreference,
|
||||
sessions :: Maybe SessionsGroupPreference,
|
||||
comments :: Maybe CommentsGroupPreference,
|
||||
commands :: Maybe [ChatBotCommand]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
@@ -354,6 +366,7 @@ setGroupPreference_ f pref prefs =
|
||||
SGFReports -> prefs {reports = pref}
|
||||
SGFHistory -> prefs {history = pref}
|
||||
SGFSessions -> prefs {sessions = pref}
|
||||
SGFComments -> prefs {comments = pref}
|
||||
|
||||
setGroupTimedMessagesPreference :: TimedMessagesGroupPreference -> Maybe GroupPreferences -> GroupPreferences
|
||||
setGroupTimedMessagesPreference pref prefs_ =
|
||||
@@ -396,6 +409,7 @@ data FullGroupPreferences = FullGroupPreferences
|
||||
reports :: ReportsGroupPreference,
|
||||
history :: HistoryGroupPreference,
|
||||
sessions :: SessionsGroupPreference,
|
||||
comments :: CommentsGroupPreference,
|
||||
commands :: ListDef ChatBotCommand
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
@@ -465,11 +479,12 @@ defaultGroupPrefs =
|
||||
reports = ReportsGroupPreference {enable = FEOn},
|
||||
history = HistoryGroupPreference {enable = FEOff},
|
||||
sessions = SessionsGroupPreference {enable = FEOff, role = Nothing},
|
||||
comments = CommentsGroupPreference {enable = FEOff, duration = Nothing},
|
||||
commands = ListDef []
|
||||
}
|
||||
|
||||
emptyGroupPrefs :: GroupPreferences
|
||||
emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
|
||||
emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
|
||||
|
||||
businessGroupPrefs :: Preferences -> GroupPreferences
|
||||
businessGroupPrefs Preferences {timedMessages, fullDelete, reactions, voice, files, sessions, commands} =
|
||||
@@ -501,6 +516,7 @@ defaultBusinessGroupPrefs =
|
||||
reports = Just $ ReportsGroupPreference FEOff,
|
||||
history = Just $ HistoryGroupPreference FEOn,
|
||||
sessions = Just $ SessionsGroupPreference FEOn Nothing,
|
||||
comments = Just $ CommentsGroupPreference FEOff Nothing,
|
||||
commands = Nothing
|
||||
}
|
||||
|
||||
@@ -635,6 +651,14 @@ data SessionsGroupPreference = SessionsGroupPreference
|
||||
{enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- Channel comments. ``duration` is time in seconds since post creation
|
||||
-- after which a channel post stops accepting new comments; `Nothing` means accept comments indefinitely.
|
||||
data CommentsGroupPreference = CommentsGroupPreference
|
||||
{ enable :: GroupFeatureEnabled,
|
||||
duration :: Maybe Int
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
class (Eq (GroupFeaturePreference f), HasField "enable" (GroupFeaturePreference f) GroupFeatureEnabled) => GroupFeatureI f where
|
||||
type GroupFeaturePreference (f :: GroupFeature) = p | p -> f
|
||||
sGroupFeature :: SGroupFeature f
|
||||
@@ -678,6 +702,9 @@ instance HasField "enable" HistoryGroupPreference GroupFeatureEnabled where
|
||||
instance HasField "enable" SessionsGroupPreference GroupFeatureEnabled where
|
||||
hasField p@SessionsGroupPreference {enable} = (\e -> p {enable = e}, enable)
|
||||
|
||||
instance HasField "enable" CommentsGroupPreference GroupFeatureEnabled where
|
||||
hasField p@CommentsGroupPreference {enable} = (\e -> p {enable = e}, enable)
|
||||
|
||||
instance GroupFeatureI 'GFTimedMessages where
|
||||
type GroupFeaturePreference 'GFTimedMessages = TimedMessagesGroupPreference
|
||||
sGroupFeature = SGFTimedMessages
|
||||
@@ -738,6 +765,12 @@ instance GroupFeatureI 'GFSessions where
|
||||
groupPrefParam _ = Nothing
|
||||
groupPrefRole SessionsGroupPreference {role} = role
|
||||
|
||||
instance GroupFeatureI 'GFComments where
|
||||
type GroupFeaturePreference 'GFComments = CommentsGroupPreference
|
||||
sGroupFeature = SGFComments
|
||||
groupPrefParam CommentsGroupPreference {duration} = duration
|
||||
groupPrefRole _ = Nothing
|
||||
|
||||
instance GroupFeatureNoRoleI 'GFTimedMessages
|
||||
|
||||
instance GroupFeatureNoRoleI 'GFFullDelete
|
||||
@@ -748,6 +781,8 @@ instance GroupFeatureNoRoleI 'GFReports
|
||||
|
||||
instance GroupFeatureNoRoleI 'GFHistory
|
||||
|
||||
instance GroupFeatureNoRoleI 'GFComments
|
||||
|
||||
instance HasField "role" DirectMessagesGroupPreference (Maybe GroupMemberRole) where
|
||||
hasField p@DirectMessagesGroupPreference {role} = (\r -> p {role = r}, role)
|
||||
|
||||
@@ -788,6 +823,7 @@ groupPrefStateText feature pref param role =
|
||||
groupParamText_ :: GroupFeature -> Maybe Int -> Text
|
||||
groupParamText_ feature param = case feature of
|
||||
GFTimedMessages -> maybe "" (\p -> " (" <> timedTTLText p <> ")") param
|
||||
GFComments -> maybe "" (\p -> " (close after " <> timedTTLText p <> ")") param
|
||||
_ -> ""
|
||||
|
||||
groupPreferenceText :: forall f. GroupFeatureI f => GroupFeaturePreference f -> Text
|
||||
@@ -938,6 +974,7 @@ mergeGroupPreferences groupPreferences =
|
||||
reports = pref SGFReports,
|
||||
history = pref SGFHistory,
|
||||
sessions = pref SGFSessions,
|
||||
comments = pref SGFComments,
|
||||
commands = ListDef $ fromMaybe [] $ groupPreferences >>= commands_
|
||||
}
|
||||
where
|
||||
@@ -957,6 +994,7 @@ toGroupPreferences groupPreferences@FullGroupPreferences {commands = ListDef cmd
|
||||
reports = pref SGFReports,
|
||||
history = pref SGFHistory,
|
||||
sessions = pref SGFSessions,
|
||||
comments = pref SGFComments,
|
||||
commands = Just cmds
|
||||
}
|
||||
where
|
||||
@@ -1091,6 +1129,12 @@ instance FromJSON SessionsGroupPreference where
|
||||
parseJSON v = $(J.mkParseJSON defaultJSON ''SessionsGroupPreference) v
|
||||
omittedField = Just SessionsGroupPreference {enable = FEOff, role = Nothing}
|
||||
|
||||
$(J.deriveToJSON defaultJSON ''CommentsGroupPreference)
|
||||
|
||||
instance FromJSON CommentsGroupPreference where
|
||||
parseJSON v = $(J.mkParseJSON defaultJSON ''CommentsGroupPreference) v
|
||||
omittedField = Just CommentsGroupPreference {enable = FEOff, duration = Nothing}
|
||||
|
||||
$(J.deriveJSON defaultJSON ''GroupPreferences)
|
||||
|
||||
instance ToField GroupPreferences where
|
||||
|
||||
@@ -671,6 +671,7 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa
|
||||
CIDirectRcv -> case content of
|
||||
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc
|
||||
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta
|
||||
CIRcvMsgError err -> viewRcvMsgError from err ts tz meta
|
||||
CIRcvGroupEvent {} -> showRcvItemProhibited from
|
||||
_ -> showRcvItem from
|
||||
where
|
||||
@@ -694,6 +695,7 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa
|
||||
rcvGroupItem m_ = case content of
|
||||
CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc
|
||||
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta
|
||||
CIRcvMsgError err -> viewRcvMsgError from err ts tz meta
|
||||
CIRcvGroupInvitation {} | isJust m_ -> showRcvItemProhibited from
|
||||
CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g scopeInfo m_) context meta [plainContent content] False
|
||||
CIRcvBlocked {} -> receivedWithTime_ ts tz (ttyFromGroup g scopeInfo m_) context meta [plainContent content] False
|
||||
@@ -715,6 +717,7 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa
|
||||
CILocalRcv -> case content of
|
||||
CIRcvMsgContent mc -> withLocalFile from $ rcvMsg from context mc
|
||||
CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta
|
||||
CIRcvMsgError err -> viewRcvMsgError from err ts tz meta
|
||||
CIRcvGroupEvent {} -> showRcvItemProhibited from
|
||||
_ -> showRcvItem from
|
||||
where
|
||||
@@ -991,6 +994,9 @@ viewRcvIntegrityError from msgErr ts tz meta = receivedWithTime_ ts tz from [] m
|
||||
viewMsgIntegrityError :: MsgErrorType -> [StyledString]
|
||||
viewMsgIntegrityError err = [ttyError $ msgIntegrityError err]
|
||||
|
||||
viewRcvMsgError :: StyledString -> RcvMsgError -> CurrentTime -> TimeZone -> CIMeta c 'MDRcv -> [StyledString]
|
||||
viewRcvMsgError from rcvErr ts tz meta = receivedWithTime_ ts tz from [] meta [ttyError $ rcvMsgErrorText rcvErr] False
|
||||
|
||||
viewInvalidConnReq :: [StyledString]
|
||||
viewInvalidConnReq =
|
||||
[ "",
|
||||
@@ -2656,7 +2662,7 @@ viewChatError isCmd logLevel testView = \case
|
||||
BRContent -> "content violates conditions of use"
|
||||
BROKER _ (NETWORK _) | not isCmd -> []
|
||||
BROKER _ TIMEOUT | not isCmd -> []
|
||||
AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug || isCmd]
|
||||
AGENT A_DUPLICATE {} -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug || isCmd]
|
||||
AGENT (A_PROHIBITED e) -> [withConnEntity <> "error: AGENT A_PROHIBITED, " <> plain e | logLevel <= CLLWarning || isCmd]
|
||||
CONN NOT_FOUND _ -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning || isCmd]
|
||||
CRITICAL restart e -> [plain $ "critical error: " <> e] <> ["please restart the app" | restart]
|
||||
|
||||
@@ -101,7 +101,7 @@ testChatPreferences :: Maybe Preferences
|
||||
testChatPreferences = Just Preferences {voice = Just VoicePreference {allow = FAYes}, files = Nothing, fullDelete = Nothing, timedMessages = Nothing, calls = Nothing, reactions = Just ReactionsPreference {allow = FAYes}, sessions = Nothing, commands = Nothing}
|
||||
|
||||
testGroupPreferences :: Maybe GroupPreferences
|
||||
testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, sessions = Nothing, commands = Nothing}
|
||||
testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, sessions = Nothing, comments = Nothing, commands = Nothing}
|
||||
|
||||
testProfile :: Profile
|
||||
testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences}
|
||||
|
||||
Reference in New Issue
Block a user