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:
Skye Elliot
2026-02-11 12:22:39 +00:00
committed by GitHub
parent 992cec91d6
commit 4841a3bdaf
57 changed files with 333 additions and 67 deletions
@@ -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 {
@@ -266,6 +266,14 @@
<key>Type</key>
<string>PSChildPaneSpecifier</string>
</dict>
<dict>
<key>File</key>
<string>Packages/Flow</string>
<key>Title</key>
<string>Flow</string>
<key>Type</key>
<string>PSChildPaneSpecifier</string>
</dict>
<dict>
<key>File</key>
<string>Packages/swiftui-introspect</string>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreferenceSpecifiers</key>
<array>
<dict>
<key>FooterText</key>
<string>MIT License
Copyright (c) 2023 Laszlo Teveli
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
</array>
</dict>
</plist>
+2
View File
@@ -249,6 +249,8 @@ targets:
- package: WysiwygComposer
- package: SwiftOGG
- package: SwiftSoup
- package: SwiftFlow
product: Flow
- package: DSWaveformImage
product: DSWaveformImageViews