mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Add history sharing badges to room details (#5043)
* chore: Add dependency `SwiftFlow` for use in avatar header badges. Signed-off-by: Skye Elliot <actuallyori@gmail.com> * feat: Display history sharing pill in room details. Signed-off-by: Skye Elliot <actuallyori@gmail.com> * tests: Add tests for history sharing room details pill. Signed-off-by: Skye Elliot <actuallyori@gmail.com> * tests: Update screenshots for RoomDetailsScreen preview tests. * fix: Use Localazy translations for room details badges * fix: Address review comments * fix: Remove line limit from room details badges --------- Signed-off-by: Skye Elliot <actuallyori@gmail.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Compound
|
||||
import Flow
|
||||
import SwiftUI
|
||||
|
||||
struct AvatarHeaderView<Footer: View>: View {
|
||||
@@ -17,6 +18,7 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
|
||||
private enum Badge: Hashable {
|
||||
case encrypted(Bool)
|
||||
case historySharingState(RoomHistorySharingState)
|
||||
case `public`
|
||||
case verified
|
||||
}
|
||||
@@ -57,6 +59,9 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
if room.isPublic {
|
||||
badges.append(.public)
|
||||
}
|
||||
if let state = room.historySharingState {
|
||||
badges.append(.historySharingState(state))
|
||||
}
|
||||
self.badges = badges
|
||||
}
|
||||
|
||||
@@ -129,7 +134,10 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
}
|
||||
|
||||
private var badgesStack: some View {
|
||||
HStack(spacing: 8) {
|
||||
HFlow(horizontalAlignment: .center,
|
||||
verticalAlignment: .top,
|
||||
horizontalSpacing: 8.0,
|
||||
verticalSpacing: 8.0) {
|
||||
ForEach(badges, id: \.self) { badge in
|
||||
switch badge {
|
||||
case .encrypted(true):
|
||||
@@ -148,6 +156,18 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
BadgeLabel(title: L10n.commonVerified,
|
||||
icon: \.verified,
|
||||
style: .accent)
|
||||
case .historySharingState(.hidden):
|
||||
BadgeLabel(title: L10n.cryptoHistorySharingRoomInfoHiddenBadgeContent,
|
||||
icon: \.visibilityOff,
|
||||
style: .info)
|
||||
case .historySharingState(.shared):
|
||||
BadgeLabel(title: L10n.cryptoHistorySharingRoomInfoSharedBadgeContent,
|
||||
icon: \.history,
|
||||
style: .info)
|
||||
case .historySharingState(.worldReadable):
|
||||
BadgeLabel(title: L10n.cryptoHistorySharingRoomInfoWorldReadableBadgeContent,
|
||||
icon: \.userProfileSolid,
|
||||
style: .info)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,7 +253,8 @@ struct AvatarHeaderView_Previews: PreviewProvider, TestablePreview {
|
||||
canonicalAlias: "#test:matrix.org",
|
||||
isEncrypted: true,
|
||||
isPublic: true,
|
||||
isDirect: false),
|
||||
isDirect: false,
|
||||
historySharingState: nil),
|
||||
avatarSize: .room(on: .details),
|
||||
mediaProvider: MediaProviderMock(configuration: .init())) {
|
||||
HStack(spacing: 32) {
|
||||
@@ -279,5 +300,34 @@ struct AvatarHeaderView_Previews: PreviewProvider, TestablePreview {
|
||||
.background(Color.compound.bgSubtleSecondaryLevel0)
|
||||
.previewLayout(.sizeThatFits)
|
||||
.previewDisplayName("Members")
|
||||
|
||||
makeHistorySharingPreview(state: .hidden).previewDisplayName("History Sharing - Hidden")
|
||||
makeHistorySharingPreview(state: .shared).previewDisplayName("History Sharing - Shared")
|
||||
makeHistorySharingPreview(state: .worldReadable).previewDisplayName("History Sharing - World Readable")
|
||||
}
|
||||
|
||||
private static func makeHistorySharingPreview(state: RoomHistorySharingState) -> some View {
|
||||
Form {
|
||||
AvatarHeaderView(room: .init(id: "@test:matrix.org",
|
||||
name: "Test Room",
|
||||
avatar: .room(id: "@test:matrix.org",
|
||||
name: "Test Room",
|
||||
avatarURL: .mockMXCAvatar),
|
||||
canonicalAlias: "#test:matrix.org",
|
||||
isEncrypted: true,
|
||||
isPublic: true,
|
||||
isDirect: false,
|
||||
historySharingState: state),
|
||||
avatarSize: .room(on: .details),
|
||||
mediaProvider: MediaProviderMock(configuration: .init())) {
|
||||
HStack(spacing: 32) {
|
||||
ShareLink(item: "test") {
|
||||
CompoundIcon(\.shareIos)
|
||||
}
|
||||
.buttonStyle(FormActionButtonStyle(title: "Test"))
|
||||
}
|
||||
.padding(.top, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,9 @@ struct BadgeLabel_Previews: PreviewProvider, TestablePreview {
|
||||
BadgeLabel(title: "1234",
|
||||
icon: \.userProfile,
|
||||
style: .default)
|
||||
BadgeLabel(title: "Very long text that potentially will wrap around in constrained environments, maybe into two or three lines, depending on the exact length of the text",
|
||||
icon: \.userProfile,
|
||||
style: .default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +82,9 @@ struct RoomHeaderView: View {
|
||||
|
||||
private var historySharingIcon: KeyPath<CompoundIcons, Image>? {
|
||||
switch roomHistorySharingState {
|
||||
case .none, .hidden: nil
|
||||
case .shared: \.history
|
||||
case .worldReadable: \.userProfileSolid
|
||||
case .none: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,13 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
|
||||
let topic = attributedStringBuilder.fromPlain(roomProxy.infoPublisher.value.topic)
|
||||
|
||||
super.init(initialViewState: .init(details: roomProxy.details,
|
||||
// Clear details.historySharingState manually while we are still behind a feature flag.
|
||||
var details = roomProxy.details
|
||||
if !appSettings.enableKeyShareOnInvite {
|
||||
details.historySharingState = nil
|
||||
}
|
||||
|
||||
super.init(initialViewState: .init(details: details,
|
||||
isEncrypted: roomProxy.infoPublisher.value.isEncrypted,
|
||||
isDirect: roomProxy.infoPublisher.value.isDirect,
|
||||
topic: topic,
|
||||
@@ -275,6 +281,14 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
|
||||
state.details = roomProxy.details
|
||||
|
||||
// Set state.details.historySharingState manually while we are still behind
|
||||
// a feature flag.
|
||||
if appSettings.enableKeyShareOnInvite {
|
||||
state.details.historySharingState = roomInfo.historySharingState
|
||||
} else {
|
||||
state.details.historySharingState = nil
|
||||
}
|
||||
|
||||
let topic = attributedStringBuilder.fromPlain(roomInfo.topic)
|
||||
state.topic = topic
|
||||
state.topicSummary = topic?.unattributedStringByReplacingNewlinesWithSpaces()
|
||||
|
||||
@@ -328,17 +328,24 @@ struct RoomDetailsScreen: View {
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let genericRoomViewModel = makeGenericRoomViewModel()
|
||||
static let genericWorldReadableRoomViewModel = makeGenericRoomViewModel(historyVisibility: .worldReadable)
|
||||
static let genericJoinedRoomViewModel = makeGenericRoomViewModel(historyVisibility: .joined)
|
||||
static let simpleRoomViewModel = makeSimpleRoomViewModel()
|
||||
static let dmRoomViewModel = makeDMViewModel(verificationState: .notVerified)
|
||||
static let dmRoomVerifiedViewModel = makeDMViewModel(verificationState: .verified)
|
||||
static let dmRoomVerificationViolationViewModel = makeDMViewModel(verificationState: .verificationViolation)
|
||||
|
||||
static var previews: some View {
|
||||
RoomDetailsScreen(context: genericRoomViewModel.context)
|
||||
.snapshotPreferences(expect: genericRoomViewModel.context.observe(\.viewState.permalink).map { $0 != nil })
|
||||
.previewDisplayName("Generic Room")
|
||||
RoomDetailsScreen(context: genericJoinedRoomViewModel.context)
|
||||
.snapshotPreferences(expect: genericJoinedRoomViewModel.context.observe(\.viewState.permalink).map { $0 != nil })
|
||||
.previewDisplayName("Generic Room - Joined History Visibility")
|
||||
|
||||
RoomDetailsScreen(context: genericWorldReadableRoomViewModel.context)
|
||||
.snapshotPreferences(expect: genericWorldReadableRoomViewModel.context.observe(\.viewState.permalink).map { $0 != nil })
|
||||
.previewDisplayName("Generic Room - World Readable History Visibility")
|
||||
|
||||
RoomDetailsScreen(context: simpleRoomViewModel.context)
|
||||
.snapshotPreferences(expect: simpleRoomViewModel.context.observe(\.viewState.permalink).map { $0 != nil })
|
||||
@@ -357,7 +364,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
.previewDisplayName("DM Room Verification Violation")
|
||||
}
|
||||
|
||||
private static func makeGenericRoomViewModel() -> RoomDetailsScreenViewModel {
|
||||
private static func makeGenericRoomViewModel(historyVisibility: RoomHistoryVisibility) -> RoomDetailsScreenViewModel {
|
||||
ServiceLocator.shared.settings.enableKeyShareOnInvite = true
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
let knockRequests: [KnockRequestProxyMock] = [.init()]
|
||||
|
||||
@@ -381,6 +389,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
isDirect: false,
|
||||
isEncrypted: true,
|
||||
canonicalAlias: "#alias:domain.com",
|
||||
historyVisibility: historyVisibility,
|
||||
members: members,
|
||||
knockRequestsState: .loaded(knockRequests),
|
||||
joinRule: .knock))
|
||||
@@ -400,6 +409,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
}
|
||||
|
||||
private static func makeSimpleRoomViewModel() -> RoomDetailsScreenViewModel {
|
||||
ServiceLocator.shared.settings.enableKeyShareOnInvite = true
|
||||
ServiceLocator.shared.settings.knockingEnabled = true
|
||||
let knockRequests: [KnockRequestProxyMock] = [.init()]
|
||||
|
||||
@@ -429,6 +439,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
}
|
||||
|
||||
private static func makeDMViewModel(verificationState: UserIdentityVerificationState) -> RoomDetailsScreenViewModel {
|
||||
ServiceLocator.shared.settings.enableKeyShareOnInvite = true
|
||||
|
||||
let members: [RoomMemberProxyMock] = [
|
||||
.mockMe,
|
||||
.mockDan
|
||||
|
||||
@@ -16,4 +16,5 @@ struct RoomDetails {
|
||||
let isEncrypted: Bool
|
||||
let isPublic: Bool
|
||||
let isDirect: Bool
|
||||
var historySharingState: RoomHistorySharingState?
|
||||
}
|
||||
|
||||
@@ -8,8 +8,13 @@
|
||||
/// 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`.
|
||||
/// The feature flag is set, and the room history visibility is either `invited` or `joined`. New
|
||||
/// members of the room cannot read the room history.
|
||||
case hidden
|
||||
/// The feature flag is set, and the room history visibility is set to `shared`. New members of the
|
||||
/// room can read the room history.
|
||||
case shared
|
||||
/// The feature flag is set, and the room history visibility is set to `world_readable`.
|
||||
/// The feature flag is set, and the room history visibility is set to `world_readable`. Anyone
|
||||
/// can read the room history.
|
||||
case worldReadable
|
||||
}
|
||||
|
||||
@@ -142,11 +142,13 @@ extension RoomInfoProxyProtocol {
|
||||
return nil
|
||||
}
|
||||
return switch historyVisibility {
|
||||
case .joined, .invited:
|
||||
.hidden
|
||||
case .shared:
|
||||
.shared
|
||||
case .worldReadable:
|
||||
.worldReadable
|
||||
default:
|
||||
case .custom:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,13 +196,20 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
|
||||
|
||||
extension JoinedRoomProxyProtocol {
|
||||
var details: RoomDetails {
|
||||
RoomDetails(id: id,
|
||||
name: infoPublisher.value.displayName,
|
||||
avatar: infoPublisher.value.avatar,
|
||||
canonicalAlias: infoPublisher.value.canonicalAlias,
|
||||
isEncrypted: infoPublisher.value.isEncrypted,
|
||||
isPublic: !(infoPublisher.value.isPrivate ?? false),
|
||||
isDirect: infoPublisher.value.isDirect)
|
||||
let historySharingState: RoomHistorySharingState? = if infoPublisher.value.isEncrypted {
|
||||
infoPublisher.value.historySharingState
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
return RoomDetails(id: id,
|
||||
name: infoPublisher.value.displayName,
|
||||
avatar: infoPublisher.value.avatar,
|
||||
canonicalAlias: infoPublisher.value.canonicalAlias,
|
||||
isEncrypted: infoPublisher.value.isEncrypted,
|
||||
isPublic: !(infoPublisher.value.isPrivate ?? false),
|
||||
isDirect: infoPublisher.value.isDirect,
|
||||
historySharingState: historySharingState)
|
||||
}
|
||||
|
||||
var isDirectOneToOneRoom: Bool {
|
||||
|
||||
Reference in New Issue
Block a user