mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-06-04 01:41:43 +00:00
Merge branch 'master' into master-android
This commit is contained in:
@@ -2481,10 +2481,15 @@ func processReceivedMsg(_ res: ChatEvent) async {
|
||||
await MainActor.run {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
if m.chatId == groupInfo.id,
|
||||
case .memberSupport(nil) = m.secondaryIM?.groupScopeInfo {
|
||||
await MainActor.run {
|
||||
m.secondaryPendingInviteeChatOpened = false
|
||||
if m.chatId == groupInfo.id {
|
||||
if groupInfo.membership.memberPending {
|
||||
await MainActor.run {
|
||||
m.secondaryPendingInviteeChatOpened = true
|
||||
}
|
||||
} else if case .memberSupport(nil) = m.secondaryIM?.groupScopeInfo {
|
||||
await MainActor.run {
|
||||
m.secondaryPendingInviteeChatOpened = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +290,13 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: chatModel.secondaryPendingInviteeChatOpened) { secondaryChatOpened in
|
||||
if secondaryChatOpened {
|
||||
ItemsModel.loadSecondaryChat(chat.id, chatFilter: .groupChatScopeContext(groupScopeInfo: userSupportScopeInfo)) {
|
||||
showUserSupportChatSheet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { cId in
|
||||
ConnectProgressManager.shared.cancelConnectProgress()
|
||||
showChatInfoSheet = false
|
||||
@@ -2025,7 +2032,7 @@ struct ChatView: View {
|
||||
} label: {
|
||||
Label(
|
||||
NSLocalizedString("Save", comment: "chat item action"),
|
||||
systemImage: file.cryptoArgs == nil ? "square.and.arrow.down" : "lock.open"
|
||||
systemImage: "square.and.arrow.down"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,9 +63,10 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
field.textAlignment = alignment(text)
|
||||
field.updateFont()
|
||||
field.updateHeight(updateBindingNow: false)
|
||||
field.placeholder = text.isEmpty ? placeholder : ""
|
||||
}
|
||||
if field.placeholder != placeholder {
|
||||
field.placeholder = placeholder
|
||||
field.placeholder = text.isEmpty ? placeholder : ""
|
||||
}
|
||||
if field.selectedRange != selectedRange {
|
||||
field.selectedRange = selectedRange
|
||||
|
||||
@@ -426,7 +426,7 @@ struct GroupChatInfoView: View {
|
||||
|
||||
if user {
|
||||
v
|
||||
} else if groupInfo.membership.memberRole >= .admin {
|
||||
} else if groupInfo.membership.memberRole >= .moderator {
|
||||
// TODO if there are more actions, refactor with lists of swipeActions
|
||||
let canBlockForAll = member.canBlockForAll(groupInfo: groupInfo)
|
||||
let canRemove = member.canBeRemoved(groupInfo: groupInfo)
|
||||
@@ -469,7 +469,7 @@ struct GroupChatInfoView: View {
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} else {
|
||||
let role = member.memberRole
|
||||
if [.owner, .admin, .observer].contains(role) {
|
||||
if [.owner, .admin, .moderator, .observer].contains(role) {
|
||||
Text(member.memberRole.text)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if groupInfo.membership.memberRole >= .admin {
|
||||
if groupInfo.membership.memberRole >= .moderator {
|
||||
adminDestructiveSection(member)
|
||||
} else {
|
||||
nonAdminBlockSection(member)
|
||||
|
||||
@@ -146,7 +146,7 @@ struct SelectedItemsBottomToolbar: View {
|
||||
private func possibleToModerate(_ chatInfo: ChatInfo) -> Bool {
|
||||
return switch chatInfo {
|
||||
case let .group(groupInfo, _):
|
||||
groupInfo.membership.memberRole >= .admin
|
||||
groupInfo.membership.memberRole >= .moderator
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ struct ContactConnectionInfo: View {
|
||||
.onAppear {
|
||||
localAlias = contactConnection.localAlias
|
||||
}
|
||||
.onDisappear(perform: setConnectionAlias)
|
||||
}
|
||||
|
||||
private func setConnectionAlias() {
|
||||
|
||||
@@ -412,7 +412,7 @@ struct SubscriptionStatusIndicatorView: View {
|
||||
var hasSess: Bool
|
||||
|
||||
var body: some View {
|
||||
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(
|
||||
let (color, variableValue, opacity) = subscriptionStatusInfo(
|
||||
online: m.networkInfo.online,
|
||||
usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil,
|
||||
subs: subs,
|
||||
@@ -431,25 +431,19 @@ struct SubscriptionStatusIndicatorView: View {
|
||||
|
||||
struct SubscriptionStatusPercentageView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var subs: SMPServerSubs
|
||||
var hasSess: Bool
|
||||
|
||||
var body: some View {
|
||||
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(
|
||||
online: m.networkInfo.online,
|
||||
usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil,
|
||||
subs: subs,
|
||||
hasSess: hasSess,
|
||||
primaryColor: theme.colors.primary
|
||||
)
|
||||
Text(verbatim: "\(Int(floor(statusPercent * 100)))%")
|
||||
let statusPercent = subscriptionStatusPercent(online: m.networkInfo.online, subs: subs, hasSess: hasSess)
|
||||
let percentText: String = subs.total > 0 || hasSess ? "\(Int(floor(statusPercent * 100)))%" : "%"
|
||||
Text(percentText)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
func subscriptionStatusColorAndPercentage(online: Bool, usesProxy: Bool, subs: SMPServerSubs, hasSess: Bool, primaryColor: Color) -> (Color, Double, Double, Double) {
|
||||
func subscriptionStatusInfo(online: Bool, usesProxy: Bool, subs: SMPServerSubs, hasSess: Bool, primaryColor: Color) -> (Color, Double, Double) {
|
||||
func roundedToQuarter(_ n: Double) -> Double {
|
||||
n >= 1 ? 1
|
||||
: n <= 0 ? 0
|
||||
@@ -457,26 +451,28 @@ func subscriptionStatusColorAndPercentage(online: Bool, usesProxy: Bool, subs: S
|
||||
}
|
||||
|
||||
let activeColor: Color = usesProxy ? .indigo : primaryColor
|
||||
let noConnColorAndPercent: (Color, Double, Double, Double) = (Color(uiColor: .tertiaryLabel), 1, 1, 0)
|
||||
let noConnColorAndPercent: (Color, Double, Double) = (Color(uiColor: .tertiaryLabel), 1, 1)
|
||||
let activeSubsRounded = roundedToQuarter(subs.shareOfActive)
|
||||
|
||||
return !online
|
||||
? noConnColorAndPercent
|
||||
: (
|
||||
subs.total == 0 && !hasSess
|
||||
? (activeColor, 0, 0.33, 0) // On freshly installed app (without chats) and on app start
|
||||
: (
|
||||
subs.ssActive == 0
|
||||
? (
|
||||
hasSess ? (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) : noConnColorAndPercent
|
||||
)
|
||||
: ( // ssActive > 0
|
||||
hasSess
|
||||
? (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive)
|
||||
: (.orange, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) // This would mean implementation error
|
||||
)
|
||||
)
|
||||
: subs.total == 0 && !hasSess
|
||||
? (activeColor, 0, 0.33) // On freshly installed app (without chats) and on app start
|
||||
: subs.ssActive == 0
|
||||
? (
|
||||
hasSess ? (activeColor, activeSubsRounded, subs.shareOfActive) : noConnColorAndPercent
|
||||
)
|
||||
: ( // ssActive > 0
|
||||
hasSess
|
||||
? (activeColor, activeSubsRounded, subs.shareOfActive)
|
||||
: (.orange, activeSubsRounded, subs.shareOfActive) // This would mean implementation error
|
||||
)
|
||||
}
|
||||
|
||||
func subscriptionStatusPercent(online: Bool, subs: SMPServerSubs, hasSess: Bool) -> Double {
|
||||
online && (hasSess || (subs.total > 0 && subs.ssActive > 0))
|
||||
? subs.shareOfActive
|
||||
: 0
|
||||
}
|
||||
|
||||
struct SMPServerSummaryView: View {
|
||||
|
||||
@@ -97,7 +97,7 @@ struct UserPicker: View {
|
||||
}
|
||||
.onAppear {
|
||||
// This check prevents the call of listUsers after the app is suspended, and the database is closed.
|
||||
if case .active = scenePhase {
|
||||
if case .active = scenePhase, hasChatCtrl() {
|
||||
currentUser = m.currentUser?.userId
|
||||
Task {
|
||||
do {
|
||||
|
||||
@@ -998,7 +998,7 @@ private func showOwnGroupLinkConfirmConnectSheet(
|
||||
title: NSLocalizedString("Open group", comment: "new chat action"),
|
||||
style: .default,
|
||||
handler: { _ in
|
||||
openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil)
|
||||
openKnownGroup(groupInfo, dismiss: dismiss, cleanup: cleanup)
|
||||
}
|
||||
),
|
||||
UIAlertAction(
|
||||
@@ -1052,8 +1052,7 @@ private func showPrepareContactAlert(
|
||||
let chat = try await apiPrepareContact(connLink: connectionLink, contactShortLinkData: contactShortLinkData)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.addChat(Chat(chat))
|
||||
openKnownChat(chat.id, dismiss: dismiss, showAlreadyExistsAlert: nil)
|
||||
cleanup?()
|
||||
openKnownChat(chat.id, dismiss: dismiss, cleanup: cleanup)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("showPrepareContactAlert apiPrepareContact error: \(error.localizedDescription)")
|
||||
@@ -1088,8 +1087,7 @@ private func showPrepareGroupAlert(
|
||||
let chat = try await apiPrepareGroup(connLink: connectionLink, groupShortLinkData: groupShortLinkData)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.addChat(Chat(chat))
|
||||
openKnownChat(chat.id, dismiss: dismiss, showAlreadyExistsAlert: nil)
|
||||
cleanup?()
|
||||
openKnownChat(chat.id, dismiss: dismiss, cleanup: cleanup)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("showPrepareGroupAlert apiPrepareGroup error: \(error.localizedDescription)")
|
||||
@@ -1124,7 +1122,7 @@ private func showOpenKnownContactAlert(
|
||||
? NSLocalizedString("Open new chat", comment: "new chat action")
|
||||
: NSLocalizedString("Open chat", comment: "new chat action"),
|
||||
onConfirm: {
|
||||
openKnownContact(contact, dismiss: dismiss, showAlreadyExistsAlert: nil)
|
||||
openKnownContact(contact, dismiss: dismiss, cleanup: nil)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1156,7 +1154,7 @@ private func showOpenKnownGroupAlert(
|
||||
: NSLocalizedString("Open chat", comment: "new chat action")
|
||||
),
|
||||
onConfirm: {
|
||||
openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil)
|
||||
openKnownGroup(groupInfo, dismiss: dismiss, cleanup: nil)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1471,28 +1469,28 @@ private func connectViaLink(
|
||||
}
|
||||
}
|
||||
|
||||
func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
|
||||
func openKnownContact(_ contact: Contact, dismiss: Bool, cleanup: (() -> Void)?) {
|
||||
if let c = ChatModel.shared.getContactChat(contact.contactId) {
|
||||
openKnownChat(c.id, dismiss: dismiss, showAlreadyExistsAlert: showAlreadyExistsAlert)
|
||||
openKnownChat(c.id, dismiss: dismiss, cleanup: cleanup)
|
||||
}
|
||||
}
|
||||
|
||||
func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
|
||||
func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, cleanup: (() -> Void)?) {
|
||||
if let g = ChatModel.shared.getGroupChat(groupInfo.groupId) {
|
||||
openKnownChat(g.id, dismiss: dismiss, showAlreadyExistsAlert: showAlreadyExistsAlert)
|
||||
openKnownChat(g.id, dismiss: dismiss, cleanup: cleanup)
|
||||
}
|
||||
}
|
||||
|
||||
func openKnownChat(_ chatId: ChatId, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
|
||||
func openKnownChat(_ chatId: ChatId, dismiss: Bool, cleanup: (() -> Void)?) {
|
||||
if dismiss {
|
||||
dismissAllSheets(animated: true) {
|
||||
ItemsModel.shared.loadOpenChat(chatId) {
|
||||
showAlreadyExistsAlert?()
|
||||
cleanup?()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ItemsModel.shared.loadOpenChat(chatId) {
|
||||
showAlreadyExistsAlert?()
|
||||
cleanup?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,8 +183,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.4.0.5.1-H9b1PUqJfdC45BY0VzYHX-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5.1-H9b1PUqJfdC45BY0VzYHX-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5.1-H9b1PUqJfdC45BY0VzYHX.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5.1-H9b1PUqJfdC45BY0VzYHX.a */; };
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.6-DxjNre3mzYy3LhsUMQ3UN8-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.6-DxjNre3mzYy3LhsUMQ3UN8-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.6-DxjNre3mzYy3LhsUMQ3UN8.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.6-DxjNre3mzYy3LhsUMQ3UN8.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 */; };
|
||||
@@ -553,8 +553,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.4.0.5.1-H9b1PUqJfdC45BY0VzYHX-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.5.1-H9b1PUqJfdC45BY0VzYHX-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5.1-H9b1PUqJfdC45BY0VzYHX.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.5.1-H9b1PUqJfdC45BY0VzYHX.a"; sourceTree = "<group>"; };
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.6-DxjNre3mzYy3LhsUMQ3UN8-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.6-DxjNre3mzYy3LhsUMQ3UN8-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.6-DxjNre3mzYy3LhsUMQ3UN8.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.6-DxjNre3mzYy3LhsUMQ3UN8.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>"; };
|
||||
@@ -714,8 +714,8 @@
|
||||
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
|
||||
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
|
||||
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5.1-H9b1PUqJfdC45BY0VzYHX-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5.1-H9b1PUqJfdC45BY0VzYHX.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.6-DxjNre3mzYy3LhsUMQ3UN8-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.6-DxjNre3mzYy3LhsUMQ3UN8.a in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -800,8 +800,8 @@
|
||||
64C829992D54AEEE006B9E89 /* libffi.a */,
|
||||
64C829982D54AEED006B9E89 /* libgmp.a */,
|
||||
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5.1-H9b1PUqJfdC45BY0VzYHX-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5.1-H9b1PUqJfdC45BY0VzYHX.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.6-DxjNre3mzYy3LhsUMQ3UN8-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.6-DxjNre3mzYy3LhsUMQ3UN8.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -2005,7 +2005,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 288;
|
||||
CURRENT_PROJECT_VERSION = 289;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2055,7 +2055,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 288;
|
||||
CURRENT_PROJECT_VERSION = 289;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2097,7 +2097,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 288;
|
||||
CURRENT_PROJECT_VERSION = 289;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
@@ -2117,7 +2117,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 288;
|
||||
CURRENT_PROJECT_VERSION = 289;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
@@ -2142,7 +2142,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 288;
|
||||
CURRENT_PROJECT_VERSION = 289;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -2179,7 +2179,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 288;
|
||||
CURRENT_PROJECT_VERSION = 289;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -2216,7 +2216,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 288;
|
||||
CURRENT_PROJECT_VERSION = 289;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2267,7 +2267,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 288;
|
||||
CURRENT_PROJECT_VERSION = 289;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2318,7 +2318,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 288;
|
||||
CURRENT_PROJECT_VERSION = 289;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2352,7 +2352,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 288;
|
||||
CURRENT_PROJECT_VERSION = 289;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
|
||||
@@ -1768,9 +1768,9 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var sndReady: Bool { get { ready || activeConn?.connStatus == .sndReady } }
|
||||
public var active: Bool { get { contactStatus == .active } }
|
||||
public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } }
|
||||
public var nextConnectPrepared: Bool { preparedContact != nil && (activeConn == nil || activeConn?.connStatus == .prepared) }
|
||||
public var nextConnectPrepared: Bool { active && preparedContact != nil && (activeConn == nil || activeConn?.connStatus == .prepared) }
|
||||
public var profileChangeProhibited: Bool { activeConn != nil }
|
||||
public var nextAcceptContactRequest: Bool { contactRequestId != nil && (activeConn == nil || activeConn?.connStatus == .new) }
|
||||
public var nextAcceptContactRequest: Bool { active && contactRequestId != nil && (activeConn == nil || activeConn?.connStatus == .new) }
|
||||
public var sendMsgToConnect: Bool { nextSendGrpInv || nextConnectPrepared }
|
||||
public var displayName: String { localAlias == "" ? profile.displayName : localAlias }
|
||||
public var fullName: String { get { profile.fullName } }
|
||||
@@ -2419,8 +2419,8 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
|
||||
|
||||
public func canBlockForAll(groupInfo: GroupInfo) -> Bool {
|
||||
let userRole = groupInfo.membership.memberRole
|
||||
return memberStatus != .memRemoved && memberStatus != .memLeft && memberRole < .admin
|
||||
&& userRole >= .admin && userRole >= memberRole && groupInfo.membership.memberActive
|
||||
return memberStatus != .memRemoved && memberStatus != .memLeft && memberRole < .moderator
|
||||
&& userRole >= .moderator && userRole >= memberRole && groupInfo.membership.memberActive
|
||||
}
|
||||
|
||||
public var canReceiveReports: Bool {
|
||||
@@ -2980,12 +2980,12 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
switch (chatInfo, chatDir) {
|
||||
case let (.group(groupInfo, _), .groupRcv(groupMember)):
|
||||
let m = groupInfo.membership
|
||||
return m.memberRole >= .admin && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil
|
||||
return m.memberRole >= .moderator && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil
|
||||
? (groupInfo, groupMember)
|
||||
: nil
|
||||
case let (.group(groupInfo, _), .groupSnd):
|
||||
let m = groupInfo.membership
|
||||
return m.memberRole >= .admin ? (groupInfo, nil) : nil
|
||||
return m.memberRole >= .moderator ? (groupInfo, nil) : nil
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ actual fun SaveOrOpenFileMenu(
|
||||
}
|
||||
ItemAction(
|
||||
stringResource(MR.strings.save_verb),
|
||||
painterResource(if (encrypted) MR.images.ic_lock_open_right else MR.images.ic_download),
|
||||
painterResource(MR.images.ic_download),
|
||||
color = MaterialTheme.colors.primary,
|
||||
onClick = {
|
||||
saveFile()
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ actual fun ReactionIcon(text: String, fontSize: TextUnit) {
|
||||
@Composable
|
||||
actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) {
|
||||
val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = {
|
||||
ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = {
|
||||
when (cItem.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.status == PermissionStatus.Granted) {
|
||||
|
||||
+2
-2
@@ -1743,9 +1743,9 @@ data class Contact(
|
||||
val active get() = contactStatus == ContactStatus.Active
|
||||
override val nextConnect get() = sendMsgToConnect
|
||||
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
|
||||
override val nextConnectPrepared get() = preparedContact != null && (activeConn == null || activeConn.connStatus == ConnStatus.Prepared)
|
||||
override val nextConnectPrepared get() = active && preparedContact != null && (activeConn == null || activeConn.connStatus == ConnStatus.Prepared)
|
||||
override val profileChangeProhibited get() = activeConn != null
|
||||
val nextAcceptContactRequest get() = contactRequestId != null && (activeConn == null || activeConn.connStatus == ConnStatus.New)
|
||||
val nextAcceptContactRequest get() = active && contactRequestId != null && (activeConn == null || activeConn.connStatus == ConnStatus.New)
|
||||
val sendMsgToConnect get() = nextSendGrpInv || nextConnectPrepared
|
||||
override val incognito get() = contactConnIncognito
|
||||
override fun featureEnabled(feature: ChatFeature) = when (feature) {
|
||||
|
||||
+1
@@ -2872,6 +2872,7 @@ object ChatController {
|
||||
}
|
||||
if (
|
||||
chatModel.chatId.value == r.groupInfo.id
|
||||
&& !r.groupInfo.membership.memberPending
|
||||
&& ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)
|
||||
&& chatModel.secondaryChatsContext.value?.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext
|
||||
) {
|
||||
|
||||
+1
-1
@@ -989,7 +989,7 @@ fun ChatLayout(
|
||||
if (oneHandUI.value) {
|
||||
StatusBarBackground()
|
||||
}
|
||||
Column(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
|
||||
Column(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
|
||||
Box {
|
||||
if (selectedChatItems.value == null) {
|
||||
MemberSupportChatAppBar(chatsCtx, chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_, { ModalManager.end.closeModal() }, onSearchValueChanged)
|
||||
|
||||
+75
-74
@@ -1101,86 +1101,87 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
|
||||
val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) }
|
||||
LaunchedEffect(allowedVoiceByPrefs) {
|
||||
if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) {
|
||||
// Voice was disabled right when this user records it, just cancel it
|
||||
cancelVoice()
|
||||
}
|
||||
}
|
||||
val needToAllowVoiceToContact = remember(chat.chatInfo) {
|
||||
chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.voice) {
|
||||
((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) &&
|
||||
contactPreference.allow == FeatureAllowed.YES
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { recState.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
when (it) {
|
||||
is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false)
|
||||
is RecordingState.Finished -> if (it.durationMs > 300) {
|
||||
onAudioAdded(it.filePath, it.durationMs, true)
|
||||
} else {
|
||||
cancelVoice()
|
||||
}
|
||||
is RecordingState.NotStarted -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(rememberUpdatedState(chat.chatInfo.sendMsgEnabled).value) {
|
||||
if (!chat.chatInfo.sendMsgEnabled) {
|
||||
clearCurrentDraft()
|
||||
clearState()
|
||||
}
|
||||
}
|
||||
|
||||
KeyChangeEffect(chatModel.chatId.value) { prevChatId ->
|
||||
val cs = composeState.value
|
||||
if (cs.liveMessage != null && (cs.message.text.isNotEmpty() || cs.liveMessage.sent)) {
|
||||
sendMessage(null)
|
||||
resetLinkPreview()
|
||||
clearPrevDraft(prevChatId)
|
||||
deleteUnusedFiles()
|
||||
} else if (cs.inProgress) {
|
||||
clearPrevDraft(prevChatId)
|
||||
} else if (!cs.empty) {
|
||||
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
|
||||
composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
|
||||
}
|
||||
if (saveLastDraft) {
|
||||
chatModel.draft.value = composeState.value
|
||||
chatModel.draftChatId.value = prevChatId
|
||||
}
|
||||
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
} else if (chatModel.draftChatId.value == chatModel.chatId.value && chatModel.draft.value != null) {
|
||||
composeState.value = chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
} else {
|
||||
clearPrevDraft(prevChatId)
|
||||
deleteUnusedFiles()
|
||||
}
|
||||
chatModel.removeLiveDummy()
|
||||
CIFile.cachedRemoteFileRequests.clear()
|
||||
}
|
||||
if (appPlatform.isDesktop) {
|
||||
// Don't enable this on Android, it breaks it, This method only works on desktop. For Android there is a `KeyChangeEffect(chatModel.chatId.value)`
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (chatModel.sharedContent.value is SharedContent.Forward && saveLastDraft && !composeState.value.empty) {
|
||||
chatModel.draft.value = composeState.value
|
||||
chatModel.draftChatId.value = chat.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SendMsgView_(
|
||||
disableSendButton: Boolean,
|
||||
placeholder: String? = null,
|
||||
sendToConnect: (() -> Unit)? = null
|
||||
) {
|
||||
val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) }
|
||||
LaunchedEffect(allowedVoiceByPrefs) {
|
||||
if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) {
|
||||
// Voice was disabled right when this user records it, just cancel it
|
||||
cancelVoice()
|
||||
}
|
||||
}
|
||||
val needToAllowVoiceToContact = remember(chat.chatInfo) {
|
||||
chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.voice) {
|
||||
((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) &&
|
||||
contactPreference.allow == FeatureAllowed.YES
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { recState.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
when (it) {
|
||||
is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false)
|
||||
is RecordingState.Finished -> if (it.durationMs > 300) {
|
||||
onAudioAdded(it.filePath, it.durationMs, true)
|
||||
} else {
|
||||
cancelVoice()
|
||||
}
|
||||
is RecordingState.NotStarted -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(rememberUpdatedState(chat.chatInfo.sendMsgEnabled).value) {
|
||||
if (!chat.chatInfo.sendMsgEnabled) {
|
||||
clearCurrentDraft()
|
||||
clearState()
|
||||
}
|
||||
}
|
||||
|
||||
KeyChangeEffect(chatModel.chatId.value) { prevChatId ->
|
||||
val cs = composeState.value
|
||||
if (cs.liveMessage != null && (cs.message.text.isNotEmpty() || cs.liveMessage.sent)) {
|
||||
sendMessage(null)
|
||||
resetLinkPreview()
|
||||
clearPrevDraft(prevChatId)
|
||||
deleteUnusedFiles()
|
||||
} else if (cs.inProgress) {
|
||||
clearPrevDraft(prevChatId)
|
||||
} else if (!cs.empty) {
|
||||
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
|
||||
composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
|
||||
}
|
||||
if (saveLastDraft) {
|
||||
chatModel.draft.value = composeState.value
|
||||
chatModel.draftChatId.value = prevChatId
|
||||
}
|
||||
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
} else if (chatModel.draftChatId.value == chatModel.chatId.value && chatModel.draft.value != null) {
|
||||
composeState.value = chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
} else {
|
||||
clearPrevDraft(prevChatId)
|
||||
deleteUnusedFiles()
|
||||
}
|
||||
chatModel.removeLiveDummy()
|
||||
CIFile.cachedRemoteFileRequests.clear()
|
||||
}
|
||||
if (appPlatform.isDesktop) {
|
||||
// Don't enable this on Android, it breaks it, This method only works on desktop. For Android there is a `KeyChangeEffect(chatModel.chatId.value)`
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (chatModel.sharedContent.value is SharedContent.Forward && saveLastDraft && !composeState.value.empty) {
|
||||
chatModel.draft.value = composeState.value
|
||||
chatModel.draftChatId.value = chat.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) }
|
||||
val sendButtonColor =
|
||||
if (chat.chatInfo.incognito)
|
||||
|
||||
+3
-2
@@ -56,6 +56,7 @@ fun MemberSupportChatAppBar(
|
||||
onSearchValueChanged: (String) -> Unit
|
||||
) {
|
||||
val oneHandUI = remember { ChatController.appPrefs.oneHandUI.state }
|
||||
val chatBottomBar = remember { ChatController.appPrefs.chatBottomBar.state }
|
||||
val showSearch = rememberSaveable { mutableStateOf(false) }
|
||||
val onBackClicked = {
|
||||
if (!showSearch.value) {
|
||||
@@ -71,7 +72,7 @@ fun MemberSupportChatAppBar(
|
||||
navigationButton = { NavigationButtonBack(onBackClicked) },
|
||||
title = { MemberSupportChatToolbarTitle(scopeMember_) },
|
||||
onTitleClick = null,
|
||||
onTop = !oneHandUI.value,
|
||||
onTop = !oneHandUI.value || !chatBottomBar.value,
|
||||
showSearch = showSearch.value,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = {
|
||||
@@ -85,7 +86,7 @@ fun MemberSupportChatAppBar(
|
||||
navigationButton = { NavigationButtonBack(onBackClicked) },
|
||||
fixedTitleText = stringResource(MR.strings.support_chat),
|
||||
onTitleClick = null,
|
||||
onTop = !oneHandUI.value,
|
||||
onTop = !oneHandUI.value || !chatBottomBar.value,
|
||||
showSearch = showSearch.value,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = {
|
||||
|
||||
+67
-53
@@ -212,61 +212,75 @@ fun ChatPreviewView(
|
||||
fun chatPreviewText() {
|
||||
val previewText = chatPreviewInfoText()
|
||||
val ci = chat.chatItems.lastOrNull()
|
||||
if (ci?.content?.hasMsgContent != true && previewText != null) {
|
||||
if (chatModelDraftChatId == chat.id && chatModelDraft != null) {
|
||||
val sp20 = with(LocalDensity.current) { 20.sp.toDp() }
|
||||
val (text: CharSequence, inlineTextContent) = remember(chatModelDraft) { chatModelDraft.message.text to messageDraft(chatModelDraft, sp20) }
|
||||
val formattedText = null
|
||||
MarkdownText(
|
||||
text,
|
||||
formattedText,
|
||||
toggleSecrets = false,
|
||||
linkMode = linkMode,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TextStyle(
|
||||
fontFamily = Inter,
|
||||
fontSize = 15.sp,
|
||||
color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight,
|
||||
lineHeight = 21.sp
|
||||
),
|
||||
inlineContent = inlineTextContent,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} else if (ci?.content?.hasMsgContent != true && previewText != null) {
|
||||
Text(previewText.first, color = previewText.second)
|
||||
} else if (ci != null) {
|
||||
if (showChatPreviews || (chatModelDraftChatId == chat.id && chatModelDraft != null)) {
|
||||
val sp20 = with(LocalDensity.current) { 20.sp.toDp() }
|
||||
val (text: CharSequence, inlineTextContent) = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { chatModelDraft.message.text to messageDraft(chatModelDraft, sp20) }
|
||||
ci.meta.itemDeleted == null -> ci.text to null
|
||||
else -> markedDeletedText(ci, chat.chatInfo) to null
|
||||
}
|
||||
val formattedText = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||
ci.meta.itemDeleted == null -> ci.formattedText
|
||||
else -> null
|
||||
}
|
||||
val prefix = when (val mc = ci.content.msgContent) {
|
||||
is MsgContent.MCReport ->
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(color = Color.Red, fontStyle = FontStyle.Italic)) {
|
||||
append(if (text.isEmpty()) mc.reason.text else "${mc.reason.text}: ")
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
MarkdownText(
|
||||
text,
|
||||
formattedText,
|
||||
sender = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||
cInfo is ChatInfo.Group && !ci.chatDir.sent && !ci.meta.showGroupAsSender -> ci.memberDisplayName
|
||||
else -> null
|
||||
},
|
||||
mentions = ci.mentions,
|
||||
userMemberId = when {
|
||||
cInfo is ChatInfo.Group -> cInfo.groupInfo.membership.memberId
|
||||
else -> null
|
||||
},
|
||||
toggleSecrets = false,
|
||||
linkMode = linkMode,
|
||||
senderBold = true,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TextStyle(
|
||||
fontFamily = Inter,
|
||||
fontSize = 15.sp,
|
||||
color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight,
|
||||
lineHeight = 21.sp
|
||||
),
|
||||
inlineContent = inlineTextContent,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
prefix = prefix
|
||||
)
|
||||
} else if (ci != null && showChatPreviews) {
|
||||
val (text: CharSequence, inlineTextContent) = when {
|
||||
ci.meta.itemDeleted == null -> ci.text to null
|
||||
else -> markedDeletedText(ci, chat.chatInfo) to null
|
||||
}
|
||||
val formattedText = when {
|
||||
ci.meta.itemDeleted == null -> ci.formattedText
|
||||
else -> null
|
||||
}
|
||||
val prefix = when (val mc = ci.content.msgContent) {
|
||||
is MsgContent.MCReport ->
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(color = Color.Red, fontStyle = FontStyle.Italic)) {
|
||||
append(if (text.isEmpty()) mc.reason.text else "${mc.reason.text}: ")
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
MarkdownText(
|
||||
text,
|
||||
formattedText,
|
||||
sender = when {
|
||||
cInfo is ChatInfo.Group && !ci.chatDir.sent && !ci.meta.showGroupAsSender -> ci.memberDisplayName
|
||||
else -> null
|
||||
},
|
||||
mentions = ci.mentions,
|
||||
userMemberId = when {
|
||||
cInfo is ChatInfo.Group -> cInfo.groupInfo.membership.memberId
|
||||
else -> null
|
||||
},
|
||||
toggleSecrets = false,
|
||||
linkMode = linkMode,
|
||||
senderBold = true,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TextStyle(
|
||||
fontFamily = Inter,
|
||||
fontSize = 15.sp,
|
||||
color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight,
|
||||
lineHeight = 21.sp
|
||||
),
|
||||
inlineContent = inlineTextContent,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
prefix = prefix
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ fun SubscriptionStatusIndicatorView(subs: SMPServerSubs, hasSess: Boolean, leadi
|
||||
val netCfg = rememberUpdatedState(chatModel.controller.getNetCfg())
|
||||
val statusColorAndPercentage = subscriptionStatusColorAndPercentage(chatModel.networkInfo.value.online, netCfg.value.socksProxy, subs, hasSess)
|
||||
val pref = remember { chatModel.controller.appPrefs.networkShowSubscriptionPercentage }
|
||||
val percentageText = "${(floor(statusColorAndPercentage.statusPercent * 100)).toInt()}%"
|
||||
val percentageText = if (subs.total > 0 || hasSess) "${(floor(statusColorAndPercentage.statusPercent * 100)).toInt()}%" else "%"
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ actual fun ReactionIcon(text: String, fontSize: TextUnit) {
|
||||
|
||||
@Composable
|
||||
actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = {
|
||||
ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = {
|
||||
val saveIfExists = {
|
||||
when (cItem.content.msgContent) {
|
||||
is MsgContent.MCImage, is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withLongRunningApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") }
|
||||
|
||||
@@ -24,13 +24,13 @@ android.nonTransitiveRClass=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=6.4-beta.5
|
||||
android.version_code=301
|
||||
android.version_name=6.4-beta.6
|
||||
android.version_code=302
|
||||
|
||||
android.bundle=false
|
||||
|
||||
desktop.version_name=6.4-beta.5
|
||||
desktop.version_code=110
|
||||
desktop.version_name=6.4-beta.6
|
||||
desktop.version_code=111
|
||||
|
||||
kotlin.version=1.9.23
|
||||
gradle.plugin.version=8.2.0
|
||||
|
||||
+27
-3
@@ -32,6 +32,7 @@ revision: 23.04.2024
|
||||
|
||||
[Privacy and security](#privacy-and-security)
|
||||
- [Does SimpleX support post quantum cryptography?](#does-simplex-support-post-quantum-cryptography)
|
||||
- [Why can't I use the same profile on different devices?](#why-cant-i-use-the-same-profile-on-different-devices)
|
||||
- [What user data can be provided on request?](#what-user-data-can-be-provided-on-request)
|
||||
- [Does SimpleX protect my IP address?](#does-simplex-protect-my-ip-address)
|
||||
- [Doesn't private message routing reinvent Tor?](#doesnt-private-message-routing-reinvent-tor)
|
||||
@@ -53,15 +54,15 @@ Please check our [Groups Directory](./DIRECTORY.md) in the first place. You migh
|
||||
|
||||
Database is essential for SimpleX Chat to function properly. In comparison to centralized messaging providers, it is _the user_ who is responsible for taking care of their data. On the other hand, user is sure that _nobody but them_ has access to it. Please read more about it: [Database](./guide/managing-data.md).
|
||||
|
||||
### Can I send files over SimpleX?
|
||||
### Can I send files over SimpleX?
|
||||
|
||||
Of course! While doing so, you are using a _state-of-the-art_ protocol that greatly reduces metadata leaks. Please read more about it: [XFTP Protocol](../blog/20230301-simplex-file-transfer-protocol.md).
|
||||
|
||||
### What’s incognito profile?
|
||||
|
||||
This feature is unique to SimpleX Chat – it is independent from chat profiles.
|
||||
This feature is unique to SimpleX Chat – it is independent from chat profiles.
|
||||
|
||||
When "Incognito Mode” is turned on, your currently chosen profile name and image are hidden from your new contacts. It allows anonymous connections with other people without any shared data – when you make new connections or join groups via a link a new random profile name will be generated for each connection.
|
||||
When "Incognito Mode” is turned on, your currently chosen profile name and image are hidden from your new contacts. It allows anonymous connections with other people without any shared data – when you make new connections or join groups via a link a new random profile name will be generated for each connection.
|
||||
|
||||
### How do invitations work?
|
||||
|
||||
@@ -256,6 +257,29 @@ You can resolve it by deleting the app's database: (WARNING: this results in del
|
||||
|
||||
Yes! Please read more about quantum resistant encryption is added to SimpleX Chat and about various properties of end-to-end encryption in [this post](../blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md).
|
||||
|
||||
### Why can't I use the same profile on different devices?
|
||||
|
||||
SimpleX Chat apps support [linking of mobile and desktop apps](https://simplex.chat/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.html#link-mobile-and-desktop-apps-via-secure-quantum-resistant-protocol) via secure quantum-resistant protocol. It allows using the profile on your mobile device from desktop clients.
|
||||
|
||||
Seamlessly and securely using the same profile from two or more devices is a complex and unsolved problem. All apps that provide multi-device support do so at a cost of compromising security of end-to-end encryption. E.g., Session removed the Double Ratchet algorithm entirely to enable multi-device support, sacrificing forward secrecy. Signal provides multi-device support with Double Ratchet algorithm, but by [compromising its "break-in recovery" property](https://eprint.iacr.org/2021/626.pdf) (aka post-compromise security).
|
||||
|
||||
To the best of our knowledge there is no end-to-end encrypted messenger that solved this problem without compromising security, but we believe that the solution is possible. We have considered several approaches:
|
||||
|
||||
1. Convert each direct conversation into a group, where each device participates as a member. This is the approach that Signal and WhatsApp use, and while Signal implementation does not protect from a temporary compromise of long-term identity key (break-in recovery), such protection is possible. The downside of this approach is that the contacts and groups you participate in would know which device you use. Another possible attack is to send different messages to different devices, or to send messages to some devices but not to the others. This could lead to message history inconsistency or enable targeted attacks.
|
||||
|
||||
2. Store the state of the Double Ratchet algorithm for each conversation in an encrypted container on the server, allowing concurrent access and modification by each device for encrypting and decrypting messages. We did not see this approach used in any of the messaging apps, but it is technically viable. This approach has no downsides of the first, but it would increase the time it takes to send and to receive messages, as each message would require additional access to the server.
|
||||
|
||||
3. "Thin client" approach when user profile is stored on the server. The main challenge with this approach is to prevent the server knowing who connects to whom.
|
||||
|
||||
Whichever approach we choose for multi-device support, it requires careful design and implementation, and there is no existing secure solution to copy from. While we value usability very highly, we will not be improving usability in a way that compromises users' security. We will take a slower path of designing and implementing a solution for multi-device that achieves a better trade-off between usability and security than currently offered.
|
||||
|
||||
In the meantime, here are several secure options to enhance usability:
|
||||
- link mobile profiles with desktop app. It does not compromise security in any way.
|
||||
- create small groups with trusted contacts. These contacts would still know which device you use when you send the message, but it won't be shared with all contacts and groups you participate in. This approach is also secure, and it prevents devices being added to the conversation without user noticing.
|
||||
- use "[business address](https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html#business-chats)" - the app would create a new small group with everybody who connects to you via your address, and you will be able to add your other devices to these groups.
|
||||
|
||||
While these approaches are not as convenient as seamless multi-device support offered by other apps, they also do not compromise security to achieve that convenience.
|
||||
|
||||
### What user data can be provided on request?
|
||||
|
||||
Our objective is to consistently ensure that no user data and absolute minimum of the metadata required for the network to function is available for disclosure by any infrastructure operators, under any circumstances.
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 6.4.0.6
|
||||
version: 6.4.0.7
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
||||
@@ -1672,8 +1672,7 @@ processChatCommand vr nm = \case
|
||||
gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId
|
||||
m <- withFastStore $ \db -> getGroupMember db vr user gId mId
|
||||
let GroupInfo {membership = GroupMember {memberRole = membershipRole}} = gInfo
|
||||
-- TODO GRModerator when most users migrate
|
||||
when (membershipRole >= GRAdmin) $ throwChatError $ CECantBlockMemberForSelf gInfo m showMessages
|
||||
when (membershipRole >= GRModerator) $ throwChatError $ CECantBlockMemberForSelf gInfo m showMessages
|
||||
let settings = (memberSettings m) {showMessages}
|
||||
processChatCommand vr nm $ APISetMemberSettings gId mId settings
|
||||
ContactInfo cName -> withContactName cName APIContactInfo
|
||||
@@ -3162,7 +3161,6 @@ processChatCommand vr nm = \case
|
||||
Nothing -> do
|
||||
setGroupLinkData'
|
||||
let recipients = filter memberCurrentOrPending ms
|
||||
liftIO $ putStrLn $ "about to sendGroupMessage to " <> show (length recipients)
|
||||
sendGroupMessage user g' Nothing recipients (XGrpInfo p')
|
||||
where
|
||||
setGroupLinkData' :: CM ()
|
||||
@@ -3200,7 +3198,7 @@ processChatCommand vr nm = \case
|
||||
delGroupChatItemsForMembers :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [GroupMember] -> [CChatItem 'CTGroup] -> CM [ChatItemDeletion]
|
||||
delGroupChatItemsForMembers user gInfo chatScopeInfo ms items = do
|
||||
assertDeletable gInfo items
|
||||
assertUserGroupRole gInfo GRAdmin -- TODO GRModerator when most users migrate
|
||||
assertUserGroupRole gInfo GRModerator
|
||||
let msgMemIds = itemsMsgMemIds gInfo items
|
||||
events = L.nonEmpty $ map (\(msgId, memId) -> XMsgDel msgId (Just memId) $ toMsgScope gInfo <$> chatScopeInfo) msgMemIds
|
||||
mapM_ (sendGroupMessages_ user gInfo ms) events
|
||||
@@ -3414,8 +3412,8 @@ processChatCommand vr nm = \case
|
||||
Nothing -> do
|
||||
(cReq, cData) <- getShortLinkConnReq user l'
|
||||
withFastStore' (\db -> getContactWithoutConnViaShortAddress db vr user l') >>= \case
|
||||
Just ct' -> pure (con cReq, CPContactAddress (CAPContactViaAddress ct'))
|
||||
Nothing -> do
|
||||
Just ct' | not (contactDeleted ct') -> pure (con cReq, CPContactAddress (CAPContactViaAddress ct'))
|
||||
_ -> do
|
||||
contactSLinkData_ <- liftIO $ decodeShortLinkData cData
|
||||
plan <- contactRequestPlan user cReq contactSLinkData_
|
||||
pure (con cReq, plan)
|
||||
@@ -3488,8 +3486,8 @@ processChatCommand vr nm = \case
|
||||
withFastStore' (\db -> getContactConnEntityByConnReqHash db vr user cReqHashes) >>= \case
|
||||
Nothing ->
|
||||
withFastStore' (\db -> getContactWithoutConnViaAddress db vr user cReqSchemas) >>= \case
|
||||
Nothing -> pure $ CPContactAddress (CAPOk contactSLinkData_)
|
||||
Just ct -> pure $ CPContactAddress (CAPContactViaAddress ct)
|
||||
Just ct | not (contactDeleted ct) -> pure $ CPContactAddress (CAPContactViaAddress ct)
|
||||
_ -> pure $ CPContactAddress (CAPOk contactSLinkData_)
|
||||
Just (RcvDirectMsgConnection Connection {connStatus} Nothing)
|
||||
| connStatus == ConnPrepared -> pure $ CPContactAddress (CAPOk contactSLinkData_)
|
||||
| otherwise -> pure $ CPContactAddress CAPConnectingConfirmReconnect
|
||||
|
||||
@@ -2389,7 +2389,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
let gInfo' = gInfo {membership = membership'}
|
||||
cd = CDGroupRcv gInfo' Nothing m
|
||||
createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing
|
||||
createGroupFeatureItems user cd CIRcvGroupFeature gInfo'
|
||||
let prepared = preparedGroup gInfo'
|
||||
unless (isJust prepared) $ createGroupFeatureItems user cd CIRcvGroupFeature gInfo'
|
||||
let welcomeMsgId_ = (\PreparedGroup {welcomeSharedMsgId = mId} -> mId) <$> preparedGroup gInfo'
|
||||
unless (isJust welcomeMsgId_) $ maybeCreateGroupDescrLocal gInfo' m
|
||||
createInternalChatItem user cd (CIRcvGroupEvent RGEUserAccepted) Nothing
|
||||
|
||||
@@ -1729,7 +1729,7 @@ getIntroducedGroupMemberIds db invitee =
|
||||
getForwardIntroducedMembers :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> Bool -> IO [GroupMember]
|
||||
getForwardIntroducedMembers db vr user invitee highlyAvailable = do
|
||||
memberIds <- map fromOnly <$> query
|
||||
filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds
|
||||
rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds
|
||||
where
|
||||
mId = groupMemberId' invitee
|
||||
query
|
||||
@@ -1769,7 +1769,7 @@ getForwardIntroducedModerators db vr user@User {userContactId} invitee = do
|
||||
getForwardInvitedMembers :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> Bool -> IO [GroupMember]
|
||||
getForwardInvitedMembers db vr user forwardMember highlyAvailable = do
|
||||
memberIds <- map fromOnly <$> query
|
||||
filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds
|
||||
rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds
|
||||
where
|
||||
mId = groupMemberId' forwardMember
|
||||
query
|
||||
|
||||
@@ -809,8 +809,11 @@ findDirectChatPreviews_ db User {userId} pagination clq =
|
||||
getDirectChatPreview_ :: DB.Connection -> VersionRangeChat -> User -> ChatPreviewData 'CTDirect -> ExceptT StoreError IO AChat
|
||||
getDirectChatPreview_ db vr user (DirectChatPD _ contactId lastItemId_ stats) = do
|
||||
contact <- getContact db vr user contactId
|
||||
ts <- liftIO getCurrentTime
|
||||
lastItem <- case lastItemId_ of
|
||||
Just lastItemId -> (: []) <$> getDirectChatItem db user contactId lastItemId
|
||||
Just lastItemId -> do
|
||||
previewItem <- liftIO $ safeGetDirectItem db user contact ts lastItemId
|
||||
pure [previewItem]
|
||||
Nothing -> pure []
|
||||
pure $ AChat SCTDirect (Chat (DirectChat contact) lastItem stats)
|
||||
|
||||
@@ -917,8 +920,11 @@ findGroupChatPreviews_ db User {userId} pagination clq =
|
||||
getGroupChatPreview_ :: DB.Connection -> VersionRangeChat -> User -> ChatPreviewData 'CTGroup -> ExceptT StoreError IO AChat
|
||||
getGroupChatPreview_ db vr user (GroupChatPD _ groupId lastItemId_ stats) = do
|
||||
groupInfo <- getGroupInfo db vr user groupId
|
||||
ts <- liftIO getCurrentTime
|
||||
lastItem <- case lastItemId_ of
|
||||
Just lastItemId -> (: []) <$> getGroupCIWithReactions db user groupInfo lastItemId
|
||||
Just lastItemId -> do
|
||||
previewItem <- liftIO $ safeGetGroupItem db user groupInfo ts lastItemId
|
||||
pure [previewItem]
|
||||
Nothing -> pure []
|
||||
pure $ AChat SCTGroup (Chat (GroupChat groupInfo Nothing) lastItem stats)
|
||||
|
||||
@@ -999,8 +1005,11 @@ findLocalChatPreviews_ db User {userId} pagination clq =
|
||||
getLocalChatPreview_ :: DB.Connection -> User -> ChatPreviewData 'CTLocal -> ExceptT StoreError IO AChat
|
||||
getLocalChatPreview_ db user (LocalChatPD _ noteFolderId lastItemId_ stats) = do
|
||||
nf <- getNoteFolder db user noteFolderId
|
||||
ts <- liftIO getCurrentTime
|
||||
lastItem <- case lastItemId_ of
|
||||
Just lastItemId -> (: []) <$> getLocalChatItem db user noteFolderId lastItemId
|
||||
Just lastItemId -> do
|
||||
previewItem <- liftIO $ safeGetLocalItem db user nf ts lastItemId
|
||||
pure [previewItem]
|
||||
Nothing -> pure []
|
||||
pure $ AChat SCTLocal (Chat (LocalChat nf) lastItem stats)
|
||||
|
||||
|
||||
@@ -1782,14 +1782,14 @@ testMultipleUserAddresses =
|
||||
cLinkAlisa <- getContactLink alice True
|
||||
bob ##> ("/c " <> cLinkAlisa)
|
||||
alice <#? bob
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", "Audio/video calls: enabled"), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")])
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", "Audio/video calls: enabled"), ("@Ask SimpleX Team", ""), ("@SimpleX Status", ""), ("*", "")])
|
||||
alice ##> "/ac bob"
|
||||
alice <## "bob (Bob): accepting contact request, you can send messages to contact"
|
||||
concurrently_
|
||||
(bob <## "alisa: contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", lastChatFeature), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")])
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", lastChatFeature), ("@Ask SimpleX Team", ""), ("@SimpleX Status", ""), ("*", "")])
|
||||
alice <##> bob
|
||||
|
||||
bob #> "@alice hey alice"
|
||||
@@ -1820,7 +1820,7 @@ testMultipleUserAddresses =
|
||||
(cath <## "alisa: contact is connected")
|
||||
(alice <## "cath (Catherine): contact is connected")
|
||||
threadDelay 100000
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("@cath", lastChatFeature), ("@bob", "hey"), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")])
|
||||
alice #$> ("/_get chats 2 pcc=on", chats, [("@cath", lastChatFeature), ("@bob", "hey"), ("@Ask SimpleX Team", ""), ("@SimpleX Status", ""), ("*", "")])
|
||||
alice <##> cath
|
||||
|
||||
-- first user doesn't have cath as contact
|
||||
|
||||
+107
-3
@@ -204,10 +204,9 @@ chatGroupTests = do
|
||||
it "should forward messages inside support scope" testScopedSupportForward
|
||||
it "should forward messages inside support scope while member is in review" testScopedSupportForwardWhileReview
|
||||
it "should not forward messages from support to main scope" testScopedSupportDontForward
|
||||
-- TODO test messages are not forwarded between support scopes (1 in review, 1 not? combinations?)
|
||||
it "should forward group wide message (x.grp.info) to all members, including in review" testScopedSupportForwardAll
|
||||
it "should not forward messages between support scopes" testScopedSupportDontForwardBetweenScopes
|
||||
it "should forward file inside support scope" testScopedSupportForwardFile
|
||||
-- TODO test files are forwarded inside support scope while member is in review
|
||||
-- TODO test group events directed to all (e.g. XGrpInfo) are forwarded to support scope member while in review
|
||||
it "should send messages to admins and members" testSupportCLISendCommand
|
||||
it "should correctly maintain unread stats for support chats on reading chat items" testScopedSupportUnreadStatsOnRead
|
||||
it "should correctly maintain unread stats for support chats on deleting chat items" testScopedSupportUnreadStatsOnDelete
|
||||
@@ -7163,6 +7162,111 @@ testScopedSupportDontForward =
|
||||
cath #> "#team (support) 4"
|
||||
[alice, dan] *<# "#team (support: cath) cath> 4"
|
||||
|
||||
testScopedSupportForwardAll :: HasCallStack => TestParams -> IO ()
|
||||
testScopedSupportForwardAll =
|
||||
testChat5 aliceProfile bobProfile cathProfile danProfile eveProfile $
|
||||
\alice bob cath dan eve -> do
|
||||
createGroup4 "team" alice (bob, GRMember) (cath, GRMember) (dan, GROwner)
|
||||
|
||||
alice ##> "/set admission review #team all"
|
||||
alice <## "changed member admission rules"
|
||||
concurrentlyN_
|
||||
[ do
|
||||
bob <## "alice updated group #team:"
|
||||
bob <## "changed member admission rules",
|
||||
do
|
||||
cath <## "alice updated group #team:"
|
||||
cath <## "changed member admission rules",
|
||||
do
|
||||
dan <## "alice updated group #team:"
|
||||
dan <## "changed member admission rules"
|
||||
]
|
||||
|
||||
alice ##> "/create link #team"
|
||||
gLink <- getGroupLink alice "team" GRMember True
|
||||
eve ##> ("/c " <> gLink)
|
||||
eve <## "connection request sent!"
|
||||
alice <## "eve (Eve): accepting request to join group #team..."
|
||||
concurrentlyN_
|
||||
[ alice <## "#team: eve connected and pending review",
|
||||
eve
|
||||
<### [ "#team: alice accepted you to the group, pending review",
|
||||
"#team: joining the group...",
|
||||
"#team: you joined the group, connecting to group moderators for admission to group",
|
||||
"#team: member dan (Daniel) is connected"
|
||||
],
|
||||
do
|
||||
dan <## "#team: alice added eve (Eve) to the group (connecting and pending review...), use /_accept member #1 5 <role> to accept member"
|
||||
dan <## "#team: new member eve is connected and pending review, use /_accept member #1 5 <role> to accept member"
|
||||
]
|
||||
|
||||
setupGroupForwarding alice bob dan
|
||||
setupGroupForwarding alice dan eve
|
||||
|
||||
-- messages are forwarded in main scope between bob and dan
|
||||
bob #> "#team 1"
|
||||
[alice, cath] *<# "#team bob> 1"
|
||||
dan <# "#team bob> 1 [>>]"
|
||||
|
||||
dan #> "#team 2"
|
||||
[alice, cath] *<# "#team dan> 2"
|
||||
bob <# "#team dan> 2 [>>]"
|
||||
|
||||
-- messages are forwarded in support scope between dan and eve
|
||||
eve #> "#team (support) 3"
|
||||
alice <# "#team (support: eve) eve> 3"
|
||||
dan <# "#team (support: eve) eve> 3 [>>]"
|
||||
|
||||
dan #> "#team (support: eve) 4"
|
||||
alice <# "#team (support: eve) dan> 4"
|
||||
eve <# "#team (support) dan> 4 [>>]"
|
||||
|
||||
-- x.grp.info is forwarded from dan to both bob and eve
|
||||
dan ##> "/gp team my_team"
|
||||
dan <## "changed to #my_team"
|
||||
concurrentlyN_
|
||||
[ do
|
||||
alice <## "dan updated group #team:"
|
||||
alice <## "changed to #my_team",
|
||||
do
|
||||
bob <## "dan updated group #team:"
|
||||
bob <## "changed to #my_team",
|
||||
do
|
||||
cath <## "dan updated group #team:"
|
||||
cath <## "changed to #my_team",
|
||||
do
|
||||
eve <## "dan updated group #team:"
|
||||
eve <## "changed to #my_team"
|
||||
]
|
||||
|
||||
testScopedSupportDontForwardBetweenScopes :: HasCallStack => TestParams -> IO ()
|
||||
testScopedSupportDontForwardBetweenScopes =
|
||||
testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do
|
||||
createGroup4 "team" alice (bob, GRMember) (cath, GRMember) (dan, GRModerator)
|
||||
setupGroupForwarding alice bob cath
|
||||
|
||||
-- messages are forwarded in main scope
|
||||
bob #> "#team 1"
|
||||
[alice, dan] *<# "#team bob> 1"
|
||||
cath <# "#team bob> 1 [>>]"
|
||||
|
||||
cath #> "#team 2"
|
||||
[alice, dan] *<# "#team cath> 2"
|
||||
bob <# "#team cath> 2 [>>]"
|
||||
|
||||
-- messages not forwarded between support scopes
|
||||
bob #> "#team (support) 3"
|
||||
alice <# "#team (support: bob) bob> 3"
|
||||
dan <# "#team (support: bob) bob> 3"
|
||||
|
||||
cath #> "#team (support) 4"
|
||||
alice <# "#team (support: cath) cath> 4"
|
||||
dan <# "#team (support: cath) cath> 4"
|
||||
|
||||
bob #> "#team (support) 5"
|
||||
alice <# "#team (support: bob) bob> 5"
|
||||
dan <# "#team (support: bob) bob> 5"
|
||||
|
||||
testScopedSupportForwardFile :: HasCallStack => TestParams -> IO ()
|
||||
testScopedSupportForwardFile =
|
||||
testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> withXFTPServer $ do
|
||||
|
||||
@@ -124,6 +124,7 @@ shortLinkTests largeLinkData = do
|
||||
else it "prepare contact with a long name in profile" testShortLinkInvitationLongName
|
||||
it "prepare contact via invitation and retry connecting" testShortLinkInvitationConnectRetry
|
||||
it "prepare contact using address short link data and connect" testShortLinkAddressPrepareContact
|
||||
it "address connect plan after contact is deleted but conversation kept" testShortLinkAddressDeleteContact
|
||||
it "prepare contact via invitation and connect after it is deleted" testShortLinkDeletedInvitation
|
||||
it "prepare contact via address and connect after it is deleted" testShortLinkDeletedAddress
|
||||
it "prepare contact via address and connect with retry after error" testShortLinkAddressConnectRetry
|
||||
@@ -3138,6 +3139,46 @@ testShortLinkAddressPrepareContact ps@TestParams {largeLinkData} = testChatCfg2
|
||||
bob <## "contact address: ok to connect"
|
||||
void $ getTermLine bob
|
||||
|
||||
testShortLinkAddressDeleteContact :: HasCallStack => TestParams -> IO ()
|
||||
testShortLinkAddressDeleteContact ps@TestParams {largeLinkData} = testChatCfg2 testCfg {largeLinkData} aliceProfile bobProfile test ps
|
||||
where
|
||||
test alice bob = do
|
||||
alice ##> "/ad"
|
||||
(shortLink, fullLink) <- getContactLinks alice True
|
||||
alice ##> "/pa on"
|
||||
alice <## "new contact address set"
|
||||
bob ##> ("/_connect plan 1 " <> shortLink)
|
||||
bob <## "contact address: ok to connect"
|
||||
contactSLinkData <- getTermLine bob
|
||||
bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData)
|
||||
bob <## "alice: contact is prepared"
|
||||
bob ##> "/_connect contact @2 text hello"
|
||||
bob
|
||||
<### [ "alice: connection started",
|
||||
WithTime "@alice hello"
|
||||
]
|
||||
alice
|
||||
<### [ "bob (Bob) wants to connect to you!",
|
||||
WithTime "bob> hello"
|
||||
]
|
||||
alice <## "to accept: /ac bob"
|
||||
alice <## "to reject: /rc bob (the sender will NOT be notified)"
|
||||
alice ##> "/ac bob"
|
||||
alice <## "bob (Bob): accepting contact request, you can send messages to contact"
|
||||
unless largeLinkData $
|
||||
bob <## "contact alice updated bio: Alice"
|
||||
concurrently_
|
||||
(bob <## "alice (Alice): contact is connected")
|
||||
(alice <## "bob (Bob): contact is connected")
|
||||
alice <##> bob
|
||||
threadDelay 250000
|
||||
bob ##> "/d alice entity"
|
||||
bob <## "alice: contact is deleted"
|
||||
alice <## "bob (Bob) deleted contact with you"
|
||||
bob ##> ("/_connect plan 1 " <> shortLink)
|
||||
bob <## "contact address: ok to connect"
|
||||
void $ getTermLine bob
|
||||
|
||||
testShortLinkDeletedInvitation :: HasCallStack => TestParams -> IO ()
|
||||
testShortLinkDeletedInvitation ps@TestParams {largeLinkData} = testChatCfg2 testCfg {largeLinkData} aliceProfile bobProfile test ps
|
||||
where
|
||||
@@ -3682,8 +3723,8 @@ testShortLinkChangePreparedContactUser ps@TestParams {largeLinkData} = testChatC
|
||||
|
||||
alice @@@ [("@robert", "hey")]
|
||||
alice `hasContactProfiles` ["alice", "robert"]
|
||||
bob #$> ("/_get chats 2 pcc=on", chats, [("@alice", "hey"), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")])
|
||||
bob `hasContactProfiles` ["robert", "alice", "SimpleX Chat team", "SimpleX-Status"]
|
||||
bob #$> ("/_get chats 2 pcc=on", chats, [("@alice", "hey"), ("@Ask SimpleX Team", ""), ("@SimpleX Status", ""), ("*", "")])
|
||||
bob `hasContactProfiles` ["robert", "alice", "Ask SimpleX Team", "SimpleX Status"]
|
||||
bob ##> "/user bob"
|
||||
showActiveUser bob "bob (Bob)"
|
||||
bob @@@ []
|
||||
@@ -3741,8 +3782,8 @@ testShortLinkChangePreparedContactUserDuplicate ps@TestParams {largeLinkData} =
|
||||
|
||||
alice @@@ [("@robert", "hey"), ("@robert_1", "hey")]
|
||||
alice `hasContactProfiles` ["alice", "robert", "robert"]
|
||||
bob #$> ("/_get chats 2 pcc=on", chats, [("@alice", "hey"), ("@alice_1", "hey"), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")])
|
||||
bob `hasContactProfiles` ["robert", "alice", "alice", "SimpleX Chat team", "SimpleX-Status"]
|
||||
bob #$> ("/_get chats 2 pcc=on", chats, [("@alice", "hey"), ("@alice_1", "hey"), ("@Ask SimpleX Team", ""), ("@SimpleX Status", ""), ("*", "")])
|
||||
bob `hasContactProfiles` ["robert", "alice", "alice", "Ask SimpleX Team", "SimpleX Status"]
|
||||
bob ##> "/user bob"
|
||||
showActiveUser bob "bob (Bob)"
|
||||
bob @@@ []
|
||||
@@ -3835,8 +3876,8 @@ testShortLinkChangePreparedGroupUser ps@TestParams {largeLinkData} = testChatCfg
|
||||
|
||||
alice @@@ [("#team", "3"), ("@cath","sent invitation to join group team as admin")]
|
||||
alice `hasContactProfiles` ["alice", "cath", "robert"]
|
||||
bob #$> ("/_get chats 2 pcc=on", chats, [("#team", "3"), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")])
|
||||
bob `hasContactProfiles` ["robert", "alice", "cath", "SimpleX Chat team", "SimpleX-Status"]
|
||||
bob #$> ("/_get chats 2 pcc=on", chats, [("#team", "3"), ("@Ask SimpleX Team", ""), ("@SimpleX Status", ""), ("*", "")])
|
||||
bob `hasContactProfiles` ["robert", "alice", "cath", "Ask SimpleX Team", "SimpleX Status"]
|
||||
cath @@@ [("#team", "3"), ("@alice","received invitation to join group team as admin")]
|
||||
cath `hasContactProfiles` ["cath", "alice", "robert"]
|
||||
bob ##> "/user bob"
|
||||
@@ -3949,7 +3990,7 @@ testShortLinkChangePreparedGroupUserDuplicate ps@TestParams {largeLinkData} = te
|
||||
|
||||
alice @@@ [("#team", "7"), ("@cath","sent invitation to join group team as admin")]
|
||||
alice `hasContactProfiles` ["alice", "cath", "robert", "robert"]
|
||||
bob `hasContactProfiles` ["robert", "robert", "robert", "alice", "alice", "cath", "cath", "SimpleX Chat team", "SimpleX-Status"]
|
||||
bob `hasContactProfiles` ["robert", "robert", "robert", "alice", "alice", "cath", "cath", "Ask SimpleX Team", "SimpleX Status"]
|
||||
cath @@@ [("#team", "7"), ("@alice","received invitation to join group team as admin")]
|
||||
cath `hasContactProfiles` ["cath", "alice", "robert", "robert"]
|
||||
bob ##> "/user bob"
|
||||
|
||||
Reference in New Issue
Block a user