mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Display an icon in the room header for rooms with shared history (#5016)
* feat: Display an icon in the room header for rooms with shared history * fix: Apply suggestions from code review - Simplifies `isRoomHistoryShared` expressions using type inferrence. - Adds failure messages to unit test. Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> * fix: Remove extension method in favour of field on configuration. * fix: Distinguish between `shared` and `worldReadable` icons. * refactor: Use `RoomHistorySharingState` enum with extension methods --------- Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
@@ -27,6 +27,7 @@ struct JoinedRoomProxyMockConfiguration {
|
||||
var canonicalAlias: String?
|
||||
var alternativeAliases: [String] = []
|
||||
var pinnedEventIDs: Set<String> = []
|
||||
var historyVisibility: RoomHistoryVisibility = .shared
|
||||
|
||||
var timelineStartReached = false
|
||||
|
||||
@@ -190,7 +191,7 @@ extension RoomInfoProxyMock {
|
||||
unreadMentionsCount = 0
|
||||
pinnedEventIDs = configuration.pinnedEventIDs
|
||||
joinRule = configuration.joinRule
|
||||
historyVisibility = .shared
|
||||
historyVisibility = configuration.historyVisibility
|
||||
|
||||
powerLevels = RoomPowerLevelsProxyMock(configuration: configuration.powerLevelsConfiguration)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ struct RoomHeaderView: View {
|
||||
var roomSubtitle: String?
|
||||
let roomAvatar: RoomAvatar
|
||||
var dmRecipientVerificationState: UserIdentityVerificationState?
|
||||
var roomHistorySharingState: RoomHistorySharingState?
|
||||
|
||||
let mediaProvider: MediaProviderProtocol?
|
||||
|
||||
@@ -63,6 +64,11 @@ struct RoomHeaderView: View {
|
||||
if let dmRecipientVerificationState {
|
||||
VerificationBadge(verificationState: dmRecipientVerificationState, size: .xSmall, relativeTo: .compound.bodyMDSemibold)
|
||||
}
|
||||
|
||||
if let historySharingIcon {
|
||||
CompoundIcon(historySharingIcon, size: .xSmall, relativeTo: .compound.bodyMDSemibold)
|
||||
.foregroundStyle(.compound.iconInfoPrimary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,6 +79,14 @@ struct RoomHeaderView: View {
|
||||
mediaProvider: mediaProvider)
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomScreen.avatar)
|
||||
}
|
||||
|
||||
private var historySharingIcon: KeyPath<CompoundIcons, Image>? {
|
||||
switch roomHistorySharingState {
|
||||
case .shared: \.history
|
||||
case .worldReadable: \.userProfileSolid
|
||||
case .none: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomHeaderView {
|
||||
@@ -106,19 +120,26 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview {
|
||||
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)
|
||||
}
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
|
||||
static func makeHeader(avatarURL: URL?,
|
||||
roomSubtitle: String? = nil,
|
||||
verificationState: UserIdentityVerificationState) -> some View {
|
||||
verificationState: UserIdentityVerificationState,
|
||||
historySharingState: RoomHistorySharingState? = nil) -> some View {
|
||||
RoomHeaderView(roomName: "Some Room name",
|
||||
roomSubtitle: roomSubtitle,
|
||||
roomAvatar: .room(id: "1",
|
||||
name: "Some Room Name",
|
||||
avatarURL: avatarURL),
|
||||
dmRecipientVerificationState: verificationState,
|
||||
roomHistorySharingState: historySharingState,
|
||||
|
||||
mediaProvider: MediaProviderMock(configuration: .init())) { }
|
||||
.padding()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import OrderedCollections
|
||||
|
||||
enum RoomScreenViewModelAction: Equatable {
|
||||
@@ -80,6 +81,9 @@ struct RoomScreenViewState: BindableState {
|
||||
(canAcceptKnocks || canDeclineKnocks || canBan)
|
||||
}
|
||||
|
||||
/// If `enableKeyShareOnInvite` is set, determines the current history sharing state.
|
||||
var roomHistorySharingState: RoomHistorySharingState?
|
||||
|
||||
var footerDetails: RoomScreenFooterViewDetails?
|
||||
|
||||
var bindings = RoomScreenViewStateBindings()
|
||||
|
||||
@@ -67,10 +67,17 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
self.initialSelectedPinnedEventID = initialSelectedPinnedEventID
|
||||
pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID)
|
||||
|
||||
let roomHistorySharingState: RoomHistorySharingState? = if appSettings.enableKeyShareOnInvite {
|
||||
roomProxy.infoPublisher.value.historySharingState
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
let viewState = RoomScreenViewState(roomTitle: roomProxy.infoPublisher.value.displayName ?? roomProxy.id,
|
||||
roomAvatar: roomProxy.infoPublisher.value.avatar,
|
||||
hasOngoingCall: roomProxy.infoPublisher.value.hasRoomCall,
|
||||
hasSuccessor: roomProxy.infoPublisher.value.successor != nil)
|
||||
hasSuccessor: roomProxy.infoPublisher.value.successor != nil,
|
||||
roomHistorySharingState: roomHistorySharingState)
|
||||
super.init(initialViewState: appHooks.roomScreenHook.update(viewState),
|
||||
mediaProvider: userSession.mediaProvider)
|
||||
|
||||
@@ -342,6 +349,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
state.canDeclineKnocks = powerLevels.canOwnUserKick()
|
||||
state.canBan = powerLevels.canOwnUserBan()
|
||||
}
|
||||
|
||||
// This causes the UI to become inconsistent with the user's mental model if the user
|
||||
// does not restart the app after disabling the feature flag. We can probably ignore
|
||||
// such cases, since we explicitly ask for an app restart in the caption of the feature
|
||||
// flag switch.
|
||||
if appSettings.enableKeyShareOnInvite {
|
||||
state.roomHistorySharingState = roomInfo.historySharingState
|
||||
}
|
||||
}
|
||||
|
||||
private func setupPinnedEventsTimelineItemProviderIfNeeded() {
|
||||
|
||||
@@ -157,6 +157,7 @@ struct RoomScreen: View {
|
||||
RoomHeaderView(roomName: context.viewState.roomTitle,
|
||||
roomAvatar: context.viewState.roomAvatar,
|
||||
dmRecipientVerificationState: context.viewState.dmRecipientVerificationState,
|
||||
roomHistorySharingState: context.viewState.roomHistorySharingState,
|
||||
mediaProvider: context.mediaProvider) {
|
||||
context.send(viewAction: .displayRoomDetails)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ struct ThreadTimelineScreenViewState: BindableState {
|
||||
var roomAvatar: RoomAvatar
|
||||
var canSendMessage = true
|
||||
var dmRecipientVerificationState: UserIdentityVerificationState?
|
||||
var roomHistorySharingState: RoomHistorySharingState?
|
||||
|
||||
var bindings = ThreadTimelineScreenViewStateBindings()
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ struct ThreadTimelineScreen: View {
|
||||
roomSubtitle: context.viewState.roomTitle,
|
||||
roomAvatar: context.viewState.roomAvatar,
|
||||
dmRecipientVerificationState: context.viewState.dmRecipientVerificationState,
|
||||
roomHistorySharingState: context.viewState.roomHistorySharingState,
|
||||
mediaProvider: context.mediaProvider) {
|
||||
// There is no action but the iOS 26 designs have it looking like a button.
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
/// Enumeration of the two possible cases in which history sharing under MSC4268 is enabled. These
|
||||
/// variants implicitly assume that the feature flag, `enableKeyShareOnInvite`, is set.
|
||||
enum RoomHistorySharingState: Equatable {
|
||||
/// The feature flag is set, and the room history visibility is set to `shared`.
|
||||
case shared
|
||||
/// The feature flag is set, and the room history visibility is set to `world_readable`.
|
||||
case worldReadable
|
||||
}
|
||||
@@ -134,4 +134,20 @@ extension RoomInfoProxyProtocol {
|
||||
// And finally return whatever the first alternative alias is
|
||||
return alternativeAliases.first
|
||||
}
|
||||
|
||||
/// If present, the state of history sharing in this room. This *does not* consider the `enableKeyShareOnInvite`
|
||||
/// feature flag, so consumers should be careful to check the flag is true before utilising this property.
|
||||
var historySharingState: RoomHistorySharingState? {
|
||||
guard isEncrypted else {
|
||||
return nil
|
||||
}
|
||||
return switch historyVisibility {
|
||||
case .shared:
|
||||
.shared
|
||||
case .worldReadable:
|
||||
.worldReadable
|
||||
default:
|
||||
nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user