mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-02 15:41:44 +00:00
Merge branch 'master' into f/request-roster
This commit is contained in:
@@ -14,6 +14,29 @@ import Combine
|
||||
|
||||
private let memberImageSize: CGFloat = 34
|
||||
|
||||
private func shouldShowAvatar(_ current: ChatItem, _ older: ChatItem?) -> Bool {
|
||||
let oldIsGroupRcv = switch older?.chatDir {
|
||||
case .groupRcv: true
|
||||
case .channelRcv: true
|
||||
default: false
|
||||
}
|
||||
let sameMember = switch (older?.chatDir, current.chatDir) {
|
||||
case (.groupRcv(let oldMember), .groupRcv(let member)):
|
||||
oldMember.memberId == member.memberId
|
||||
case (.channelRcv, .channelRcv):
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
if case .groupRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) {
|
||||
return true
|
||||
} else if case .channelRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Spec: spec/client/chat-view.md#ChatView
|
||||
struct ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@@ -896,8 +919,14 @@ struct ChatView: View {
|
||||
} else {
|
||||
let voiceNoFrame = voiceWithoutFrame(ci)
|
||||
let channelReceived = !ci.chatDir.sent && cInfo.isChannel
|
||||
// consecutive (no-avatar) received messages in channels drop the avatar-sized
|
||||
// left padding (see .leading padding below), so they get the full row width here
|
||||
// too — otherwise the reserved avatar inset would leave a gap on the right
|
||||
let channelReceivedNoAvatar = channelReceived && !shouldShowAvatar(mergedItem.newest().item, mergedItem.oldest().nextItem)
|
||||
let maxWidth = cInfo.chatType == .group
|
||||
? voiceNoFrame || channelReceived
|
||||
? channelReceivedNoAvatar
|
||||
? g.size.width - 26
|
||||
: voiceNoFrame || channelReceived
|
||||
? (g.size.width - 28) - 42
|
||||
: (g.size.width - 28) * 0.84 - 42
|
||||
: voiceNoFrame
|
||||
@@ -1733,29 +1762,6 @@ struct ChatView: View {
|
||||
)
|
||||
}
|
||||
|
||||
func shouldShowAvatar(_ current: ChatItem, _ older: ChatItem?) -> Bool {
|
||||
let oldIsGroupRcv = switch older?.chatDir {
|
||||
case .groupRcv: true
|
||||
case .channelRcv: true
|
||||
default: false
|
||||
}
|
||||
let sameMember = switch (older?.chatDir, current.chatDir) {
|
||||
case (.groupRcv(let oldMember), .groupRcv(let member)):
|
||||
oldMember.memberId == member.memberId
|
||||
case (.channelRcv, .channelRcv):
|
||||
true
|
||||
default:
|
||||
false
|
||||
}
|
||||
if case .groupRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) {
|
||||
return true
|
||||
} else if case .channelRcv = current.chatDir, (older == nil || (!oldIsGroupRcv || !sameMember)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let last = isLastItem ? im.reversedChatItems.last : nil
|
||||
let listItem = merged.newest()
|
||||
@@ -1979,7 +1985,7 @@ struct ChatView: View {
|
||||
}
|
||||
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
|
||||
.padding(.trailing)
|
||||
.padding(.leading, 10 + memberImageSize + 12)
|
||||
.padding(.leading, chat.chatInfo.isChannel ? nil : 10 + memberImageSize + 12)
|
||||
}
|
||||
.padding(.bottom, bottomPadding)
|
||||
}
|
||||
@@ -2076,7 +2082,7 @@ struct ChatView: View {
|
||||
}
|
||||
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
|
||||
.padding(.trailing)
|
||||
.padding(.leading, 10 + memberImageSize + 12)
|
||||
.padding(.leading, chat.chatInfo.isChannel ? nil : 10 + memberImageSize + 12)
|
||||
}
|
||||
.padding(.bottom, bottomPadding)
|
||||
}
|
||||
|
||||
@@ -392,38 +392,31 @@ struct ComposeView: View {
|
||||
}
|
||||
|
||||
let ownerState = ownerRelayState
|
||||
let subscriberState = subscriberRelayState
|
||||
if let gInfo = chat.chatInfo.groupInfo, gInfo.useRelays,
|
||||
![.memRejected, .memLeft, .memRemoved, .memGroupDeleted].contains(gInfo.membership.memberStatus) {
|
||||
if gInfo.membership.memberRole == .owner {
|
||||
if let s = ownerState, s.relays.isEmpty || s.activeCount < s.relays.count {
|
||||
ownerChannelRelayBar(relays: s.relays, activeCount: s.activeCount, failedCount: s.failedCount, removedCount: s.removedCount)
|
||||
}
|
||||
} else {
|
||||
let hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?? []).sorted()
|
||||
let relayMembers = chatModel.groupMembers
|
||||
.filter { $0.wrapped.memberRole == .relay && ![.memRemoved, .memGroupDeleted].contains($0.wrapped.memberStatus) }
|
||||
.sorted { hostFromRelayLink($0.wrapped.relayLink ?? "") < hostFromRelayLink($1.wrapped.relayLink ?? "") }
|
||||
} else if let s = subscriberState {
|
||||
let showProgress = !gInfo.nextConnectPrepared || composeState.inProgress
|
||||
let removedCount = relayMembers.filter { relayMemberRemoved($0.wrapped.memberStatus) }.count
|
||||
let connectedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connStatus == .ready && $0.wrapped.activeConn?.connFailedErr == nil }.count
|
||||
let failedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connFailedErr != nil }.count
|
||||
let resolvedCount = connectedCount + removedCount + failedCount
|
||||
let total = relayMembers.count > 0 ? relayMembers.count : hostnames.count
|
||||
if total == 0 || removedCount + failedCount > 0 || resolvedCount < total {
|
||||
let resolvedCount = s.connectedCount + s.removedCount + s.failedCount
|
||||
if s.total == 0 || s.removedCount + s.failedCount > 0 || resolvedCount < s.total {
|
||||
subscriberChannelRelayBar(
|
||||
hostnames: hostnames,
|
||||
relayMembers: relayMembers,
|
||||
connectedCount: connectedCount,
|
||||
removedCount: removedCount,
|
||||
failedCount: failedCount,
|
||||
total: total,
|
||||
hostnames: s.hostnames,
|
||||
relayMembers: s.relayMembers,
|
||||
connectedCount: s.connectedCount,
|
||||
removedCount: s.removedCount,
|
||||
failedCount: s.failedCount,
|
||||
total: s.total,
|
||||
showProgress: showProgress
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let userCantSendReason = chat.chatInfo.userCantSendReason(allRelaysBroken: ownerState?.noActiveRelays ?? false)
|
||||
let userCantSendReason = chat.chatInfo.userCantSendReason(allRelaysBroken: (ownerState?.noActiveRelays ?? subscriberState?.noActiveRelays) ?? false)
|
||||
let composeEnabled = (
|
||||
userCantSendReason == nil ||
|
||||
(chat.chatInfo.groupInfo?.nextConnectPrepared ?? false) ||
|
||||
@@ -748,8 +741,25 @@ struct ComposeView: View {
|
||||
return (relays, activeCount, failedCount, removedCount, noActiveRelays)
|
||||
}
|
||||
|
||||
private var subscriberRelayState: (hostnames: [String], relayMembers: [GMember], connectedCount: Int, removedCount: Int, failedCount: Int, total: Int, noActiveRelays: Bool)? {
|
||||
guard let gInfo = chat.chatInfo.groupInfo, gInfo.useRelays,
|
||||
gInfo.membership.memberRole != .owner,
|
||||
![.memRejected, .memLeft, .memRemoved, .memGroupDeleted].contains(gInfo.membership.memberStatus)
|
||||
else { return nil }
|
||||
let hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?? []).sorted()
|
||||
let relayMembers = chatModel.groupMembers
|
||||
.filter { $0.wrapped.memberRole == .relay && ![.memRemoved, .memGroupDeleted].contains($0.wrapped.memberStatus) }
|
||||
.sorted { hostFromRelayLink($0.wrapped.relayLink ?? "") < hostFromRelayLink($1.wrapped.relayLink ?? "") }
|
||||
let removedCount = relayMembers.filter { relayMemberRemoved($0.wrapped.memberStatus) }.count
|
||||
let connectedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connStatus == .ready && $0.wrapped.activeConn?.connFailedErr == nil }.count
|
||||
let failedCount = relayMembers.filter { !relayMemberRemoved($0.wrapped.memberStatus) && $0.wrapped.activeConn?.connFailedErr != nil }.count
|
||||
let total = relayMembers.count > 0 ? relayMembers.count : hostnames.count
|
||||
let noActiveRelays = connectedCount == 0 && (removedCount + failedCount) == total
|
||||
return (hostnames, relayMembers, connectedCount, removedCount, failedCount, total, noActiveRelays)
|
||||
}
|
||||
|
||||
private var disabledText: LocalizedStringKey? {
|
||||
chat.chatInfo.userCantSendReason(allRelaysBroken: ownerRelayState?.noActiveRelays ?? false)?.composeLabel
|
||||
chat.chatInfo.userCantSendReason(allRelaysBroken: (ownerRelayState?.noActiveRelays ?? subscriberRelayState?.noActiveRelays) ?? false)?.composeLabel
|
||||
}
|
||||
|
||||
@ViewBuilder private func ownerChannelRelayBar(relays: [GroupRelay], activeCount: Int, failedCount: Int, removedCount: Int) -> some View {
|
||||
|
||||
@@ -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-7.0.0.5-3gtbZllPEb75FgChAFCqhx-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a */; };
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.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-7.0.0.5-3gtbZllPEb75FgChAFCqhx-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a"; sourceTree = "<group>"; };
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.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>"; };
|
||||
@@ -735,8 +735,8 @@
|
||||
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
|
||||
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
|
||||
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -822,8 +822,8 @@
|
||||
64C829992D54AEEE006B9E89 /* libffi.a */,
|
||||
64C829982D54AEED006B9E89 /* libgmp.a */,
|
||||
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -2081,7 +2081,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 338;
|
||||
CURRENT_PROJECT_VERSION = 339;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2131,7 +2131,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 338;
|
||||
CURRENT_PROJECT_VERSION = 339;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2173,7 +2173,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 338;
|
||||
CURRENT_PROJECT_VERSION = 339;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
@@ -2193,7 +2193,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 338;
|
||||
CURRENT_PROJECT_VERSION = 339;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
@@ -2218,7 +2218,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 338;
|
||||
CURRENT_PROJECT_VERSION = 339;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -2255,7 +2255,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 338;
|
||||
CURRENT_PROJECT_VERSION = 339;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -2292,7 +2292,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 338;
|
||||
CURRENT_PROJECT_VERSION = 339;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2343,7 +2343,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 338;
|
||||
CURRENT_PROJECT_VERSION = 339;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2397,7 +2397,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 338;
|
||||
CURRENT_PROJECT_VERSION = 339;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2431,7 +2431,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 338;
|
||||
CURRENT_PROJECT_VERSION = 339;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
|
||||
@@ -1720,11 +1720,11 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
if groupInfo.membership.memberActive {
|
||||
switch(groupChatScope) {
|
||||
case .none:
|
||||
if allRelaysBroken && groupInfo.useRelays { return ("can't broadcast", nil) }
|
||||
if groupInfo.membership.memberPending { return ("reviewed by admins", "Please contact group admin.") }
|
||||
if groupInfo.membership.memberRole == .observer {
|
||||
return groupInfo.useRelays ? ("you are subscriber", nil) : ("you are observer", "Please contact group admin.")
|
||||
}
|
||||
if allRelaysBroken && groupInfo.useRelays { return ("can't broadcast", nil) }
|
||||
return nil
|
||||
case let .some(.memberSupport(groupMember_: .some(supportMember))):
|
||||
if supportMember.versionRange.maxVersion < GROUP_KNOCKING_VERSION && !supportMember.memberPending {
|
||||
|
||||
+3
-3
@@ -1660,9 +1660,6 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
if (groupInfo.membership.memberActive) {
|
||||
when (groupChatScope) {
|
||||
null -> {
|
||||
if (allRelaysBroken && groupInfo.useRelays) {
|
||||
return generalGetString(MR.strings.cant_broadcast_message) to null
|
||||
}
|
||||
if (groupInfo.membership.memberPending) {
|
||||
return generalGetString(MR.strings.reviewed_by_admins) to generalGetString(MR.strings.observer_cant_send_message_desc)
|
||||
}
|
||||
@@ -1673,6 +1670,9 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
generalGetString(MR.strings.observer_cant_send_message_title) to generalGetString(MR.strings.observer_cant_send_message_desc)
|
||||
}
|
||||
}
|
||||
if (allRelaysBroken && groupInfo.useRelays) {
|
||||
return generalGetString(MR.strings.cant_broadcast_message) to null
|
||||
}
|
||||
return null
|
||||
}
|
||||
is GroupChatScopeInfo.MemberSupport ->
|
||||
|
||||
+6
-6
@@ -1607,7 +1607,7 @@ object ChatController {
|
||||
suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData): Chat? {
|
||||
val userId = try { currentUserId("apiPrepareContact") } catch (e: Exception) { return null }
|
||||
val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData))
|
||||
if (r is API.Result && r.res is CR.NewPreparedChat) return r.res.chat
|
||||
if (r is API.Result && r.res is CR.NewPreparedChat) return if (rh == null) r.res.chat else r.res.chat.copy(remoteHostId = rh)
|
||||
Log.e(TAG, "apiPrepareContact bad response: ${r.responseType} ${r.details}")
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_contact), "${r.responseType}: ${r.details}")
|
||||
return null
|
||||
@@ -1616,7 +1616,7 @@ object ChatController {
|
||||
suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, directLink: Boolean, groupShortLinkData: GroupShortLinkData): Chat? {
|
||||
val userId = try { currentUserId("apiPrepareGroup") } catch (e: Exception) { return null }
|
||||
val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, directLink, groupShortLinkData))
|
||||
if (r is API.Result && r.res is CR.NewPreparedChat) return r.res.chat
|
||||
if (r is API.Result && r.res is CR.NewPreparedChat) return if (rh == null) r.res.chat else r.res.chat.copy(remoteHostId = rh)
|
||||
Log.e(TAG, "apiPrepareGroup bad response: ${r.responseType} ${r.details}")
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_group), "${r.responseType}: ${r.details}")
|
||||
return null
|
||||
@@ -2172,8 +2172,8 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiGetGroupRelays(groupId: Long): List<GroupRelay> {
|
||||
val r = sendCmd(null, CC.ApiGetGroupRelays(groupId))
|
||||
suspend fun apiGetGroupRelays(rh: Long?, groupId: Long): List<GroupRelay> {
|
||||
val r = sendCmd(rh, CC.ApiGetGroupRelays(groupId))
|
||||
if (r is API.Result && r.res is CR.GroupRelays) return r.res.groupRelays
|
||||
return emptyList()
|
||||
}
|
||||
@@ -2183,8 +2183,8 @@ object ChatController {
|
||||
data class AddFailed(val addRelayResults: List<AddRelayResult>): AddGroupRelaysResult()
|
||||
}
|
||||
|
||||
suspend fun apiAddGroupRelays(groupId: Long, relayIds: List<Long>): AddGroupRelaysResult? {
|
||||
val r = sendCmdWithRetry(null, CC.ApiAddGroupRelays(groupId, relayIds))
|
||||
suspend fun apiAddGroupRelays(rh: Long?, groupId: Long, relayIds: List<Long>): AddGroupRelaysResult? {
|
||||
val r = sendCmdWithRetry(rh, CC.ApiAddGroupRelays(groupId, relayIds))
|
||||
if (r is API.Result && r.res is CR.GroupRelaysAdded) return AddGroupRelaysResult.Added(r.res.groupInfo, r.res.groupLink, r.res.groupRelays)
|
||||
if (r is API.Result && r.res is CR.GroupRelaysAddFailed) return AddGroupRelaysResult.AddFailed(r.res.addRelayResults)
|
||||
if (r != null) throw Exception("${r.responseType}: ${r.details}")
|
||||
|
||||
+3
-3
@@ -208,7 +208,7 @@ fun ChatView(
|
||||
withBGApi {
|
||||
setGroupMembers(chatRh, cInfo.groupInfo, chatModel)
|
||||
if (cInfo.groupInfo.membership.memberRole == GroupMemberRole.Owner) {
|
||||
val relays = chatModel.controller.apiGetGroupRelays(cInfo.groupInfo.groupId)
|
||||
val relays = chatModel.controller.apiGetGroupRelays(chatRh, cInfo.groupInfo.groupId)
|
||||
withContext(Dispatchers.Main) {
|
||||
ChannelRelaysModel.set(cInfo.groupInfo.groupId, relays)
|
||||
}
|
||||
@@ -2084,7 +2084,7 @@ fun BoxScope.ChatItemsList(
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||
.padding(start = if (chatInfo.isChannel) 12.dp else 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
||||
.then(swipeableOrSelectionModifier)
|
||||
) {
|
||||
@@ -2167,7 +2167,7 @@ fun BoxScope.ChatItemsList(
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||
.padding(start = if (chatInfo.isChannel) 12.dp else 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack || chatInfo.isChannel) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
|
||||
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
|
||||
.then(swipeableOrSelectionModifier)
|
||||
) {
|
||||
|
||||
+35
-13
@@ -1203,8 +1203,9 @@ fun ComposeView(
|
||||
}
|
||||
|
||||
val ownerRelayState = ownerRelayState(chat, chatModel)
|
||||
val subscriberRelayState = subscriberRelayState(chat, chatModel)
|
||||
|
||||
val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason(ownerRelayState?.noActiveRelays == true))
|
||||
val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason((ownerRelayState?.noActiveRelays ?: subscriberRelayState?.noActiveRelays) == true))
|
||||
val sendMsgEnabled = rememberUpdatedState(userCantSendReason.value == null)
|
||||
val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv)
|
||||
|
||||
@@ -1575,18 +1576,12 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?: emptyList()).sorted()
|
||||
val relayMembers = chatModel.groupMembers.value
|
||||
.filter { it.memberRole == GroupMemberRole.Relay && it.memberStatus !in listOf(GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) }
|
||||
.sortedBy { hostFromRelayLink(it.relayLink ?: "") }
|
||||
val showProgress = !gInfo.nextConnectPrepared || composeState.value.inProgress
|
||||
val removedCount = relayMembers.count { relayMemberRemoved(it.memberStatus) }
|
||||
val connectedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connStatus == ConnStatus.Ready && it.activeConn?.connFailedErr == null }
|
||||
val failedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connFailedErr != null }
|
||||
val resolvedCount = connectedCount + removedCount + failedCount
|
||||
val total = if (relayMembers.isNotEmpty()) relayMembers.size else hostnames.size
|
||||
if (total == 0 || removedCount + failedCount > 0 || resolvedCount < total) {
|
||||
SubscriberChannelRelayBar(hostnames, relayMembers, connectedCount, removedCount, failedCount, total, showProgress, relayListExpanded)
|
||||
subscriberRelayState?.let { s ->
|
||||
val showProgress = !gInfo.nextConnectPrepared || composeState.value.inProgress
|
||||
val resolvedCount = s.connectedCount + s.removedCount + s.failedCount
|
||||
if (s.total == 0 || s.removedCount + s.failedCount > 0 || resolvedCount < s.total) {
|
||||
SubscriberChannelRelayBar(s.hostnames, s.relayMembers, s.connectedCount, s.removedCount, s.failedCount, s.total, showProgress, relayListExpanded)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2052,6 +2047,33 @@ private data class OwnerRelayState(
|
||||
val noActiveRelays: Boolean
|
||||
)
|
||||
|
||||
private fun subscriberRelayState(chat: Chat, chatModel: ChatModel): SubscriberRelayState? {
|
||||
val gInfo = (chat.chatInfo as? ChatInfo.Group)?.groupInfo ?: return null
|
||||
if (!gInfo.useRelays || gInfo.membership.memberRole == GroupMemberRole.Owner ||
|
||||
gInfo.membership.memberStatus in listOf(GroupMemberStatus.MemRejected, GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted)
|
||||
) return null
|
||||
val hostnames = (chatModel.channelRelayHostnames[gInfo.groupId] ?: emptyList()).sorted()
|
||||
val relayMembers = chatModel.groupMembers.value
|
||||
.filter { it.memberRole == GroupMemberRole.Relay && it.memberStatus !in listOf(GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted) }
|
||||
.sortedBy { hostFromRelayLink(it.relayLink ?: "") }
|
||||
val removedCount = relayMembers.count { relayMemberRemoved(it.memberStatus) }
|
||||
val connectedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connStatus == ConnStatus.Ready && it.activeConn?.connFailedErr == null }
|
||||
val failedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connFailedErr != null }
|
||||
val total = if (relayMembers.isNotEmpty()) relayMembers.size else hostnames.size
|
||||
val noActiveRelays = connectedCount == 0 && (removedCount + failedCount) == total
|
||||
return SubscriberRelayState(hostnames, relayMembers, connectedCount, removedCount, failedCount, total, noActiveRelays)
|
||||
}
|
||||
|
||||
private data class SubscriberRelayState(
|
||||
val hostnames: List<String>,
|
||||
val relayMembers: List<GroupMember>,
|
||||
val connectedCount: Int,
|
||||
val removedCount: Int,
|
||||
val failedCount: Int,
|
||||
val total: Int,
|
||||
val noActiveRelays: Boolean
|
||||
)
|
||||
|
||||
private fun relayMemberRemoved(status: GroupMemberStatus?): Boolean =
|
||||
status in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted)
|
||||
|
||||
|
||||
+5
-3
@@ -31,6 +31,7 @@ data class AvailableRelay(
|
||||
|
||||
@Composable
|
||||
fun AddGroupRelayView(
|
||||
rhId: Long?,
|
||||
groupInfo: GroupInfo,
|
||||
existingRelayIds: Set<Long>,
|
||||
onRelayAdded: () -> Unit,
|
||||
@@ -46,7 +47,7 @@ fun AddGroupRelayView(
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
val servers = ChatController.getUserServers(null)
|
||||
val servers = ChatController.getUserServers(rhId)
|
||||
if (servers != null) {
|
||||
val relays = mutableListOf<AvailableRelay>()
|
||||
for (op in servers) {
|
||||
@@ -80,7 +81,7 @@ fun AddGroupRelayView(
|
||||
if (relayIds.isEmpty()) return@AddGroupRelayLayout
|
||||
isAdding = true
|
||||
scope.launch {
|
||||
addSelectedRelays(groupInfo, relayIds, selectedRelayIds, availableRelays, onRelayAdded, close) { newSelectedIds, newAvailableRelays ->
|
||||
addSelectedRelays(rhId, groupInfo, relayIds, selectedRelayIds, availableRelays, onRelayAdded, close) { newSelectedIds, newAvailableRelays ->
|
||||
selectedRelayIds = newSelectedIds
|
||||
availableRelays = newAvailableRelays
|
||||
isAdding = false
|
||||
@@ -183,6 +184,7 @@ private fun AddRelaysButton(onClick: () -> Unit, disabled: Boolean) {
|
||||
}
|
||||
|
||||
private suspend fun addSelectedRelays(
|
||||
rhId: Long?,
|
||||
groupInfo: GroupInfo,
|
||||
relayIds: List<Long>,
|
||||
selectedRelayIds: Set<Long>,
|
||||
@@ -192,7 +194,7 @@ private suspend fun addSelectedRelays(
|
||||
updateState: (Set<Long>, List<AvailableRelay>) -> Unit
|
||||
) {
|
||||
try {
|
||||
val result = ChatController.apiAddGroupRelays(groupInfo.groupId, relayIds)
|
||||
val result = ChatController.apiAddGroupRelays(rhId, groupInfo.groupId, relayIds)
|
||||
if (result == null) {
|
||||
updateState(selectedRelayIds, availableRelays)
|
||||
return
|
||||
|
||||
+2
-1
@@ -37,7 +37,7 @@ fun ChannelRelaysView(
|
||||
LaunchedEffect(Unit) {
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
if (groupInfo.isOwner) {
|
||||
val relays = chatModel.controller.apiGetGroupRelays(groupInfo.groupId)
|
||||
val relays = chatModel.controller.apiGetGroupRelays(rhId, groupInfo.groupId)
|
||||
ChannelRelaysModel.set(groupId = groupInfo.groupId, groupRelays = relays)
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,7 @@ private fun ChannelRelaysLayout(
|
||||
val existingRelayIds = groupRelays.mapNotNull { it.userChatRelay.chatRelayId }.toSet()
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close ->
|
||||
AddGroupRelayView(
|
||||
rhId = rhId,
|
||||
groupInfo = groupInfo,
|
||||
existingRelayIds = existingRelayIds,
|
||||
onRelayAdded = { withBGApi { setGroupMembers(rhId, groupInfo, chatModel) } },
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ fun ChannelWebPageView(
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val relays = chatModel.controller.apiGetGroupRelays(groupInfo.groupId)
|
||||
val relays = chatModel.controller.apiGetGroupRelays(rhId, groupInfo.groupId)
|
||||
groupRelays.clear()
|
||||
groupRelays.addAll(relays)
|
||||
}
|
||||
|
||||
+18
-7
@@ -15,7 +15,9 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.*
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -30,7 +32,10 @@ import java.net.URI
|
||||
@Composable
|
||||
fun CIFileView(
|
||||
file: CIFile?,
|
||||
edited: Boolean,
|
||||
meta: CIMeta,
|
||||
chatTTL: Int?,
|
||||
showViaProxy: Boolean,
|
||||
showTimestamp: Boolean,
|
||||
showMenu: MutableState<Boolean>,
|
||||
smallView: Boolean = false,
|
||||
senderProfile: LocalProfile?,
|
||||
@@ -202,10 +207,13 @@ fun CIFileView(
|
||||
) {
|
||||
fileIndicator()
|
||||
if (!smallView) {
|
||||
val metaReserve = if (edited)
|
||||
" "
|
||||
else
|
||||
" "
|
||||
val secondaryColor = MaterialTheme.colors.secondary
|
||||
val encrypted = if (file?.fileSource == null) null else file.fileSource.cryptoArgs != null
|
||||
val metaReserve = buildAnnotatedString {
|
||||
withStyle(reserveTimestampStyle) {
|
||||
append(reserveSpaceForMeta(meta, chatTTL, encrypted, secondaryColor = secondaryColor, showViaProxy = showViaProxy, showTimestamp = showTimestamp))
|
||||
}
|
||||
}
|
||||
if (file != null) {
|
||||
Column {
|
||||
Text(
|
||||
@@ -213,8 +221,11 @@ fun CIFileView(
|
||||
maxLines = 1
|
||||
)
|
||||
Text(
|
||||
formatBytes(file.fileSize) + metaReserve,
|
||||
color = MaterialTheme.colors.secondary,
|
||||
buildAnnotatedString {
|
||||
append(formatBytes(file.fileSize))
|
||||
append(metaReserve)
|
||||
},
|
||||
color = secondaryColor,
|
||||
fontSize = 14.sp,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ fun FramedItemView(
|
||||
|
||||
@Composable
|
||||
fun ciFileView(ci: ChatItem, text: String) {
|
||||
CIFileView(ci.file, ci.meta.itemEdited, showMenu, false, ciSenderProfile(ci, chatInfo), receiveFile)
|
||||
CIFileView(ci.file, ci.meta, chatTTL, showViaProxy, showTimestamp, showMenu, false, ciSenderProfile(ci, chatInfo), receiveFile)
|
||||
if (text != "" || ci.meta.isLive) {
|
||||
CIMarkdownText(chatsCtx, ci, chat, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
|
||||
+1
-1
@@ -342,7 +342,7 @@ fun ChatPreviewView(
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile -> SmallContentPreviewFile {
|
||||
CIFileView(ci.file, false, remember { mutableStateOf(false) }, smallView = true, senderProfile = ciSenderProfile(ci, chat.chatInfo)) {
|
||||
CIFileView(ci.file, ci.meta, cInfo.timedMessagesTTL, showViaProxy = false, showTimestamp = true, showMenu = remember { mutableStateOf(false) }, smallView = true, senderProfile = ciSenderProfile(ci, chat.chatInfo)) {
|
||||
val user = chatModel.currentUser.value ?: return@CIFileView
|
||||
withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) }
|
||||
}
|
||||
|
||||
+7
-3
@@ -1,4 +1,5 @@
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
@@ -27,7 +28,7 @@ import chat.simplex.common.views.onboarding.SelectableCard
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItemWithContent
|
||||
import chat.simplex.res.MR
|
||||
|
||||
private val SectionCardShape = RoundedCornerShape(16.dp)
|
||||
val SectionCardShape = RoundedCornerShape(16.dp)
|
||||
val CARD_PADDING = 18.dp
|
||||
val ICON_TEXT_SPACING = 8.dp
|
||||
|
||||
@@ -113,15 +114,18 @@ fun SectionView(
|
||||
iconTint: Color = MaterialTheme.colors.secondary,
|
||||
leadingIcon: Boolean = false,
|
||||
padding: PaddingValues = PaddingValues(),
|
||||
onIconClick: (() -> Unit)? = null,
|
||||
content: (@Composable ColumnScope.() -> Unit)
|
||||
) {
|
||||
val card = LocalCardScreen.current
|
||||
Column {
|
||||
val iconSize = with(LocalDensity.current) { 21.sp.toDp() }
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val iconClickable = if (onIconClick != null) Modifier.clickable(interactionSource = interactionSource, indication = ripple(bounded = false, radius = iconSize * 0.75f), onClick = onIconClick) else Modifier
|
||||
Row(Modifier.padding(start = if (card) DEFAULT_PADDING + DEFAULT_PADDING_HALF else DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (leadingIcon) Icon(icon, null, Modifier.padding(end = DEFAULT_PADDING_HALF).size(iconSize), tint = iconTint)
|
||||
if (leadingIcon) Icon(icon, null, Modifier.padding(end = DEFAULT_PADDING_HALF).size(iconSize).then(iconClickable), tint = iconTint)
|
||||
Text(title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = if (card) 14.sp else 12.sp, fontWeight = if (card) FontWeight.Medium else FontWeight.Normal)
|
||||
if (!leadingIcon) Icon(icon, null, Modifier.padding(start = DEFAULT_PADDING_HALF).size(iconSize), tint = iconTint)
|
||||
if (!leadingIcon) Icon(icon, null, Modifier.padding(start = DEFAULT_PADDING_HALF).size(iconSize).then(iconClickable), tint = iconTint)
|
||||
}
|
||||
CardColumn(padding) { content() }
|
||||
}
|
||||
|
||||
+2
-1
@@ -31,6 +31,7 @@ fun TextEditor(
|
||||
modifier: Modifier,
|
||||
placeholder: String? = null,
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING),
|
||||
shape: Shape = RoundedCornerShape(14.dp),
|
||||
isValid: (String) -> Boolean = { true },
|
||||
focusRequester: FocusRequester? = null,
|
||||
enabled: Boolean = true
|
||||
@@ -53,7 +54,7 @@ fun TextEditor(
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding)
|
||||
.heightIn(min = 52.dp)
|
||||
.border(border = BorderStroke(1.dp, strokeColor), shape = RoundedCornerShape(14.dp)),
|
||||
.border(border = BorderStroke(1.dp, strokeColor), shape = shape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val textFieldModifier = modifier
|
||||
|
||||
+21
-17
@@ -38,7 +38,8 @@ import dev.icerock.moko.resources.compose.painterResource
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
@Composable
|
||||
fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit) {
|
||||
fun AddChannelView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, closeAll: () -> Unit) {
|
||||
val rhId = rh?.remoteHostId
|
||||
val view = LocalMultiplatformView()
|
||||
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -56,7 +57,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
|
||||
|
||||
val gInfo = groupInfo.value
|
||||
if (showLinkStep.value && gInfo != null) {
|
||||
LinkStepView(chatModel, gInfo, groupLink, closeAll)
|
||||
LinkStepView(chatModel, rhId, gInfo, groupLink, closeAll)
|
||||
} else if (gInfo != null) {
|
||||
ProgressStepView(
|
||||
chatModel, gInfo, groupRelays, relayListExpanded,
|
||||
@@ -65,9 +66,9 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
|
||||
chatModel.creatingChannelId.value = null
|
||||
closeAll()
|
||||
withBGApi {
|
||||
openGroupChat(null, gInfo.groupId)
|
||||
openGroupChat(rhId, gInfo.groupId)
|
||||
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close ->
|
||||
GroupLinkView(chatModel, rhId = null, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, shareGroupInfo = gInfo, close = close)
|
||||
GroupLinkView(chatModel, rhId = rhId, groupInfo = gInfo, groupLink = groupLink.value, onGroupLinkUpdated = null, creatingGroup = true, isChannel = true, shareGroupInfo = gInfo, close = close)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,9 +81,9 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
|
||||
closeAll()
|
||||
withBGApi {
|
||||
try {
|
||||
chatModel.controller.apiDeleteChat(rh = null, type = ChatType.Group, id = gInfo.apiId)
|
||||
chatModel.controller.apiDeleteChat(rh = rhId, type = ChatType.Group, id = gInfo.apiId)
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.removeChat(null, gInfo.id)
|
||||
chatModel.chatsContext.removeChat(rhId, gInfo.id)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "cancelChannelCreation error: ${e.message}")
|
||||
@@ -93,6 +94,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
|
||||
} else {
|
||||
ProfileStepView(
|
||||
chatModel = chatModel,
|
||||
rhId = rhId,
|
||||
displayName = displayName,
|
||||
profileImage = profileImage,
|
||||
chosenImage = chosenImage,
|
||||
@@ -120,7 +122,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
|
||||
creationInProgress.value = true
|
||||
withBGApi {
|
||||
try {
|
||||
val enabledRelays = chooseRandomRelays()
|
||||
val enabledRelays = chooseRandomRelays(rhId)
|
||||
val relayIds = enabledRelays.mapNotNull { it.chatRelayId }
|
||||
if (relayIds.isEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -130,7 +132,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
|
||||
return@withBGApi
|
||||
}
|
||||
val result = chatModel.controller.apiNewPublicGroup(
|
||||
rh = null,
|
||||
rh = rhId,
|
||||
incognito = false,
|
||||
relayIds = relayIds,
|
||||
groupProfile = profile
|
||||
@@ -138,7 +140,7 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
|
||||
when (result) {
|
||||
is ChatController.PublicGroupCreationResult.Created -> {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId = null, result.groupInfo)
|
||||
chatModel.chatsContext.updateGroup(rhId = rhId, result.groupInfo)
|
||||
chatModel.creatingChannelId.value = result.groupInfo.id
|
||||
groupInfo.value = result.groupInfo
|
||||
groupLink.value = result.groupLink
|
||||
@@ -178,8 +180,8 @@ fun AddChannelView(chatModel: ChatModel, close: () -> Unit, closeAll: () -> Unit
|
||||
|
||||
private const val maxRelays = 3
|
||||
|
||||
private suspend fun chooseRandomRelays(): List<UserChatRelay> {
|
||||
val servers = getUserServers(rh = null) ?: return emptyList()
|
||||
private suspend fun chooseRandomRelays(rhId: Long?): List<UserChatRelay> {
|
||||
val servers = getUserServers(rh = rhId) ?: return emptyList()
|
||||
// Operator relays are grouped per operator; custom relays (null operator)
|
||||
// are treated independently to maximize trust distribution.
|
||||
val operatorGroups = mutableListOf<List<UserChatRelay>>()
|
||||
@@ -215,8 +217,8 @@ private suspend fun chooseRandomRelays(): List<UserChatRelay> {
|
||||
return selected
|
||||
}
|
||||
|
||||
private suspend fun checkHasRelays(): Boolean {
|
||||
val servers = try { getUserServers(rh = null) } catch (_: Exception) { null } ?: return false
|
||||
private suspend fun checkHasRelays(rhId: Long?): Boolean {
|
||||
val servers = try { getUserServers(rh = rhId) } catch (_: Exception) { null } ?: return false
|
||||
return servers.any { op ->
|
||||
(op.operator?.enabled ?: true) &&
|
||||
op.chatRelays.any { it.enabled && !it.deleted && it.chatRelayId != null }
|
||||
@@ -226,6 +228,7 @@ private suspend fun checkHasRelays(): Boolean {
|
||||
@Composable
|
||||
private fun ProfileStepView(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
displayName: MutableState<String>,
|
||||
profileImage: MutableState<String?>,
|
||||
chosenImage: MutableState<URI?>,
|
||||
@@ -239,7 +242,7 @@ private fun ProfileStepView(
|
||||
createChannel: () -> Unit
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
hasRelays.value = checkHasRelays()
|
||||
hasRelays.value = checkHasRelays(rhId)
|
||||
}
|
||||
|
||||
ModalBottomSheetLayout(
|
||||
@@ -553,6 +556,7 @@ private fun RelayRow(relay: GroupRelay, connFailed: Boolean) {
|
||||
@Composable
|
||||
private fun LinkStepView(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
gInfo: GroupInfo,
|
||||
groupLink: MutableState<GroupLink?>,
|
||||
closeAll: () -> Unit
|
||||
@@ -563,14 +567,14 @@ private fun LinkStepView(
|
||||
delay(500)
|
||||
withContext(Dispatchers.Main) {
|
||||
ModalManager.start.closeModals()
|
||||
openGroupChat(null, gInfo.groupId)
|
||||
openGroupChat(rhId, gInfo.groupId)
|
||||
}
|
||||
}
|
||||
}
|
||||
ModalView(close = close, showClose = false, cardScreen = true) {
|
||||
GroupLinkView(
|
||||
chatModel = chatModel,
|
||||
rhId = null,
|
||||
rhId = rhId,
|
||||
groupInfo = gInfo,
|
||||
groupLink = groupLink.value,
|
||||
onGroupLinkUpdated = { groupLink.value = it },
|
||||
@@ -660,6 +664,6 @@ fun RelayProgressIndicator(active: Int, total: Int) {
|
||||
@Composable
|
||||
fun PreviewAddChannelView() {
|
||||
SimpleXTheme {
|
||||
AddChannelView(chatModel = ChatModel, close = {}, closeAll = {})
|
||||
AddChannelView(chatModel = ChatModel, rh = null, close = {}, closeAll = {})
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) }
|
||||
},
|
||||
createChannel = {
|
||||
ModalManager.start.showCustomModal { close -> AddChannelView(chatModel, close, closeAll) }
|
||||
ModalManager.start.showCustomModal { close -> AddChannelView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) }
|
||||
},
|
||||
rh = rh,
|
||||
close = close
|
||||
|
||||
+5
-9
@@ -148,7 +148,6 @@ private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) {
|
||||
AppBarTitle(stringResource(MR.strings.connecting_to_desktop))
|
||||
SectionView(stringResource(MR.strings.connecting_to_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
CtrlDeviceNameText(session, rc)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
CtrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
@@ -257,7 +256,6 @@ private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessC
|
||||
AppBarTitle(stringResource(MR.strings.verify_connection))
|
||||
SectionView(stringResource(MR.strings.connected_to_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
CtrlDeviceNameText(session, rc)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
CtrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
@@ -265,16 +263,15 @@ private fun VerifySession(session: RemoteCtrlSession, rc: RemoteCtrlInfo?, sessC
|
||||
|
||||
SectionView(stringResource(MR.strings.verify_code_with_desktop)) {
|
||||
SessionCodeText(sessCode)
|
||||
SectionItemView({ verifyDesktopSessionCode(remoteCtrls, sessCode) }) {
|
||||
Icon(painterResource(MR.images.ic_check), generalGetString(MR.strings.confirm_verb), tint = MaterialTheme.colors.secondary)
|
||||
TextIconSpaced(false)
|
||||
Text(generalGetString(MR.strings.confirm_verb))
|
||||
}
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionItemView({ verifyDesktopSessionCode(remoteCtrls, sessCode) }) {
|
||||
Icon(painterResource(MR.images.ic_check), generalGetString(MR.strings.confirm_verb), tint = MaterialTheme.colors.secondary)
|
||||
TextIconSpaced(false)
|
||||
Text(generalGetString(MR.strings.confirm_verb))
|
||||
}
|
||||
|
||||
SectionView {
|
||||
DisconnectButton(onClick = ::disconnectDesktop)
|
||||
}
|
||||
@@ -312,7 +309,6 @@ private fun ActiveSession(session: RemoteCtrlSession, rc: RemoteCtrlInfo, close:
|
||||
AppBarTitle(stringResource(MR.strings.connected_to_desktop))
|
||||
SectionView(stringResource(MR.strings.connected_desktop), contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
Text(rc.deviceViewName)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
|
||||
CtrlDeviceVersionText(session)
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
@@ -38,12 +39,14 @@ fun CallSettingsLayout(
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.your_calls))
|
||||
val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) }
|
||||
SectionView(stringResource(MR.strings.settings_section_title_settings)) {
|
||||
SectionItemView(editIceServers) { Text(stringResource(MR.strings.webrtc_ice_servers)) }
|
||||
|
||||
val enabled = remember { mutableStateOf(true) }
|
||||
LockscreenOpts(lockCallState, enabled, onSelected = { callOnLockScreen.set(it); lockCallState.value = it })
|
||||
if (appPlatform.isAndroid) {
|
||||
val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) }
|
||||
val enabled = remember { mutableStateOf(true) }
|
||||
LockscreenOpts(lockCallState, enabled, onSelected = { callOnLockScreen.set(it); lockCallState.value = it })
|
||||
}
|
||||
SettingsPreferenceItem(null, stringResource(MR.strings.always_use_relay), webrtcPolicyRelay)
|
||||
}
|
||||
SectionTextFooter(
|
||||
|
||||
+35
-35
@@ -665,46 +665,46 @@ fun SimplexLockView(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (performLA.value && laMode.value == LAMode.PASSCODE) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.self_destruct_passcode)) {
|
||||
val openInfo = {
|
||||
ModalManager.start.showModal {
|
||||
SelfDestructInfoView()
|
||||
}
|
||||
}
|
||||
if (performLA.value && laMode.value == LAMode.PASSCODE) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.self_destruct_passcode)) {
|
||||
val openInfo = {
|
||||
ModalManager.start.showModal {
|
||||
SelfDestructInfoView()
|
||||
}
|
||||
SettingsActionItemWithContent(null, null, click = openInfo) {
|
||||
SharedPreferenceToggleWithIcon(
|
||||
stringResource(MR.strings.enable_self_destruct),
|
||||
painterResource(MR.images.ic_info),
|
||||
openInfo,
|
||||
remember { selfDestructPref.state }.value
|
||||
) {
|
||||
toggleSelfDestruct(selfDestructPref)
|
||||
}
|
||||
}
|
||||
SettingsActionItemWithContent(null, null, click = openInfo) {
|
||||
SharedPreferenceToggleWithIcon(
|
||||
stringResource(MR.strings.enable_self_destruct),
|
||||
painterResource(MR.images.ic_info),
|
||||
openInfo,
|
||||
remember { selfDestructPref.state }.value
|
||||
) {
|
||||
toggleSelfDestruct(selfDestructPref)
|
||||
}
|
||||
}
|
||||
|
||||
if (remember { selfDestructPref.state }.value) {
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) {
|
||||
Text(
|
||||
stringResource(MR.strings.self_destruct_new_display_name),
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
|
||||
)
|
||||
ProfileNameField(selfDestructDisplayName, "", { isValidDisplayName(it.trim()) })
|
||||
LaunchedEffect(selfDestructDisplayName.value) {
|
||||
val new = selfDestructDisplayName.value
|
||||
if (isValidDisplayName(new) && selfDestructDisplayNamePref.get() != new) {
|
||||
selfDestructDisplayNamePref.set(new)
|
||||
}
|
||||
if (remember { selfDestructPref.state }.value) {
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) {
|
||||
Text(
|
||||
stringResource(MR.strings.self_destruct_new_display_name),
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
|
||||
)
|
||||
ProfileNameField(selfDestructDisplayName, "", { isValidDisplayName(it.trim()) })
|
||||
LaunchedEffect(selfDestructDisplayName.value) {
|
||||
val new = selfDestructDisplayName.value
|
||||
if (isValidDisplayName(new) && selfDestructDisplayNamePref.get() != new) {
|
||||
selfDestructDisplayNamePref.set(new)
|
||||
}
|
||||
}
|
||||
SectionItemView({ changeSelfDestructPassword() }) {
|
||||
Text(
|
||||
stringResource(MR.strings.change_self_destruct_passcode),
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionItemView({ changeSelfDestructPassword() }) {
|
||||
Text(
|
||||
stringResource(MR.strings.change_self_destruct_passcode),
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionCardShape
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
@@ -699,7 +700,13 @@ private fun AcceptIncognitoToggle(addressSettingsState: MutableState<AddressSett
|
||||
@Composable
|
||||
private fun AutoReplyEditor(addressSettingsState: MutableState<AddressSettingsState>) {
|
||||
val autoReply = rememberSaveable { mutableStateOf(addressSettingsState.value.autoReply) }
|
||||
TextEditor(autoReply, Modifier.height(100.dp), placeholder = stringResource(MR.strings.enter_welcome_message_optional))
|
||||
TextEditor(
|
||||
autoReply,
|
||||
Modifier.height(100.dp),
|
||||
placeholder = stringResource(MR.strings.enter_welcome_message_optional),
|
||||
contentPadding = PaddingValues(),
|
||||
shape = SectionCardShape
|
||||
)
|
||||
LaunchedEffect(autoReply.value) {
|
||||
if (autoReply.value != addressSettingsState.value.autoReply) {
|
||||
addressSettingsState.value = AddressSettingsState(
|
||||
|
||||
+21
-30
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.views.usersettings.networkAndServers
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionCardShape
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
@@ -10,9 +11,7 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
@@ -230,43 +229,35 @@ private fun CustomRelay(
|
||||
}
|
||||
|
||||
SectionView(
|
||||
stringResource(MR.strings.your_relay_address).uppercase(),
|
||||
stringResource(MR.strings.your_relay_address),
|
||||
icon = painterResource(MR.images.ic_error),
|
||||
iconTint = if (!validAddress.value) MaterialTheme.colors.error else Color.Transparent,
|
||||
) {
|
||||
TextEditor(
|
||||
relayAddress,
|
||||
Modifier.height(144.dp)
|
||||
Modifier.height(144.dp),
|
||||
contentPadding = PaddingValues(),
|
||||
shape = SectionCardShape
|
||||
)
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
Column {
|
||||
val iconSize = with(LocalDensity.current) { 21.sp.toDp() }
|
||||
Row(Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
stringResource(MR.strings.your_relay_name).uppercase(),
|
||||
color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = 12.sp
|
||||
)
|
||||
IconButton(
|
||||
onClick = { if (!validName.value) showInvalidRelayNameAlert(relayName) },
|
||||
enabled = !validName.value,
|
||||
modifier = Modifier.padding(start = DEFAULT_PADDING_HALF).size(iconSize)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_error), null,
|
||||
tint = if (!validName.value) MaterialTheme.colors.error else Color.Transparent
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
TextEditor(
|
||||
relayName,
|
||||
Modifier,
|
||||
placeholder = generalGetString(MR.strings.enter_relay_name),
|
||||
enabled = relay.value.tested != true
|
||||
)
|
||||
}
|
||||
SectionView(
|
||||
stringResource(MR.strings.your_relay_name),
|
||||
icon = painterResource(MR.images.ic_error),
|
||||
iconTint = if (!validName.value) MaterialTheme.colors.error else Color.Transparent,
|
||||
onIconClick = if (!validName.value) {
|
||||
{ showInvalidRelayNameAlert(relayName) }
|
||||
} else null
|
||||
) {
|
||||
TextEditor(
|
||||
relayName,
|
||||
Modifier,
|
||||
placeholder = generalGetString(MR.strings.enter_relay_name),
|
||||
contentPadding = PaddingValues(),
|
||||
shape = SectionCardShape,
|
||||
enabled = relay.value.tested != true
|
||||
)
|
||||
}
|
||||
if (relay.value.tested != true) {
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.test_relay_to_retrieve_name))
|
||||
|
||||
@@ -24,13 +24,13 @@ android.nonTransitiveRClass=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=7.0-beta.1
|
||||
android.version_code=360
|
||||
android.version_name=7.0-beta.2
|
||||
android.version_code=361
|
||||
|
||||
android.bundle=false
|
||||
|
||||
desktop.version_name=7.0-beta.1
|
||||
desktop.version_code=149
|
||||
desktop.version_name=7.0-beta.2
|
||||
desktop.version_code=150
|
||||
|
||||
kotlin.version=2.1.20
|
||||
gradle.plugin.version=8.7.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simplex-chat",
|
||||
"version": "7.0.0-beta.1",
|
||||
"version": "7.0.0-beta.2",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
|
||||
@@ -4,7 +4,7 @@ const path = require('path');
|
||||
const extract = require('extract-zip');
|
||||
|
||||
const GITHUB_REPO = 'simplex-chat/simplex-chat-libs';
|
||||
const RELEASE_TAG = 'v7.0.0-beta.1';
|
||||
const RELEASE_TAG = 'v7.0.0-beta.2';
|
||||
const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase();
|
||||
|
||||
if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') {
|
||||
|
||||
@@ -5,5 +5,5 @@ Bump both together for normal releases. For wrapper-only fixes use a PEP 440
|
||||
post-release: __version__ = "6.5.2.post1", LIBS_VERSION unchanged.
|
||||
"""
|
||||
|
||||
__version__ = "7.0.0b1" # PEP 440 — read by hatchling for wheel metadata
|
||||
LIBS_VERSION = "7.0.0-beta.1" # simplex-chat-libs release tag (no 'v' prefix)
|
||||
__version__ = "7.0.0b2" # PEP 440 — read by hatchling for wheel metadata
|
||||
LIBS_VERSION = "7.0.0-beta.2" # simplex-chat-libs release tag (no 'v' prefix)
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# Remove left padding on consecutive (no-avatar) received messages in channels
|
||||
|
||||
## Problem
|
||||
|
||||
In a channel, received messages show the sender avatar on the first message of a
|
||||
run and hide it on consecutive messages, but those consecutive messages still
|
||||
reserve the avatar-sized **left padding** so they line up under the first. For a
|
||||
channel's feed-style layout this indentation wastes horizontal space —
|
||||
consecutive received messages should sit flush-left where the avatar would be.
|
||||
This applies to **both** the channel owner's broadcasts and contributors' posts.
|
||||
|
||||
Desired behaviour: in channels, any received message that does **not** show an
|
||||
avatar (a consecutive post from the same sender) drops the avatar-sized left
|
||||
padding. The first message of a run still shows the avatar and keeps its layout;
|
||||
when the run is broken (a different sender, or a time gap), the next message
|
||||
shows the avatar again — this run logic is unchanged, only the no-avatar left
|
||||
padding is reduced.
|
||||
|
||||
## The two received directions in a channel
|
||||
|
||||
Received items in a channel arrive as one of two directions
|
||||
(`Subscriber.hs`, `saveRcvCI`):
|
||||
|
||||
- **`ChannelRcv`** (no member) — the **owner's** broadcast, sent "as the channel".
|
||||
The backend permits sending-as-group only to the owner, and a channel owner's
|
||||
main-scope messages are always sent as group (`ChatInfo.sendAsGroup` is true
|
||||
for `useRelays && memberRole >= Owner` in the main scope), so received owner
|
||||
posts arrive as `ChannelRcv`. Shows the channel avatar.
|
||||
- **`GroupRcv(member)`** (attributed) — a **contributor's** post, carrying the
|
||||
member. Shows the member avatar.
|
||||
|
||||
Both are received messages, and the change now applies to **both** when they hide
|
||||
the avatar. (An earlier revision scoped this to `ChannelRcv`/owner only; it now
|
||||
covers contributors too, per request.)
|
||||
|
||||
## Change
|
||||
|
||||
In channels — gated on `ChatInfo.isChannel` (the `useRelays` flag, which is
|
||||
reliably present, unlike the optional group-type predicate) — the no-avatar
|
||||
branches for **both** `ChannelRcv` and `GroupRcv` drop the avatar-sized left
|
||||
padding down to the base inset where the avatar itself starts. In non-channel
|
||||
groups the `GroupRcv` no-avatar layout is unchanged (`isChannel` is false). The
|
||||
avatar-shown layouts, sent messages, and all other chats are unchanged.
|
||||
|
||||
The same Row's `end` padding is already gated on `chatInfo.isChannel` (the merged
|
||||
right-gap change #7106), so gating `start` on `isChannel` keeps each Row
|
||||
internally consistent and the change precisely "in channels".
|
||||
|
||||
### Android / desktop (`apps/multiplatform`, `ChatView.kt`, `ChatItemsList`)
|
||||
|
||||
Both the `CIDirection.GroupRcv` and `CIDirection.ChannelRcv` `showAvatar == false`
|
||||
rows:
|
||||
|
||||
```kotlin
|
||||
// before
|
||||
.padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = …)
|
||||
// after
|
||||
.padding(start = if (chatInfo.isChannel) 8.dp else 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = …)
|
||||
```
|
||||
|
||||
### iOS (`apps/ios`, `ChatView.swift`, `chatItemListView`)
|
||||
|
||||
Both the `.groupRcv` and `.channelRcv` no-avatar branches:
|
||||
|
||||
```swift
|
||||
// before
|
||||
.padding(.leading, 10 + memberImageSize + 12)
|
||||
// after
|
||||
.padding(.leading, chat.chatInfo.isChannel ? 12 : 10 + memberImageSize + 12)
|
||||
```
|
||||
|
||||
## Run behaviour (unchanged)
|
||||
|
||||
`shouldShowAvatar(current, older)` shows the avatar on the first message of a
|
||||
same-sender run and hides it on consecutive ones; a different sender or a gap
|
||||
resets the run. For `GroupRcv` "same sender" is the same `memberId`; for
|
||||
`ChannelRcv` consecutive channel broadcasts count as the same sender. Only the
|
||||
no-avatar left padding is changed.
|
||||
|
||||
## Scope
|
||||
|
||||
- Affects: all received consecutive (no-avatar) messages **in channels** — owner
|
||||
broadcasts (`ChannelRcv`) and contributor posts (`GroupRcv`). This includes a
|
||||
channel's member-support sub-scope, which renders through the same
|
||||
`ChatItemsList` with the channel's `isChannel`; treating it the same way is
|
||||
consistent with the merged right-gap change (#7106), which also gates that
|
||||
Row's `end` padding on `isChannel` without a scope filter.
|
||||
- Unchanged: the first message of each run (avatar shown), sent messages, regular
|
||||
groups, business chats and direct chats (`isChannel` false — the `else` branch
|
||||
preserves the original avatar-inset value exactly), and any non-channel
|
||||
`ChannelRcv` welcome item.
|
||||
|
||||
## Verification
|
||||
|
||||
- Android/desktop: `:common:compileKotlinDesktop` compiles clean.
|
||||
- iOS: small, type-safe constant change; build/verify on macOS (Xcode) — not
|
||||
compilable on the Linux build host used here.
|
||||
- Visual (both platforms), in a channel:
|
||||
- First message of a run (owner or contributor): avatar shown, layout unchanged.
|
||||
- Following messages from the same sender (no avatar): now flush-left.
|
||||
- A different sender / time gap resets the run — the next message shows the
|
||||
avatar again.
|
||||
- Regular groups, business and direct chats keep their existing indentation.
|
||||
+1
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 7.0.0.5
|
||||
version: 7.0.0.6
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
||||
Reference in New Issue
Block a user