From 9d65b9eb2b24722be7f35e2d851ba397c2d12d37 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:07:28 +0000 Subject: [PATCH] ui: navigation from support chat to member info (#6237) --- apps/ios/Shared/Views/Chat/ChatView.swift | 38 +++++++++++++++++-- .../Chat/Group/GroupMemberInfoView.swift | 4 +- .../Views/Chat/Group/SecondaryChatView.swift | 6 ++- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 ++++---- apps/ios/SimpleXChat/ChatTypes.swift | 3 +- .../chat/simplex/common/model/ChatModel.kt | 3 +- .../simplex/common/views/chat/ChatView.kt | 6 ++- .../views/chat/group/GroupChatInfoView.kt | 2 +- .../views/chat/group/GroupMemberInfoView.kt | 10 ++++- .../views/chat/group/MemberSupportChatView.kt | 27 ++++++++++++- 10 files changed, 93 insertions(+), 22 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 2f8d6f2acd..83382cfe4f 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -60,6 +60,7 @@ struct ChatView: View { @State private var animatedScrollingInProgress: Bool = false @State private var showUserSupportChatSheet = false @State private var showCommandsMenu = false + @State private var supportChatMemberInfoLinkActive = false @State private var scrollView: EndlessScrollView = EndlessScrollView(frame: .zero) @@ -178,6 +179,28 @@ struct ChatView: View { if im.showLoadingProgress == chat.id { ProgressView().scaleEffect(2) } + if case let .group(groupInfo, _) = chat.chatInfo, + case let .groupChatScopeContext(groupScopeInfo) = im.secondaryIMFilter, + case let .memberSupport(groupMember_) = groupScopeInfo, + let groupMember = groupMember_ { + NavigationLink(isActive: $supportChatMemberInfoLinkActive) { + GroupMemberInfoView( + groupInfo: groupInfo, + chat: chat, + groupMember: GMember(groupMember), + scrollToItemId: $scrollToItemId, + openedFromSupportChat: true + ) + .navigationBarHidden(false) + .modifier(BackButton(disabled: Binding.constant(false)) { + supportChatMemberInfoLinkActive = false + }) + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } } .safeAreaInset(edge: .top) { VStack(spacing: .zero) { @@ -222,7 +245,9 @@ struct ChatView: View { } } } - .appSheet(item: $selectedMember) { member in + .appSheet(item: $selectedMember, onDismiss: { + chatModel.secondaryIM = nil + }) { member in if case let .group(groupInfo, _) = chat.chatInfo { GroupMemberInfoView( groupInfo: groupInfo, @@ -459,7 +484,10 @@ struct ChatView: View { ChatInfoToolbar(chat: chat) .tint(theme.colors.primary) } - .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) { + .appSheet(isPresented: $showChatInfoSheet, onDismiss: { + chatModel.secondaryIM = nil + theme = buildTheme() + }) { GroupChatInfoView( chat: chat, groupInfo: Binding( @@ -562,7 +590,11 @@ struct ChatView: View { switch groupScopeInfo { case let .memberSupport(groupMember_): if let groupMember = groupMember_ { - MemberSupportChatToolbar(groupMember: groupMember) + Button { + supportChatMemberInfoLinkActive = true + } label: { + MemberSupportChatToolbar(groupMember: groupMember) + } } else { textChatToolbar("Chat with admins") } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 2057b9b43c..2298af614e 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -18,6 +18,7 @@ struct GroupMemberInfoView: View { @ObservedObject var groupMember: GMember @Binding var scrollToItemId: ChatItem.ID? var navigation: Bool = false + var openedFromSupportChat: Bool = false @State private var connectionStats: ConnectionStats? = nil @State private var connectionCode: String? = nil @State private var connectionLoaded: Bool = false @@ -101,7 +102,8 @@ struct GroupMemberInfoView: View { if member.memberActive { Section { - if groupInfo.membership.memberRole >= .moderator + if !openedFromSupportChat + && groupInfo.membership.memberRole >= .moderator && (member.memberRole < .moderator || member.supportChat != nil) { MemberInfoSupportChatNavLink(groupInfo: groupInfo, member: groupMember, scrollToItemId: $scrollToItemId) } diff --git a/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift b/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift index 47c5df264f..e2092f7a24 100644 --- a/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift +++ b/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift @@ -10,6 +10,7 @@ import SwiftUI import SimpleXChat struct SecondaryChatView: View { + @Environment(\.dismiss) var dismiss @EnvironmentObject var chatModel: ChatModel @ObservedObject var chat: Chat @Binding var scrollToItemId: ChatItem.ID? @@ -23,9 +24,10 @@ struct SecondaryChatView: View { floatingButtonModel: FloatingButtonModel(im: im), scrollToItemId: $scrollToItemId ) - .onDisappear { + .modifier(BackButton(disabled: Binding.constant(false)) { chatModel.secondaryIM = nil - } + dismiss() + }) } } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 7a7f1e481d..4e7bd722c3 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.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 */; }; @@ -545,8 +545,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-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.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 = ""; }; @@ -708,8 +708,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -795,8 +795,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-IWC91cESq2l3JFgl07ryw1.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.4.2-7qDqJsgFG1qLUMejyoXxtN.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index aed5373664..febe12de02 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2634,7 +2634,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { } public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? { - if !canBeRemoved(groupInfo: groupInfo) { return nil } + if !canBeRemoved(groupInfo: groupInfo) || memberPending { return nil } let userRole = groupInfo.membership.memberRole return GroupMemberRole.supportedRoles.filter { $0 <= userRole } } @@ -2643,6 +2643,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable { let userRole = groupInfo.membership.memberRole return memberStatus != .memRemoved && memberStatus != .memLeft && memberRole < .moderator && userRole >= .moderator && userRole >= memberRole && groupInfo.membership.memberActive + && !memberPending } public var canReceiveReports: Bool { 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 047d14d9f1..7bf849f8f7 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 @@ -2343,7 +2343,7 @@ data class GroupMember ( } fun canChangeRoleTo(groupInfo: GroupInfo): List? = - if (!canBeRemoved(groupInfo)) null + if (!canBeRemoved(groupInfo) || memberPending) null else groupInfo.membership.memberRole.let { userRole -> GroupMemberRole.selectableRoles.filter { it <= userRole } } @@ -2352,6 +2352,7 @@ data class GroupMember ( val userRole = groupInfo.membership.memberRole return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft && memberRole < GroupMemberRole.Moderator && userRole >= GroupMemberRole.Moderator && userRole >= memberRole && groupInfo.membership.memberActive + && !memberPending } val versionRange: VersionRange = activeConn?.peerChatVRange ?: memberChatVRange 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 de48a67988..eb858d8d5f 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 @@ -453,7 +453,7 @@ fun ChatView( } ModalManager.end.showModalCloseable(true) { close -> remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> - GroupMemberInfoView(chatRh, groupInfo, mem, scrollToItemId, stats, code, chatModel, close, close) + GroupMemberInfoView(chatRh, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, close, close) } } } @@ -1005,7 +1005,9 @@ fun ChatLayout( Column(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) { Box { if (selectedChatItems.value == null) { - MemberSupportChatAppBar(chatsCtx, chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_, { ModalManager.end.closeModal() }, onSearchValueChanged) + if (chat != null) { + MemberSupportChatAppBar(chatsCtx, remoteHostId, chat, chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_, scrollToItemId, { ModalManager.end.closeModal() }, onSearchValueChanged) + } } else { SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 5a749766c4..1ea3daeab1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -126,7 +126,7 @@ fun ModalData.GroupChatInfoView( } ModalManager.end.showModalCloseable(true) { closeCurrent -> remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> - GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, closeCurrent) { + GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, closeCurrent) { closeCurrent() close() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index e5f0b708e7..b29374390e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -49,9 +49,13 @@ fun GroupMemberInfoView( connectionStats: ConnectionStats?, connectionCode: String?, chatModel: ChatModel, + openedFromSupportChat: Boolean, close: () -> Unit, closeAll: () -> Unit, // Close all open windows up to ChatView ) { + KeyChangeEffect(chat.simplex.common.platform.chatModel.chatId.value) { + ModalManager.end.closeModals() + } BackHandler(onBack = close) val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId } val connStats = remember { mutableStateOf(connectionStats) } @@ -225,7 +229,8 @@ fun GroupMemberInfoView( ) } } - } + }, + openedFromSupportChat = openedFromSupportChat ) if (progressIndicator) { @@ -291,6 +296,7 @@ fun GroupMemberInfoLayout( syncMemberConnection: () -> Unit, syncMemberConnectionForce: () -> Unit, verifyClicked: () -> Unit, + openedFromSupportChat: Boolean ) { val cStats = connStats.value fun knownDirectChat(contactId: Long): Pair? { @@ -440,6 +446,7 @@ fun GroupMemberInfoLayout( if (member.memberActive) { SectionView { if ( + !openedFromSupportChat && groupInfo.membership.memberRole >= GroupMemberRole.Moderator && (member.memberRole < GroupMemberRole.Moderator || member.supportChat != null) ) { @@ -924,6 +931,7 @@ fun PreviewGroupMemberInfoLayout() { syncMemberConnection = {}, syncMemberConnectionForce = {}, verifyClicked = {}, + openedFromSupportChat = false, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt index 99565618f9..6680ef99bc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt @@ -51,7 +51,10 @@ private fun MemberSupportChatView( @Composable fun MemberSupportChatAppBar( chatsCtx: ChatModel.ChatsContext, + rhId: Long?, + chat: Chat, scopeMember_: GroupMember?, + scrollToItemId: MutableState, close: () -> Unit, onSearchValueChanged: (String) -> Unit ) { @@ -67,11 +70,31 @@ fun MemberSupportChatAppBar( } } BackHandler(onBack = onBackClicked) - if (scopeMember_ != null) { + if (chat.chatInfo is ChatInfo.Group && scopeMember_ != null) { + val groupInfo = chat.chatInfo.groupInfo DefaultAppBar( navigationButton = { NavigationButtonBack(onBackClicked) }, title = { MemberSupportChatToolbarTitle(scopeMember_) }, - onTitleClick = null, + onTitleClick = { + withBGApi { + val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, scopeMember_.groupMemberId) + val stats = r?.second + val code = if (scopeMember_.memberActive) { + val memCode = chatModel.controller.apiGetGroupMemberCode(rhId, groupInfo.apiId, scopeMember_.groupMemberId) + memCode?.second + } else { + null + } + ModalManager.end.showModalCloseable(true) { closeCurrent -> + remember { derivedStateOf { chatModel.getGroupMember(scopeMember_.groupMemberId) } }.value?.let { mem -> + GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = true, close = closeCurrent) { + closeCurrent() + close() + } + } + } + } + }, onTop = !oneHandUI.value || !chatBottomBar.value, showSearch = showSearch.value, onSearchValueChanged = onSearchValueChanged,