diff --git a/ElementX/Sources/Other/SwiftUI/Views/RoomHeaderView.swift b/ElementX/Sources/Other/SwiftUI/Views/RoomHeaderView.swift index c4f239fe0..a6563c5a7 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/RoomHeaderView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/RoomHeaderView.swift @@ -11,10 +11,15 @@ import Compound import SwiftUI struct RoomHeaderView: View { + struct DMRecipientDetails { + var status: UserStatus? + var verification: UserIdentityVerificationState? + } + let roomName: String var roomSubtitle: String? let roomAvatar: RoomAvatar - var dmRecipientVerificationState: UserIdentityVerificationState? + var dmRecipientDetails = DMRecipientDetails() var roomHistorySharingState: RoomHistorySharingState? let mediaProvider: MediaProviderProtocol? @@ -49,10 +54,20 @@ struct RoomHeaderView: View { HStack(spacing: 4) { VStack(alignment: .leading, spacing: 0) { - Text(roomName) - .lineLimit(1) - .font(.compound.bodyMDSemibold) - .accessibilityIdentifier(A11yIdentifiers.roomScreen.name) + HStack(spacing: 8) { + Text(roomName) + .lineLimit(1) + .font(.compound.bodyMDSemibold) + .foregroundStyle(.compound.textPrimary) + .accessibilityIdentifier(A11yIdentifiers.roomScreen.name) + + if let statusEmoji = dmRecipientDetails.status?.displayed?.emoji { + Text(String(statusEmoji)) + .font(.compound.bodyLG) + .foregroundStyle(.compound.textPrimary) + } + } + if let roomSubtitle { Text(roomSubtitle) .lineLimit(1) @@ -61,8 +76,8 @@ struct RoomHeaderView: View { } } - if let dmRecipientVerificationState { - VerificationBadge(verificationState: dmRecipientVerificationState, size: .xSmall, relativeTo: .compound.bodyMDSemibold) + if let verificationState = dmRecipientDetails.verification { + VerificationBadge(verificationState: verificationState, size: .xSmall, relativeTo: .compound.bodyMDSemibold) } if let historySharingIcon { @@ -112,35 +127,61 @@ private extension View { struct RoomHeaderView_Previews: PreviewProvider, TestablePreview { static var previews: some View { - VStack(alignment: .leading, spacing: 8) { - makeHeader(avatarURL: nil, verificationState: .notVerified) - makeHeader(avatarURL: .mockMXCAvatar, verificationState: .notVerified) - makeHeader(avatarURL: .mockMXCAvatar, verificationState: .verified) - makeHeader(avatarURL: .mockMXCAvatar, verificationState: .verificationViolation) - makeHeader(avatarURL: .mockMXCAvatar, - roomSubtitle: "Subtitle", - verificationState: .verified) - makeHeader(avatarURL: .mockMXCAvatar, verificationState: .notVerified, historySharingState: .shared) - makeHeader(avatarURL: .mockMXCAvatar, verificationState: .notVerified, historySharingState: .worldReadable) - makeHeader(avatarURL: .mockMXCAvatar, verificationState: .verified, historySharingState: .shared) - makeHeader(avatarURL: .mockMXCAvatar, verificationState: .verificationViolation, historySharingState: .worldReadable) + VStack(alignment: .leading, spacing: 32) { + VStack(alignment: .leading, spacing: 16) { + makeHeader(avatarURL: nil) + makeHeader(avatarURL: .mockMXCAvatar) + + makeHeader(avatarURL: .mockMXCAvatar, historySharingState: .shared) + makeHeader(avatarURL: .mockMXCAvatar, historySharingState: .worldReadable) + } + + VStack(alignment: .leading, spacing: 16) { + makeHeader(avatarURL: .mockMXCUserAvatar, verificationState: .verified) + makeHeader(avatarURL: .mockMXCUserAvatar, verificationState: .verificationViolation) + + makeHeader(avatarURL: .mockMXCUserAvatar, + userStatus: .mockHoliday, + verificationState: .notVerified) + makeHeader(avatarURL: .mockMXCUserAvatar, + userStatus: .mockCall, + verificationState: .verificationViolation) + makeHeader(avatarURL: .mockMXCUserAvatar, + roomSubtitle: "Subtitle", + userStatus: .mockFocussing, + verificationState: .verified) + + makeHeader(avatarURL: .mockMXCUserAvatar, + roomSubtitle: "Subtitle", + verificationState: .verified) + + makeHeader(avatarURL: .mockMXCUserAvatar, + userStatus: .mockHoliday, + verificationState: .verified, + historySharingState: .shared) + makeHeader(avatarURL: .mockMXCUserAvatar, + verificationState: .verificationViolation, + historySharingState: .worldReadable) + } } .previewLayout(.sizeThatFits) } + @ViewBuilder static func makeHeader(avatarURL: URL?, roomSubtitle: String? = nil, - verificationState: UserIdentityVerificationState, + userStatus: UserStatus? = nil, + verificationState: UserIdentityVerificationState? = nil, historySharingState: RoomHistorySharingState? = nil) -> some View { - RoomHeaderView(roomName: "Some Room name", + let roomName = verificationState == nil ? "Some Room Name" : "Some User Name" + RoomHeaderView(roomName: roomName, roomSubtitle: roomSubtitle, roomAvatar: .room(id: "1", - name: "Some Room Name", + name: roomName, avatarURL: avatarURL), - dmRecipientVerificationState: verificationState, + dmRecipientDetails: .init(status: userStatus, verification: verificationState), roomHistorySharingState: historySharingState, mediaProvider: MediaProviderMock(.init())) { } - .padding() } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index f728617fc..71e67b4a3 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -42,7 +42,7 @@ enum RoomScreenViewAction { struct RoomScreenViewState: BindableState { var roomTitle = "" var roomAvatar: RoomAvatar - var dmRecipientVerificationState: UserIdentityVerificationState? + var dmRecipientDetails = RoomHeaderView.DMRecipientDetails() var lastScrollDirection: ScrollDirection? // This is used to control the banner diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 428d18f61..82d248a82 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -274,17 +274,17 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol guard roomProxy.infoPublisher.value.isDM, let dmRecipient = roomProxy.membersPublisher.value.first(where: { $0.userID != roomProxy.ownUserID }), case let .success(userIdentity) = await clientProxy.userIdentity(for: dmRecipient.userID, fallBackToServer: true) else { - state.dmRecipientVerificationState = .notVerified + state.dmRecipientDetails.verification = .notVerified return } guard let userIdentity else { MXLog.failure("User identity should be known at this point") - state.dmRecipientVerificationState = .notVerified + state.dmRecipientDetails.verification = .notVerified return } - state.dmRecipientVerificationState = userIdentity.verificationState + state.dmRecipientDetails.verification = userIdentity.verificationState } private func resolveIdentityPinningViolation(_ userID: String) async { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 8a4687c52..92e8fca2a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -292,7 +292,7 @@ struct RoomScreen: View { ToolbarItem(placement: .principal) { RoomHeaderView(roomName: context.viewState.roomTitle, roomAvatar: context.viewState.roomAvatar, - dmRecipientVerificationState: context.viewState.dmRecipientVerificationState, + dmRecipientDetails: context.viewState.dmRecipientDetails, roomHistorySharingState: context.viewState.roomHistorySharingState, mediaProvider: context.mediaProvider) { context.send(viewAction: .displayRoomDetails) diff --git a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenModels.swift b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenModels.swift index 71c190f47..de2c8ca47 100644 --- a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenModels.swift +++ b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenModels.swift @@ -16,7 +16,7 @@ struct ThreadTimelineScreenViewState: BindableState { var roomTitle: String var roomAvatar: RoomAvatar var canSendMessage = true - var dmRecipientVerificationState: UserIdentityVerificationState? + var dmRecipientDetails = RoomHeaderView.DMRecipientDetails() var roomHistorySharingState: RoomHistorySharingState? var bindings = ThreadTimelineScreenViewStateBindings() diff --git a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenViewModel.swift b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenViewModel.swift index 2003c546f..318953a0b 100644 --- a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenViewModel.swift +++ b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenViewModel.swift @@ -87,17 +87,17 @@ class ThreadTimelineScreenViewModel: ThreadTimelineScreenViewModelType, ThreadTi guard roomProxy.infoPublisher.value.isDM, let dmRecipient = roomProxy.membersPublisher.value.first(where: { $0.userID != roomProxy.ownUserID }), case let .success(userIdentity) = await userSession.clientProxy.userIdentity(for: dmRecipient.userID, fallBackToServer: true) else { - state.dmRecipientVerificationState = .notVerified + state.dmRecipientDetails.verification = .notVerified return } guard let userIdentity else { MXLog.failure("User identity should be known at this point") - state.dmRecipientVerificationState = .notVerified + state.dmRecipientDetails.verification = .notVerified return } - state.dmRecipientVerificationState = userIdentity.verificationState + state.dmRecipientDetails.verification = userIdentity.verificationState } private func updateRoomInfo(_ roomInfo: RoomInfoProxyProtocol) { diff --git a/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift b/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift index 5421f66ce..3ab55300f 100644 --- a/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift +++ b/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift @@ -70,7 +70,7 @@ struct ThreadTimelineScreen: View { RoomHeaderView(roomName: L10n.commonThread, roomSubtitle: context.viewState.roomTitle, roomAvatar: context.viewState.roomAvatar, - dmRecipientVerificationState: context.viewState.dmRecipientVerificationState, + dmRecipientDetails: context.viewState.dmRecipientDetails, roomHistorySharingState: context.viewState.roomHistorySharingState, mediaProvider: context.mediaProvider) { // There is no action but the iOS 26 designs have it looking like a button.