// // Copyright 2025 Element Creations Ltd. // Copyright 2022-2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. // Please see LICENSE files in the repository root for full details. // import Compound import SwiftUI struct RoomChangeRolesScreen: View { @Bindable var context: RoomChangeRolesScreenViewModel.Context var showTopSection: Bool { !context.viewState.membersWithRole.isEmpty } var body: some View { mainContent .compoundList() .scrollDismissesKeyboard(.immediately) .navigationTitle(context.viewState.title) .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(context.viewState.hasChanges) .toolbar { toolbar } .searchController(query: $context.searchQuery, placeholder: L10n.commonSearchForSomeone, showsCancelButton: false, disablesInteractiveDismiss: true) .compoundSearchField() .alert(item: $context.alertInfo) } // MARK: - Private private var mainContent: some View { GeometryReader { proxy in Form { if showTopSection { // this is a fix for having the carousel not clipped, and inside the form, so when the search is dismissed, it wont break the design Section { EmptyView() } header: { membersWithRoleSection .textCase(.none) .frame(width: proxy.size.width) } } RoomChangeRolesScreenSection(members: context.viewState.visibleOwners, role: .owner, context: context) RoomChangeRolesScreenSection(members: context.viewState.visibleAdministrators, role: .administrator, context: context) RoomChangeRolesScreenSection(members: context.viewState.visibleModerators, role: .moderator, context: context) RoomChangeRolesScreenSection(members: context.viewState.visibleUsers, role: .user, context: context) } } } @ScaledMetric private var cellWidth: CGFloat = 72 private var membersWithRoleSection: some View { ScrollView(.horizontal, showsIndicators: false) { ScrollViewReader { scrollView in HStack(spacing: 16) { ForEach(context.viewState.membersWithRole, id: \.id) { member in let dismissAction = context.viewState.isMemberDisabled(member) ? nil : { context.send(viewAction: .demoteMember(member)) } RoomChangeRolesScreenSelectedItem(member: member, mediaProvider: context.mediaProvider, dismissAction: dismissAction) .frame(width: cellWidth) } } .onChange(of: context.viewState.lastPromotedMember) { _, newValue in guard let member = newValue else { return } withElementAnimation(.easeInOut) { scrollView.scrollTo(member.id) } } .padding(.horizontal, 14) } } } @ToolbarContentBuilder private var toolbar: some ToolbarContent { ToolbarItem(placement: .confirmationAction) { ToolbarButton(role: .save) { context.send(viewAction: .save) } .disabled(!context.viewState.hasChanges) } if context.viewState.mode == .owner || context.viewState.hasChanges { ToolbarItem(placement: .cancellationAction) { ToolbarButton(role: .cancel) { context.send(viewAction: .cancel) } } } } } // MARK: - Previews struct RoomChangeRolesScreen_Previews: PreviewProvider, TestablePreview { static let ownerViewModel = makeViewModel(mode: .owner, ownRole: .creator) static let administratorOrOwnerViewModel = makeViewModel(mode: .administrator, ownRole: .creator) static let administratorViewModel = makeViewModel(mode: .administrator, ownRole: .administrator) static let moderatorViewModel = makeViewModel(mode: .moderator, ownRole: .administrator) static var previews: some View { ElementNavigationStack { RoomChangeRolesScreen(context: ownerViewModel.context) } .previewDisplayName("Owners") ElementNavigationStack { RoomChangeRolesScreen(context: administratorOrOwnerViewModel.context) } .previewDisplayName("Administrator or Owners") ElementNavigationStack { RoomChangeRolesScreen(context: administratorViewModel.context) } .previewDisplayName("Administrators") ElementNavigationStack { RoomChangeRolesScreen(context: moderatorViewModel.context) } .previewDisplayName("Moderators") } static func makeViewModel(mode: RoomRole, ownRole: RoomRole) -> RoomChangeRolesScreenViewModel { let members: [RoomMemberProxyMock] = switch ownRole { case .creator: .allMembersAsCreator default: .allMembersAsAdminV2 } return RoomChangeRolesScreenViewModel(mode: mode, roomProxy: JoinedRoomProxyMock(.init(members: members)), mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: UserIndicatorControllerMock(), analytics: ServiceLocator.shared.analytics) } }