// // 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 UserProfileScreen: View { @Bindable var context: UserProfileScreenViewModel.Context var body: some View { Form { headerSection } .compoundList() .navigationTitle(L10n.screenRoomMemberDetailsTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { toolbar } .alert(item: $context.alertInfo) .sheet(item: $context.inviteConfirmationUser) { userToInvite in SendInviteConfirmationView(userToInvite: userToInvite, mediaProvider: context.mediaProvider) { context.send(viewAction: .createDirectChat) } } .track(screen: .User) .interactiveQuickLook(item: $context.mediaPreviewItem, allowEditing: false) } // MARK: - Private @ViewBuilder private var headerSection: some View { if let userProfile = context.viewState.userProfile { AvatarHeaderView(user: userProfile, isVerified: context.viewState.showVerifiedBadge, avatarSize: .user(on: .memberDetails), mediaProvider: context.mediaProvider) { url in context.send(viewAction: .displayAvatar(url)) } footer: { otherUserFooter } } else { AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID), isVerified: context.viewState.showVerifiedBadge, avatarSize: .user(on: .memberDetails), mediaProvider: context.mediaProvider) { } } } private var otherUserFooter: some View { HStack(spacing: 8) { if context.viewState.userProfile != nil, !context.viewState.isOwnUser { Button { context.send(viewAction: .openDirectChat) } label: { CompoundIcon(\.chat) } .buttonStyle(FormActionButtonStyle(title: L10n.commonMessage)) .accessibilityIdentifier(A11yIdentifiers.roomMemberDetailsScreen.directChat) } if let roomID = context.viewState.dmRoomID { Button { context.send(viewAction: .startCall(roomID: roomID, isVoiceCall: true)) } label: { CompoundIcon(\.voiceCall) } .accessibilityLabel(L10n.a11yStartVoiceCall) .buttonStyle(FormActionButtonStyle(title: L10n.actionCall)) Button { context.send(viewAction: .startCall(roomID: roomID, isVoiceCall: false)) } label: { CompoundIcon(\.videoCall) } .accessibilityLabel(L10n.a11yStartVideoCall) .buttonStyle(FormActionButtonStyle(title: L10n.commonVideo)) } if let permalink = context.viewState.permalink { ShareLink(item: permalink) { CompoundIcon(\.shareIos) } .buttonStyle(FormActionButtonStyle(title: L10n.actionShare)) } } .padding(.top, 32) } @ToolbarContentBuilder private var toolbar: some ToolbarContent { if context.viewState.isPresentedModally { ToolbarItem(placement: .confirmationAction) { Button(L10n.actionDone) { context.send(viewAction: .dismiss) } } } } } // MARK: - Previews struct UserProfileScreen_Previews: PreviewProvider, TestablePreview { static let verifiedUserViewModel = makeViewModel(userID: RoomMemberProxyMock.mockDan.userID) static let otherUserViewModel = makeViewModel(userID: RoomMemberProxyMock.mockAlice.userID) static let accountOwnerViewModel = makeViewModel(userID: RoomMemberProxyMock.mockMe.userID) static var previews: some View { UserProfileScreen(context: verifiedUserViewModel.context) .snapshotPreferences(expect: verifiedUserViewModel.context.observe(\.viewState.isVerified).map { $0 != nil }) .previewDisplayName("Verified User") UserProfileScreen(context: otherUserViewModel.context) .snapshotPreferences(expect: otherUserViewModel.context.observe(\.viewState.isVerified).map { $0 != nil }) .previewDisplayName("Other User") UserProfileScreen(context: accountOwnerViewModel.context) .snapshotPreferences(expect: accountOwnerViewModel.context.observe(\.viewState.isVerified).map { $0 != nil }) .previewDisplayName("Account Owner") } static func makeViewModel(userID: String) -> UserProfileScreenViewModel { let clientProxyMock = ClientProxyMock(.init()) clientProxyMock.userIdentityForFallBackToServerClosure = { userID, _ in let identity = switch userID { case RoomMemberProxyMock.mockDan.userID: UserIdentityProxyMock(configuration: .init(verificationState: .verified)) default: UserIdentityProxyMock(configuration: .init()) } return .success(identity) } if userID != RoomMemberProxyMock.mockMe.userID { clientProxyMock.directRoomForUserIDReturnValue = .success("roomID") } return UserProfileScreenViewModel(userID: userID, isPresentedModally: false, userSession: UserSessionMock(.init(clientProxy: clientProxyMock)), userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics, appSettings: ServiceLocator.shared.settings) } }