mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Space Settings UI tweaks (#4678)
* design tweaks for the space settings flow * pr suggestions * update proj
This commit is contained in:
@@ -1322,6 +1322,8 @@ internal enum L10n {
|
||||
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_description") }
|
||||
/// Are you sure you want to ban this member?
|
||||
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_title") }
|
||||
/// They won’t be able to join this space again if invited, but they’ll still keep their memberships of any rooms or subspaces.
|
||||
internal static var screenBottomSheetManageRoomMemberBanMemberFromSpaceConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_from_space_confirmation_description") }
|
||||
/// Banning %1$@
|
||||
internal static func screenBottomSheetManageRoomMemberBanningUser(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_banning_user", String(describing: p1))
|
||||
@@ -1332,6 +1334,8 @@ internal enum L10n {
|
||||
internal static var screenBottomSheetManageRoomMemberKickMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_confirmation_description") }
|
||||
/// Are you sure you want to remove this member?
|
||||
internal static var screenBottomSheetManageRoomMemberKickMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_confirmation_title") }
|
||||
/// They will be able to join this space again if invited, and they’ll still keep their memberships of any rooms or subspaces.
|
||||
internal static var screenBottomSheetManageRoomMemberKickMemberFromSpaceConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_from_space_confirmation_description") }
|
||||
/// View profile
|
||||
internal static var screenBottomSheetManageRoomMemberMemberUserInfo: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_member_user_info") }
|
||||
/// Remove user
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
/// A button that contains text that is copied on tap
|
||||
struct CopyTextButton: View {
|
||||
let content: String
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
UIPasteboard.general.string = content
|
||||
} label: {
|
||||
Label {
|
||||
Text(content)
|
||||
.lineLimit(1)
|
||||
} icon: {
|
||||
CompoundIcon(\.copy, size: .small, relativeTo: .compound.bodyLG)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.labelStyle(.custom(spacing: 4, iconLayout: .trailing))
|
||||
}
|
||||
.accessibilityHint(L10n.actionCopy)
|
||||
}
|
||||
}
|
||||
|
||||
struct CopyTextButton_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
CopyTextButton(content: "Copy me!")
|
||||
.previewLayout(.sizeThatFits)
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ enum TestablePreviewsDictionary {
|
||||
"CollapsibleRoomTimelineView_Previews" : CollapsibleRoomTimelineView_Previews.self,
|
||||
"CompletionSuggestion_Previews" : CompletionSuggestion_Previews.self,
|
||||
"ComposerToolbar_Previews" : ComposerToolbar_Previews.self,
|
||||
"CopyTextButton_Previews" : CopyTextButton_Previews.self,
|
||||
"CreateRoom_Previews" : CreateRoom_Previews.self,
|
||||
"DeactivateAccountScreen_Previews" : DeactivateAccountScreen_Previews.self,
|
||||
"DeclineAndBlockScreen_Previews" : DeclineAndBlockScreen_Previews.self,
|
||||
|
||||
@@ -59,7 +59,7 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
|
||||
case .kick:
|
||||
state.bindings.alertInfo = .init(id: alertType,
|
||||
title: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationTitle,
|
||||
message: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationDescription,
|
||||
message: roomProxy.infoPublisher.value.isSpace ? L10n.screenBottomSheetManageRoomMemberKickMemberFromSpaceConfirmationDescription : L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationDescription,
|
||||
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },
|
||||
secondaryButton: .init(title: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationAction) { [weak self] in Task { await self?.kickMember(id: memberID, name: memberName, reason: reason) } },
|
||||
textFields: [.init(placeholder: L10n.commonReason,
|
||||
@@ -69,7 +69,7 @@ class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, Manage
|
||||
case .ban:
|
||||
state.bindings.alertInfo = .init(id: alertType,
|
||||
title: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationTitle,
|
||||
message: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationDescription,
|
||||
message: roomProxy.infoPublisher.value.isSpace ? L10n.screenBottomSheetManageRoomMemberBanMemberFromSpaceConfirmationDescription : L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationDescription,
|
||||
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },
|
||||
secondaryButton: .init(title: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationAction) { [weak self] in Task { await self?.banMember(id: memberID, name: memberName, reason: reason) } },
|
||||
textFields: [.init(placeholder: L10n.commonReason,
|
||||
|
||||
@@ -28,6 +28,10 @@ struct SpaceHeaderView: View {
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
if let alias = spaceRoomProxy.canonicalAlias {
|
||||
CopyTextButton(content: alias)
|
||||
}
|
||||
|
||||
spaceDetails
|
||||
|
||||
JoinedMembersBadgeView(heroes: spaceRoomProxy.heroes,
|
||||
@@ -62,7 +66,7 @@ struct SpaceHeaderView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var spaceDetails: some View {
|
||||
private var spaceDetails: some View {
|
||||
Label {
|
||||
Text(spaceDetailsVisibilityTitle)
|
||||
.font(.compound.bodyLG)
|
||||
@@ -74,7 +78,7 @@ struct SpaceHeaderView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var spaceDetailsVisibilityTitle: String {
|
||||
private var spaceDetailsVisibilityTitle: String {
|
||||
switch spaceRoomProxy.visibility {
|
||||
case .public: L10n.commonPublicSpace
|
||||
case .private: L10n.commonPrivateSpace
|
||||
@@ -83,7 +87,7 @@ struct SpaceHeaderView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var spaceDetailsVisibilityIcon: KeyPath<CompoundIcons, Image> {
|
||||
private var spaceDetailsVisibilityIcon: KeyPath<CompoundIcons, Image> {
|
||||
switch spaceRoomProxy.visibility {
|
||||
case .public: \.public
|
||||
case .private: \.lock
|
||||
@@ -122,6 +126,7 @@ struct SpaceHeaderView_Previews: PreviewProvider, TestablePreview {
|
||||
childrenCount: 20,
|
||||
joinedMembersCount: 78,
|
||||
topic: "Description of the space goes right here.",
|
||||
canonicalAlias: "#space:matrix.org",
|
||||
joinRule: .public)),
|
||||
SpaceRoomProxyMock(.init(id: "!space3:matrix.org",
|
||||
name: "Subspace",
|
||||
@@ -135,6 +140,7 @@ struct SpaceHeaderView_Previews: PreviewProvider, TestablePreview {
|
||||
"Sem amet enim habitant nibh augue mauris.",
|
||||
"Interdum mauris ultrices tincidunt proin morbi erat aenean risus nibh.",
|
||||
"Diam amet sit fermentum vulputate faucibus."].joined(separator: " "),
|
||||
canonicalAlias: "#subspace:matrix.org",
|
||||
joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")])))
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,7 +28,12 @@ struct SpaceScreenViewState: BindableState {
|
||||
var selectedSpaceRoomID: String?
|
||||
var joiningRoomIDs: Set<String> = []
|
||||
|
||||
var isSpaceManagementEnabled = false
|
||||
var canEditBaseInfo = false
|
||||
var canEditRolesAndPermissions = false
|
||||
|
||||
var isSpaceManagementEnabled: Bool {
|
||||
canEditBaseInfo || canEditRolesAndPermissions
|
||||
}
|
||||
|
||||
var bindings = SpaceScreenViewStateBindings()
|
||||
}
|
||||
|
||||
@@ -80,7 +80,17 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
||||
}
|
||||
|
||||
appSettings.$spaceSettingsEnabled
|
||||
.weakAssign(to: \.state.isSpaceManagementEnabled, on: self)
|
||||
.combineLatest(roomProxy.infoPublisher)
|
||||
.sink { [weak self] isEnabled, roomInfo in
|
||||
guard let self else { return }
|
||||
guard isEnabled, let powerLevels = roomInfo.powerLevels else {
|
||||
state.canEditBaseInfo = false
|
||||
state.canEditRolesAndPermissions = false
|
||||
return
|
||||
}
|
||||
state.canEditBaseInfo = powerLevels.canOwnUserEditBaseInfo()
|
||||
state.canEditRolesAndPermissions = powerLevels.canOwnUserEditRolesAndPermissions()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ struct LeaveSpaceView: View {
|
||||
Label(leaveHandle.confirmationTitle, icon: \.leave)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
} else if context.viewState.isSpaceManagementEnabled {
|
||||
} else if context.viewState.canEditRolesAndPermissions {
|
||||
Button {
|
||||
context.send(viewAction: .rolesAndPermissions)
|
||||
} label: {
|
||||
|
||||
@@ -114,6 +114,7 @@ struct SpaceScreen_Previews: PreviewProvider, TestablePreview {
|
||||
joinedMembersCount: 76,
|
||||
heroes: [.mockDan, .mockBob, .mockCharlie, .mockVerbose],
|
||||
topic: "Description of the space goes right here. Lorem ipsum dolor sit amet consectetur. Leo viverra morbi habitant in.",
|
||||
canonicalAlias: "#engineering-team:element.io",
|
||||
joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")])))
|
||||
let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceRoomProxy: spaceRoomProxy,
|
||||
initialSpaceRooms: .mockSpaceList))
|
||||
|
||||
@@ -15,6 +15,7 @@ struct SpaceSettingsScreenViewState: BindableState {
|
||||
var joinedMembersCount: Int
|
||||
var hasMemberIdentityVerificationStateViolations = false
|
||||
|
||||
var canEditBaseInfo = false
|
||||
var canEditRolesOrPermissions = false
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ class SpaceSettingsScreenViewModel: SpaceSettingsScreenViewModelType, SpaceSetti
|
||||
|
||||
if let powerLevels = roomInfo.powerLevels {
|
||||
state.canEditRolesOrPermissions = powerLevels.canOwnUserEditRolesAndPermissions()
|
||||
state.canEditBaseInfo = powerLevels.canOwnUserEditBaseInfo()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+38
-16
@@ -26,17 +26,24 @@ struct SpaceSettingsScreen: View {
|
||||
|
||||
private var editSection: some View {
|
||||
Section {
|
||||
ListRow(kind: .custom {
|
||||
Button {
|
||||
context.send(viewAction: .processTapEdit)
|
||||
} label: {
|
||||
editSectionContent
|
||||
}
|
||||
})
|
||||
ListRow(kind: .custom { editRow })
|
||||
}
|
||||
}
|
||||
|
||||
private var editSectionContent: some View {
|
||||
@ViewBuilder
|
||||
private var editRow: some View {
|
||||
if context.viewState.canEditBaseInfo {
|
||||
Button {
|
||||
context.send(viewAction: .processTapEdit)
|
||||
} label: {
|
||||
editRowContent
|
||||
}
|
||||
} else {
|
||||
editRowContent
|
||||
}
|
||||
}
|
||||
|
||||
private var editRowContent: some View {
|
||||
HStack(spacing: 12) {
|
||||
RoomAvatarImage(avatar: context.viewState.details.avatar,
|
||||
avatarSize: .room(on: .spaceSettings),
|
||||
@@ -57,7 +64,9 @@ struct SpaceSettingsScreen: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
ListRowAccessory.navigationLink
|
||||
if context.viewState.canEditBaseInfo {
|
||||
ListRowAccessory.navigationLink
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, ListRowPadding.horizontal)
|
||||
.padding(.vertical, 16)
|
||||
@@ -108,16 +117,29 @@ struct SpaceSettingsScreen: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct SpaceSettingsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = SpaceSettingsScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Space",
|
||||
avatarURL: .mockMXCAvatar,
|
||||
isSpace: true,
|
||||
canonicalAlias: "#space:matrix.org",
|
||||
members: .allMembersAsCreator)),
|
||||
userSession: UserSessionMock(.init()))
|
||||
static let ownerViewModel = SpaceSettingsScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Space",
|
||||
avatarURL: .mockMXCAvatar,
|
||||
isSpace: true,
|
||||
canonicalAlias: "#space:matrix.org",
|
||||
members: .allMembersAsCreator)),
|
||||
userSession: UserSessionMock(.init()))
|
||||
|
||||
static let userViewModel = SpaceSettingsScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Space",
|
||||
avatarURL: .mockMXCAvatar,
|
||||
isSpace: true,
|
||||
canonicalAlias: "#space:matrix.org",
|
||||
members: .allMembers)),
|
||||
userSession: UserSessionMock(.init()))
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
SpaceSettingsScreen(context: viewModel.context)
|
||||
SpaceSettingsScreen(context: ownerViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Owner")
|
||||
|
||||
NavigationStack {
|
||||
SpaceSettingsScreen(context: userViewModel.context)
|
||||
}
|
||||
.previewDisplayName("User")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,3 +36,12 @@ protocol RoomPowerLevelsProxyProtocol {
|
||||
func canUserPinOrUnpin(userID: String) -> Result<Bool, RoomProxyError>
|
||||
func canUserJoinCall(userID: String) -> Result<Bool, RoomProxyError>
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
extension RoomPowerLevelsProxyProtocol {
|
||||
/// Can own user edit either the room name, avatar or topic
|
||||
func canOwnUserEditBaseInfo() -> Bool {
|
||||
canOwnUser(sendStateEvent: .roomAvatar) || canOwnUser(sendStateEvent: .roomName) || canOwnUser(sendStateEvent: .roomTopic)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user