mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
UI update for room details (#2831)
This commit is contained in:
@@ -710,6 +710,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateMachine.tryEvent(.presentPollsHistory)
|
||||
case .presentRolesAndPermissionsScreen:
|
||||
stateMachine.tryEvent(.presentRolesAndPermissionsScreen)
|
||||
case .presentCall:
|
||||
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -1219,6 +1219,8 @@ internal enum L10n {
|
||||
internal static var screenQrCodeLoginDeviceCodeSubtitle: String { return L10n.tr("Localizable", "screen_qr_code_login_device_code_subtitle") }
|
||||
/// Enter the number below on your other device
|
||||
internal static var screenQrCodeLoginDeviceCodeTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_device_code_title") }
|
||||
/// Ready to scan
|
||||
internal static var screenQrCodeLoginInitialStateButtonTitle: String { return L10n.tr("Localizable", "screen_qr_code_login_initial_state_button_title") }
|
||||
/// Open %1$@ on a desktop device
|
||||
internal static func screenQrCodeLoginInitialStateItem1(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_qr_code_login_initial_state_item_1", String(describing: p1))
|
||||
@@ -1231,7 +1233,7 @@ internal enum L10n {
|
||||
}
|
||||
/// “Link new device”
|
||||
internal static var screenQrCodeLoginInitialStateItem3Action: String { return L10n.tr("Localizable", "screen_qr_code_login_initial_state_item_3_action") }
|
||||
/// Follow the instructions shown
|
||||
/// Scan the QR code with this device
|
||||
internal static var screenQrCodeLoginInitialStateItem4: String { return L10n.tr("Localizable", "screen_qr_code_login_initial_state_item_4") }
|
||||
/// Open %1$@ on another device to get the QR code
|
||||
internal static func screenQrCodeLoginInitialStateTitle(_ p1: Any) -> String {
|
||||
@@ -1409,6 +1411,12 @@ internal enum L10n {
|
||||
internal static var screenRoomDetailsAlreadyAMember: String { return L10n.tr("Localizable", "screen_room_details_already_a_member") }
|
||||
/// Already invited
|
||||
internal static var screenRoomDetailsAlreadyInvited: String { return L10n.tr("Localizable", "screen_room_details_already_invited") }
|
||||
/// Encrypted
|
||||
internal static var screenRoomDetailsBadgeEncrypted: String { return L10n.tr("Localizable", "screen_room_details_badge_encrypted") }
|
||||
/// Not encrypted
|
||||
internal static var screenRoomDetailsBadgeNotEncrypted: String { return L10n.tr("Localizable", "screen_room_details_badge_not_encrypted") }
|
||||
/// Public room
|
||||
internal static var screenRoomDetailsBadgePublic: String { return L10n.tr("Localizable", "screen_room_details_badge_public") }
|
||||
/// Edit Room
|
||||
internal static var screenRoomDetailsEditRoomTitle: String { return L10n.tr("Localizable", "screen_room_details_edit_room_title") }
|
||||
/// There was an unknown error and the information couldn't be changed.
|
||||
|
||||
@@ -17,10 +17,16 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AvatarHeaderView<Footer: View>: View {
|
||||
private enum Badge: Hashable {
|
||||
case encrypted(Bool)
|
||||
case `public`
|
||||
}
|
||||
|
||||
let id: String
|
||||
let name: String?
|
||||
let subtitle: String?
|
||||
let avatarURL: URL?
|
||||
private let badges: [Badge]
|
||||
|
||||
let avatarSize: AvatarSize
|
||||
let imageProvider: ImageProviderProtocol?
|
||||
@@ -41,6 +47,13 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
self.imageProvider = imageProvider
|
||||
self.onAvatarTap = onAvatarTap
|
||||
self.footer = footer
|
||||
|
||||
var badges = [Badge]()
|
||||
badges.append(.encrypted(room.isEncrypted))
|
||||
if room.isPublic {
|
||||
badges.append(.public)
|
||||
}
|
||||
self.badges = badges
|
||||
}
|
||||
|
||||
init(member: RoomMemberDetails,
|
||||
@@ -57,6 +70,7 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
self.imageProvider = imageProvider
|
||||
self.onAvatarTap = onAvatarTap
|
||||
self.footer = footer
|
||||
badges = []
|
||||
}
|
||||
|
||||
init(user: UserProfileProxy,
|
||||
@@ -73,6 +87,30 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
self.imageProvider = imageProvider
|
||||
self.onAvatarTap = onAvatarTap
|
||||
self.footer = footer
|
||||
badges = []
|
||||
}
|
||||
|
||||
private var badgesStack: some View {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(badges, id: \.self) { badge in
|
||||
switch badge {
|
||||
case .encrypted(let isEncrypted):
|
||||
if isEncrypted {
|
||||
BadgeLabel(title: L10n.screenRoomDetailsBadgeEncrypted,
|
||||
icon: \.lockSolid,
|
||||
isHighlighted: true)
|
||||
} else {
|
||||
BadgeLabel(title: L10n.screenRoomDetailsBadgeNotEncrypted,
|
||||
icon: \.lockOff,
|
||||
isHighlighted: false)
|
||||
}
|
||||
case .public:
|
||||
BadgeLabel(title: L10n.screenRoomDetailsBadgePublic,
|
||||
icon: \.public,
|
||||
isHighlighted: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -102,6 +140,10 @@ struct AvatarHeaderView<Footer: View>: View {
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
||||
if !badges.isEmpty {
|
||||
badgesStack
|
||||
}
|
||||
|
||||
footer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
@@ -119,7 +161,9 @@ struct AvatarHeaderView_Previews: PreviewProvider, TestablePreview {
|
||||
AvatarHeaderView(room: .init(id: "@test:matrix.org",
|
||||
name: "Test Room",
|
||||
avatarURL: URL.picturesDirectory,
|
||||
canonicalAlias: "#test:matrix.org"),
|
||||
canonicalAlias: "#test:matrix.org",
|
||||
isEncrypted: true,
|
||||
isPublic: true),
|
||||
avatarSize: .room(on: .details),
|
||||
imageProvider: MockMediaProvider()) {
|
||||
HStack(spacing: 32) {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct BadgeLabel: View {
|
||||
let title: String
|
||||
let icon: KeyPath<CompoundIcons, Image>
|
||||
let isHighlighted: Bool
|
||||
|
||||
var body: some View {
|
||||
Label(title,
|
||||
icon: icon,
|
||||
iconSize: .xSmall,
|
||||
relativeTo: .compound.bodySM)
|
||||
.labelStyle(BadgeLabelStyle(isHighlighted: isHighlighted))
|
||||
}
|
||||
}
|
||||
|
||||
private struct BadgeLabelStyle: LabelStyle {
|
||||
let isHighlighted: Bool
|
||||
|
||||
var titleColor: Color {
|
||||
isHighlighted ? .compound._badgeTextSuccess : .compound._badgeTextSubtle
|
||||
}
|
||||
|
||||
var iconColor: Color {
|
||||
isHighlighted ? .compound.iconSuccessPrimary : .compound.iconSecondary
|
||||
}
|
||||
|
||||
var backgroundColor: Color {
|
||||
isHighlighted ? .compound._bgBadgeSuccess : .compound.bgSubtlePrimary
|
||||
}
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
HStack(spacing: 4) {
|
||||
configuration.icon
|
||||
.foregroundStyle(iconColor)
|
||||
configuration.title
|
||||
.foregroundStyle(titleColor)
|
||||
}
|
||||
.font(.compound.bodySM)
|
||||
.padding(.leading, 8)
|
||||
.padding(.trailing, 12)
|
||||
.padding(.vertical, 4)
|
||||
.background(Capsule().fill(backgroundColor))
|
||||
}
|
||||
}
|
||||
|
||||
struct BadgeLabel_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 10) {
|
||||
BadgeLabel(title: "Encrypted",
|
||||
icon: \.lockSolid,
|
||||
isHighlighted: true)
|
||||
BadgeLabel(title: "Not encrypted",
|
||||
icon: \.lockSolid,
|
||||
isHighlighted: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,12 +56,7 @@ struct JoinRoomScreen: View {
|
||||
}
|
||||
|
||||
if let memberCount = context.viewState.roomDetails?.memberCount {
|
||||
Label("\(memberCount)", icon: \.userProfile, iconSize: .small, relativeTo: .compound.bodySMSemibold)
|
||||
.font(.compound.bodySMSemibold)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.padding(4)
|
||||
.background(Capsule().fill(.compound.bgSubtleSecondary))
|
||||
.labelStyle(.custom(spacing: 4))
|
||||
BadgeLabel(title: "\(memberCount)", icon: \.userProfile, isHighlighted: false)
|
||||
}
|
||||
|
||||
if let topic = context.viewState.roomDetails?.topic {
|
||||
|
||||
@@ -35,6 +35,7 @@ enum RoomDetailsScreenCoordinatorAction {
|
||||
case presentInviteUsersScreen
|
||||
case presentPollsHistory
|
||||
case presentRolesAndPermissionsScreen
|
||||
case presentCall
|
||||
}
|
||||
|
||||
final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -79,6 +80,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.presentPollsHistory)
|
||||
case .requestRolesAndPermissionsPresentation:
|
||||
actionsSubject.send(.presentRolesAndPermissionsScreen)
|
||||
case .startCall:
|
||||
actionsSubject.send(.presentCall)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -29,6 +29,7 @@ enum RoomDetailsScreenViewModelAction {
|
||||
case requestEditDetailsPresentation
|
||||
case requestPollsHistoryPresentation
|
||||
case requestRolesAndPermissionsPresentation
|
||||
case startCall
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
@@ -50,6 +51,7 @@ struct RoomDetailsScreenViewState: BindableState {
|
||||
var canEditRoomAvatar = false
|
||||
var canEditRolesOrPermissions = false
|
||||
var notificationSettingsState: RoomDetailsNotificationSettingsState = .loading
|
||||
var canJoinCall = false
|
||||
|
||||
var canEdit: Bool {
|
||||
!isDirect && (canEditRoomName || canEditRoomTopic || canEditRoomAvatar)
|
||||
@@ -65,6 +67,12 @@ struct RoomDetailsScreenViewState: BindableState {
|
||||
|
||||
var shortcuts: [RoomDetailsScreenViewShortcut] {
|
||||
var shortcuts: [RoomDetailsScreenViewShortcut] = [.mute]
|
||||
if !ProcessInfo.processInfo.isiOSAppOnMac, canJoinCall {
|
||||
shortcuts.append(.call)
|
||||
}
|
||||
if dmRecipient == nil, canInviteUsers {
|
||||
shortcuts.append(.invite)
|
||||
}
|
||||
if let permalink = dmRecipient?.permalink {
|
||||
shortcuts.append(.share(link: permalink))
|
||||
} else if let permalink {
|
||||
@@ -184,11 +192,14 @@ enum RoomDetailsScreenViewAction {
|
||||
case processTapPolls
|
||||
case toggleFavourite(isFavourite: Bool)
|
||||
case processTapRolesAndPermissions
|
||||
case processTapCall
|
||||
}
|
||||
|
||||
enum RoomDetailsScreenViewShortcut {
|
||||
case share(link: URL)
|
||||
case mute
|
||||
case call
|
||||
case invite
|
||||
}
|
||||
|
||||
extension RoomDetailsScreenViewShortcut: Hashable { }
|
||||
|
||||
@@ -63,6 +63,13 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
bindings: .init()),
|
||||
imageProvider: mediaProvider)
|
||||
|
||||
Task {
|
||||
let userID = roomProxy.ownUserID
|
||||
if case let .success(permission) = await roomProxy.canUserJoinCall(userID: userID) {
|
||||
state.canJoinCall = permission
|
||||
}
|
||||
}
|
||||
|
||||
Task {
|
||||
if case let .success(permalinkURL) = await roomProxy.matrixToPermalink() {
|
||||
state.permalink = permalinkURL
|
||||
@@ -126,6 +133,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
||||
Task { await toggleFavourite(isFavourite) }
|
||||
case .processTapRolesAndPermissions:
|
||||
actionsSubject.send(.requestRolesAndPermissionsPresentation)
|
||||
case .processTapCall:
|
||||
actionsSubject.send(.startCall)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,20 @@ struct RoomDetailsScreen: View {
|
||||
CompoundIcon(\.shareIos)
|
||||
}
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionShare))
|
||||
case .call:
|
||||
Button {
|
||||
context.send(viewAction: .processTapCall)
|
||||
} label: {
|
||||
CompoundIcon(\.videoCall)
|
||||
}
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionCall))
|
||||
case .invite:
|
||||
Button {
|
||||
context.send(viewAction: .processTapInvite)
|
||||
} label: {
|
||||
CompoundIcon(\.userAdd)
|
||||
}
|
||||
.buttonStyle(FormActionButtonStyle(title: L10n.actionInvite))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,15 +166,6 @@ struct RoomDetailsScreen: View {
|
||||
context.send(viewAction: .processTapPeople)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people)
|
||||
|
||||
if context.viewState.canInviteUsers {
|
||||
ListRow(label: .default(title: L10n.screenRoomDetailsInvitePeopleTitle,
|
||||
icon: \.userAdd),
|
||||
kind: .navigationLink {
|
||||
context.send(viewAction: .processTapInvite)
|
||||
})
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.invite)
|
||||
}
|
||||
}
|
||||
ListRow(label: .default(title: L10n.screenPollsHistoryTitle,
|
||||
icon: \.polls),
|
||||
|
||||
@@ -21,4 +21,6 @@ struct RoomDetails {
|
||||
let name: String?
|
||||
let avatarURL: URL?
|
||||
let canonicalAlias: String?
|
||||
let isEncrypted: Bool
|
||||
let isPublic: Bool
|
||||
}
|
||||
|
||||
@@ -141,7 +141,9 @@ extension RoomProxyProtocol {
|
||||
RoomDetails(id: id,
|
||||
name: name,
|
||||
avatarURL: avatarURL,
|
||||
canonicalAlias: canonicalAlias)
|
||||
canonicalAlias: canonicalAlias,
|
||||
isEncrypted: isEncrypted,
|
||||
isPublic: isPublic)
|
||||
}
|
||||
|
||||
// Avoids to duplicate the same logic around in the app
|
||||
|
||||
Reference in New Issue
Block a user