diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index ded0019692..283157864d 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -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) } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index e308a145b9..9c40b2b395 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -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 { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index f00f623e98..87e042be82 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -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 = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 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 = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.5-3gtbZllPEb75FgChAFCqhx.a"; sourceTree = ""; }; + 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 = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-7.0.0.6-IDb07VxlHBtGmeucUQceZv.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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; diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 8b186f168d..7a20a8d4ba 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -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 { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index c5dd81fec7..44361baa73 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -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 -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 54986e1815..02353e2c8a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -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 { - val r = sendCmd(null, CC.ApiGetGroupRelays(groupId)) + suspend fun apiGetGroupRelays(rh: Long?, groupId: Long): List { + 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): AddGroupRelaysResult() } - suspend fun apiAddGroupRelays(groupId: Long, relayIds: List): AddGroupRelaysResult? { - val r = sendCmdWithRetry(null, CC.ApiAddGroupRelays(groupId, relayIds)) + suspend fun apiAddGroupRelays(rh: Long?, groupId: Long, relayIds: List): 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}") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index cc9e71354c..4a3bfe4208 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -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) ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 21290b99b8..c37aa0a77f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -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, + val relayMembers: List, + 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) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt index 1ed75bd2a2..95ce003caa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupRelayView.kt @@ -31,6 +31,7 @@ data class AvailableRelay( @Composable fun AddGroupRelayView( + rhId: Long?, groupInfo: GroupInfo, existingRelayIds: Set, 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() 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, selectedRelayIds: Set, @@ -192,7 +194,7 @@ private suspend fun addSelectedRelays( updateState: (Set, List) -> Unit ) { try { - val result = ChatController.apiAddGroupRelays(groupInfo.groupId, relayIds) + val result = ChatController.apiAddGroupRelays(rhId, groupInfo.groupId, relayIds) if (result == null) { updateState(selectedRelayIds, availableRelays) return diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt index d7ffde71c8..60cc19bb1b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelRelaysView.kt @@ -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) } }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt index 98067e49dd..262a78b3c8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelWebPageView.kt @@ -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) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index 02bee37c24..b2b89ca041 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -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, 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 ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 5c07fe3abf..2e1db8928e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -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) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 2c7e443b4d..fbc4c7e336 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -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) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt index 9afcdd0b94..5196a144b1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt @@ -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() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt index cd40585cad..833f53f2af 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt @@ -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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt index b5188178fa..639a5cc78e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddChannelView.kt @@ -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 { - val servers = getUserServers(rh = null) ?: return emptyList() +private suspend fun chooseRandomRelays(rhId: Long?): List { + 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>() @@ -215,8 +217,8 @@ private suspend fun chooseRandomRelays(): List { 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, profileImage: MutableState, chosenImage: MutableState, @@ -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, 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 = {}) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index af7f59496b..68f42f4186 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt index 81e1afd22c..86be99d1bf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt @@ -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) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt index cb36e4ae1a..43863d5794 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt @@ -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( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index cf34fd5a44..0b698b2c5d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -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 + ) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index c55eaf6c10..36c74cceb6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -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) { 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( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt index ab63067226..9a2d7f8e61 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/ChatRelayView.kt @@ -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)) diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 37db0f2365..9828121a8b 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -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 diff --git a/packages/simplex-chat-nodejs/package.json b/packages/simplex-chat-nodejs/package.json index 4474348798..69be65a518 100644 --- a/packages/simplex-chat-nodejs/package.json +++ b/packages/simplex-chat-nodejs/package.json @@ -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": [ diff --git a/packages/simplex-chat-nodejs/src/download-libs.js b/packages/simplex-chat-nodejs/src/download-libs.js index 7fbf62f047..c29d6ec700 100644 --- a/packages/simplex-chat-nodejs/src/download-libs.js +++ b/packages/simplex-chat-nodejs/src/download-libs.js @@ -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') { diff --git a/packages/simplex-chat-python/src/simplex_chat/_version.py b/packages/simplex-chat-python/src/simplex_chat/_version.py index e7ad6b625a..2bd7175802 100644 --- a/packages/simplex-chat-python/src/simplex_chat/_version.py +++ b/packages/simplex-chat-python/src/simplex_chat/_version.py @@ -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) diff --git a/plans/2026-06-20-channel-received-no-avatar-left-padding.md b/plans/2026-06-20-channel-received-no-avatar-left-padding.md new file mode 100644 index 0000000000..13ee4ebc4f --- /dev/null +++ b/plans/2026-06-20-channel-received-no-avatar-left-padding.md @@ -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. diff --git a/simplex-chat.cabal b/simplex-chat.cabal index b4487dcf73..f8c599c80b 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -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