mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:06:00 +00:00
Add support for homeserver capabilities to disable the UserDetailsEditScreen UI. (#5421)
Add support for homeserver capabilities to disable editing your user profile. Also updates editable avatar size/formatting to match the latest Figma at the same time.
This commit is contained in:
@@ -29,6 +29,9 @@ struct ClientProxyMockConfiguration {
|
||||
var timelineMediaVisibility = TimelineMediaVisibility.always
|
||||
var hideInviteAvatars = false
|
||||
|
||||
var canChangeAvatar = true
|
||||
var canChangeDisplayName = true
|
||||
|
||||
var maxMediaUploadSize: UInt = 100 * 1024 * 1024
|
||||
|
||||
class Overrides {
|
||||
@@ -109,6 +112,11 @@ extension ClientProxyMock {
|
||||
spaceService = SpaceServiceProxyMock(configuration.spaceServiceConfiguration)
|
||||
linkNewDeviceServiceReturnValue = LinkNewDeviceServiceMock(.init())
|
||||
|
||||
let capabilities = HomeserverCapabilitiesProxyMock()
|
||||
capabilities.canChangeAvatarReturnValue = configuration.canChangeAvatar
|
||||
capabilities.canChangeDisplayNameReturnValue = configuration.canChangeDisplayName
|
||||
self.capabilities = capabilities
|
||||
|
||||
roomForIdentifierClosure = { [weak self] identifier in
|
||||
if let room = self?.roomSummaryProvider.roomListPublisher.value.first(where: { $0.id == identifier }) {
|
||||
let joinedRoomIDs = configuration.overrides.joinedRoomIDs
|
||||
|
||||
@@ -2696,6 +2696,11 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
||||
set(value) { underlyingSpaceService = value }
|
||||
}
|
||||
var underlyingSpaceService: SpaceServiceProxyProtocol!
|
||||
var capabilities: HomeserverCapabilitiesProxyProtocol {
|
||||
get { return underlyingCapabilities }
|
||||
set(value) { underlyingCapabilities = value }
|
||||
}
|
||||
var underlyingCapabilities: HomeserverCapabilitiesProxyProtocol!
|
||||
var isReportRoomSupportedCallsCount = 0
|
||||
var isReportRoomSupportedCalled: Bool {
|
||||
return isReportRoomSupportedCallsCount > 0
|
||||
@@ -6945,6 +6950,172 @@ class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol, @unchecked S
|
||||
}
|
||||
}
|
||||
}
|
||||
class HomeserverCapabilitiesProxyMock: HomeserverCapabilitiesProxyProtocol, @unchecked Sendable {
|
||||
|
||||
//MARK: - refresh
|
||||
|
||||
var refreshUnderlyingCallsCount = 0
|
||||
var refreshCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return refreshUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = refreshUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
refreshUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
refreshUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var refreshCalled: Bool {
|
||||
return refreshCallsCount > 0
|
||||
}
|
||||
var refreshClosure: (() async -> Void)?
|
||||
|
||||
func refresh() async {
|
||||
refreshCallsCount += 1
|
||||
await refreshClosure?()
|
||||
}
|
||||
//MARK: - canChangeAvatar
|
||||
|
||||
var canChangeAvatarUnderlyingCallsCount = 0
|
||||
var canChangeAvatarCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return canChangeAvatarUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = canChangeAvatarUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
canChangeAvatarUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
canChangeAvatarUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var canChangeAvatarCalled: Bool {
|
||||
return canChangeAvatarCallsCount > 0
|
||||
}
|
||||
|
||||
var canChangeAvatarUnderlyingReturnValue: Bool!
|
||||
var canChangeAvatarReturnValue: Bool! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return canChangeAvatarUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Bool? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = canChangeAvatarUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
canChangeAvatarUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
canChangeAvatarUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var canChangeAvatarClosure: (() async -> Bool)?
|
||||
|
||||
func canChangeAvatar() async -> Bool {
|
||||
canChangeAvatarCallsCount += 1
|
||||
if let canChangeAvatarClosure = canChangeAvatarClosure {
|
||||
return await canChangeAvatarClosure()
|
||||
} else {
|
||||
return canChangeAvatarReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - canChangeDisplayName
|
||||
|
||||
var canChangeDisplayNameUnderlyingCallsCount = 0
|
||||
var canChangeDisplayNameCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return canChangeDisplayNameUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = canChangeDisplayNameUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
canChangeDisplayNameUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
canChangeDisplayNameUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var canChangeDisplayNameCalled: Bool {
|
||||
return canChangeDisplayNameCallsCount > 0
|
||||
}
|
||||
|
||||
var canChangeDisplayNameUnderlyingReturnValue: Bool!
|
||||
var canChangeDisplayNameReturnValue: Bool! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return canChangeDisplayNameUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Bool? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = canChangeDisplayNameUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
canChangeDisplayNameUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
canChangeDisplayNameUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var canChangeDisplayNameClosure: (() async -> Bool)?
|
||||
|
||||
func canChangeDisplayName() async -> Bool {
|
||||
canChangeDisplayNameCallsCount += 1
|
||||
if let canChangeDisplayNameClosure = canChangeDisplayNameClosure {
|
||||
return await canChangeDisplayNameClosure()
|
||||
} else {
|
||||
return canChangeDisplayNameReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class InvitedRoomProxyMock: InvitedRoomProxyProtocol, @unchecked Sendable {
|
||||
var info: BaseRoomInfoProxyProtocol {
|
||||
get { return underlyingInfo }
|
||||
|
||||
@@ -134,10 +134,12 @@ enum RoomAvatarSizeOnScreen {
|
||||
case globalSearch
|
||||
case roomSelection
|
||||
case details
|
||||
case editRoomDetails
|
||||
case notificationSettings
|
||||
case roomDirectorySearch
|
||||
case joinRoom
|
||||
case spaceHeader
|
||||
case editSpaceDetails
|
||||
case spaceAddRooms
|
||||
case spaceAddRoomsSelected
|
||||
case completionSuggestions
|
||||
@@ -157,9 +159,9 @@ enum RoomAvatarSizeOnScreen {
|
||||
case .chats, .spaces, .spaceSettings,
|
||||
.spaceAddRoomsSelected:
|
||||
52
|
||||
case .joinRoom, .spaceHeader:
|
||||
case .joinRoom, .spaceHeader, .editSpaceDetails:
|
||||
64
|
||||
case .details:
|
||||
case .details, .editRoomDetails:
|
||||
96
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct EditAvatarButtonStyle: ButtonStyle {
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
if isEnabled {
|
||||
EditAvatarBadge()
|
||||
.scaledOffset(x: 8, y: 0, relativeTo: .title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EditAvatarBadge: View {
|
||||
var body: some View {
|
||||
CompoundIcon(\.edit, size: .small, relativeTo: .body)
|
||||
.foregroundStyle(.compound.iconPrimary)
|
||||
.scaledPadding(5, relativeTo: .title)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(Color.compound.bgCanvasDefault)
|
||||
.overlay {
|
||||
Circle()
|
||||
.inset(by: -0.5)
|
||||
.stroke(.compound.borderInteractiveSecondary, lineWidth: 1)
|
||||
}
|
||||
}
|
||||
.scaledPadding(4, relativeTo: .title)
|
||||
.background(.compound.bgSubtleSecondaryLevel0, in: Circle())
|
||||
.scaledPadding(-4)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
struct EditAvatarButtonStyle_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 20) {
|
||||
Button { } label: {
|
||||
LoadableAvatarImage(url: nil,
|
||||
name: "Test", contentID: "test",
|
||||
avatarSize: .user(on: .editUserDetails),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()))
|
||||
}
|
||||
.buttonStyle(EditAvatarButtonStyle())
|
||||
|
||||
Button { } label: {
|
||||
LoadableAvatarImage(url: nil,
|
||||
name: "Test", contentID: "test",
|
||||
avatarSize: .user(on: .editUserDetails),
|
||||
mediaProvider: MediaProviderMock(configuration: .init()))
|
||||
}
|
||||
.buttonStyle(EditAvatarButtonStyle())
|
||||
.disabled(true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(.compound.bgSubtleSecondaryLevel0)
|
||||
}
|
||||
}
|
||||
@@ -114,11 +114,10 @@ struct CreateRoomScreen: View {
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.scaledFrame(size: 70, relativeTo: .title)
|
||||
.clipShape(context.viewState.isSpace ? AnyShape(RoundedRectangle(cornerRadius: 16)) : AnyShape(Circle()))
|
||||
.avatarShape(context.viewState.isSpace ? .roundedRect : .circle, size: 70)
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
editAvatarBadge
|
||||
.scaledOffset(x: 12, y: 4, relativeTo: .title)
|
||||
.accessibilityHidden(true)
|
||||
EditAvatarBadge()
|
||||
.scaledOffset(x: 8, y: 0, relativeTo: .title)
|
||||
}
|
||||
} else {
|
||||
CompoundIcon(\.takePhoto, size: .medium, relativeTo: .title)
|
||||
@@ -153,23 +152,6 @@ struct CreateRoomScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var editAvatarBadge: some View {
|
||||
CompoundIcon(\.edit, size: .small, relativeTo: .body)
|
||||
.foregroundStyle(.compound.iconPrimary)
|
||||
.scaledPadding(5, relativeTo: .title)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(Color.compound.bgCanvasDefault)
|
||||
.overlay {
|
||||
Circle()
|
||||
.inset(by: 0.5)
|
||||
.stroke(.compound.borderInteractiveSecondary, lineWidth: 1)
|
||||
}
|
||||
}
|
||||
.scaledPadding(3.5, relativeTo: .title)
|
||||
.background(.compound.bgSubtleSecondaryLevel0, in: Circle())
|
||||
}
|
||||
|
||||
private var topicSection: some View {
|
||||
Section {
|
||||
ListRow(label: .plain(title: L10n.screenCreateRoomTopicPlaceholder),
|
||||
|
||||
@@ -11,11 +11,12 @@ import SwiftUI
|
||||
|
||||
struct RoomDetailsEditScreen: View {
|
||||
@ObservedObject var context: RoomDetailsEditScreenViewModel.Context
|
||||
|
||||
private enum Focus { case name, topic }
|
||||
@FocusState private var focus: Focus?
|
||||
|
||||
private enum Focus {
|
||||
case name
|
||||
case topic
|
||||
private var isSpace: Bool {
|
||||
context.viewState.isSpace
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -60,20 +61,15 @@ struct RoomDetailsEditScreen: View {
|
||||
url: context.viewState.avatarURL,
|
||||
name: context.viewState.initialName,
|
||||
contentID: context.viewState.roomID,
|
||||
shape: context.viewState.isSpace ? .roundedRect : .circle,
|
||||
avatarSize: .user(on: .memberDetails),
|
||||
shape: isSpace ? .roundedRect : .circle,
|
||||
avatarSize: .room(on: isSpace ? .editSpaceDetails : .editRoomDetails),
|
||||
mediaProvider: context.mediaProvider)
|
||||
.accessibilityLabel(L10n.a11yEditAvatar)
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
if context.viewState.canEditAvatar {
|
||||
avatarOverlayIcon
|
||||
}
|
||||
}
|
||||
.confirmationDialog("", isPresented: $context.showMediaSheet) {
|
||||
mediaActionSheet
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(EditAvatarButtonStyle())
|
||||
.disabled(!context.viewState.canEditAvatar)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.listRowBackground(Color.clear)
|
||||
@@ -100,7 +96,7 @@ struct RoomDetailsEditScreen: View {
|
||||
private var topicSection: some View {
|
||||
Section {
|
||||
if context.viewState.canEditTopic {
|
||||
ListRow(label: .plain(title: context.viewState.isSpace ? L10n.commonSpaceTopicPlaceholder : L10n.commonTopicPlaceholder),
|
||||
ListRow(label: .plain(title: isSpace ? L10n.commonSpaceTopicPlaceholder : L10n.commonTopicPlaceholder),
|
||||
kind: .textField(text: $context.topic, axis: .vertical))
|
||||
.focused($focus, equals: .topic)
|
||||
.lineLimit(3...)
|
||||
@@ -116,17 +112,6 @@ struct RoomDetailsEditScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var avatarOverlayIcon: some View {
|
||||
CompoundIcon(\.editSolid, size: .xSmall, relativeTo: .compound.bodyLG)
|
||||
.foregroundColor(.white)
|
||||
.padding(4)
|
||||
.background {
|
||||
Circle()
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var mediaActionSheet: some View {
|
||||
Button {
|
||||
@@ -152,27 +137,9 @@ struct RoomDetailsEditScreen: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct RoomDetailsEditScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let editableViewModel = {
|
||||
let roomProxy = JoinedRoomProxyMock(.init(id: "test_id",
|
||||
name: "Room",
|
||||
members: [.mockMeAdmin]))
|
||||
|
||||
return RoomDetailsEditScreenViewModel(roomProxy: roomProxy,
|
||||
userSession: UserSessionMock(.init()),
|
||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
|
||||
userIndicatorController: UserIndicatorControllerMock.default)
|
||||
}()
|
||||
|
||||
static let readOnlyViewModel = {
|
||||
let roomProxy = JoinedRoomProxyMock(.init(id: "test_id",
|
||||
name: "Room",
|
||||
members: [.mockAlice]))
|
||||
|
||||
return RoomDetailsEditScreenViewModel(roomProxy: roomProxy,
|
||||
userSession: UserSessionMock(.init()),
|
||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
|
||||
userIndicatorController: UserIndicatorControllerMock.default)
|
||||
}()
|
||||
static let editableViewModel = makeViewModel(readOnly: false)
|
||||
static let readOnlyViewModel = makeViewModel(readOnly: true)
|
||||
static let editableSpaceViewModel = makeViewModel(readOnly: false, isSpace: true)
|
||||
|
||||
static var previews: some View {
|
||||
ElementNavigationStack {
|
||||
@@ -183,9 +150,26 @@ struct RoomDetailsEditScreen_Previews: PreviewProvider, TestablePreview {
|
||||
ElementNavigationStack {
|
||||
RoomDetailsEditScreen(context: editableViewModel.context)
|
||||
}
|
||||
.snapshotPreferences(expect: editableViewModel.context.$viewState.map { state in
|
||||
state.canEditTopic == true
|
||||
})
|
||||
.snapshotPreferences(expect: editableViewModel.context.$viewState.map { $0.canEditTopic == true })
|
||||
.previewDisplayName("Editable")
|
||||
|
||||
ElementNavigationStack {
|
||||
RoomDetailsEditScreen(context: editableSpaceViewModel.context)
|
||||
}
|
||||
.snapshotPreferences(expect: editableSpaceViewModel.context.$viewState.map { $0.canEditTopic == true })
|
||||
.previewDisplayName("Space")
|
||||
}
|
||||
|
||||
static func makeViewModel(readOnly: Bool, isSpace: Bool = false) -> RoomDetailsEditScreenViewModel {
|
||||
let members: [RoomMemberProxyMock] = readOnly ? [.mockAlice] : [.mockMeAdmin]
|
||||
let roomProxy = JoinedRoomProxyMock(.init(id: "test_id",
|
||||
name: isSpace ? "Space" : "Room",
|
||||
isSpace: isSpace,
|
||||
members: members))
|
||||
|
||||
return RoomDetailsEditScreenViewModel(roomProxy: roomProxy,
|
||||
userSession: UserSessionMock(.init()),
|
||||
mediaUploadingPreprocessor: MediaUploadingPreprocessor(appSettings: ServiceLocator.shared.settings),
|
||||
userIndicatorController: UserIndicatorControllerMock.default)
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -18,6 +18,9 @@ enum UserDetailsEditScreenViewModelAction {
|
||||
struct UserDetailsEditScreenViewState: BindableState {
|
||||
let userID: String
|
||||
|
||||
var canEditAvatar = true
|
||||
var canEditDisplayName = true
|
||||
|
||||
var currentAvatarURL: URL?
|
||||
var selectedAvatarURL: URL?
|
||||
|
||||
|
||||
+2
@@ -58,6 +58,8 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe
|
||||
Task {
|
||||
await self.clientProxy.loadUserAvatarURL()
|
||||
await self.clientProxy.loadUserDisplayName()
|
||||
state.canEditAvatar = await clientProxy.capabilities.canChangeAvatar()
|
||||
state.canEditDisplayName = await clientProxy.capabilities.canChangeDisplayName()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-17
@@ -67,14 +67,12 @@ struct UserDetailsEditScreen: View {
|
||||
shape: .circle,
|
||||
avatarSize: .user(on: .editUserDetails),
|
||||
mediaProvider: context.mediaProvider)
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
avatarOverlayIcon
|
||||
}
|
||||
.confirmationDialog("", isPresented: $context.showMediaSheet) {
|
||||
mediaActionSheet
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(EditAvatarButtonStyle())
|
||||
.disabled(!context.viewState.canEditAvatar)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
@@ -84,22 +82,13 @@ struct UserDetailsEditScreen: View {
|
||||
ListRow(label: .plain(title: L10n.screenEditProfileDisplayNamePlaceholder),
|
||||
kind: .textField(text: $context.name, axis: .horizontal))
|
||||
.focused($focus)
|
||||
.disabled(!context.viewState.canEditDisplayName)
|
||||
} header: {
|
||||
Text(L10n.screenEditProfileDisplayName)
|
||||
.compoundListSectionHeader()
|
||||
}
|
||||
}
|
||||
|
||||
private var avatarOverlayIcon: some View {
|
||||
CompoundIcon(\.editSolid, size: .xSmall, relativeTo: .compound.bodyLG)
|
||||
.foregroundColor(.white)
|
||||
.padding(4)
|
||||
.background {
|
||||
Circle()
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var mediaActionSheet: some View {
|
||||
Button {
|
||||
@@ -126,13 +115,26 @@ struct UserDetailsEditScreen: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct UserDetailsEditScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = UserDetailsEditScreenViewModel(userSession: UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@stefan:matrix.org")))),
|
||||
mediaUploadingPreprocessor: .init(appSettings: ServiceLocator.shared.settings),
|
||||
userIndicatorController: UserIndicatorControllerMock.default)
|
||||
static let viewModel = makeViewModel()
|
||||
static let readOnlyViewModel = makeViewModel(canChangeProfile: false)
|
||||
|
||||
static var previews: some View {
|
||||
ElementNavigationStack {
|
||||
UserDetailsEditScreen(context: viewModel.context)
|
||||
}
|
||||
.previewDisplayName("Default")
|
||||
|
||||
ElementNavigationStack {
|
||||
UserDetailsEditScreen(context: readOnlyViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Read Only")
|
||||
}
|
||||
|
||||
static func makeViewModel(canChangeProfile: Bool = true) -> UserDetailsEditScreenViewModel {
|
||||
UserDetailsEditScreenViewModel(userSession: UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@stefan:matrix.org",
|
||||
canChangeAvatar: canChangeProfile,
|
||||
canChangeDisplayName: canChangeProfile)))),
|
||||
mediaUploadingPreprocessor: .init(appSettings: ServiceLocator.shared.settings),
|
||||
userIndicatorController: UserIndicatorControllerMock.default)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
struct HomeserverCapabilitiesProxy: HomeserverCapabilitiesProxyProtocol {
|
||||
private let underlyingCapabilities: HomeserverCapabilitiesProtocol
|
||||
|
||||
init(underlyingCapabilities: HomeserverCapabilitiesProtocol) {
|
||||
self.underlyingCapabilities = underlyingCapabilities
|
||||
}
|
||||
|
||||
func refresh() async {
|
||||
do {
|
||||
try await underlyingCapabilities.refresh()
|
||||
} catch {
|
||||
MXLog.error("Failure refreshing homeserver capabilities: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func canChangeAvatar() async -> Bool {
|
||||
do {
|
||||
return try await underlyingCapabilities.canChangeAvatar()
|
||||
} catch {
|
||||
MXLog.error("Failure checking canChangeAvatar: \(error)")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func canChangeDisplayName() async -> Bool {
|
||||
do {
|
||||
return try await underlyingCapabilities.canChangeDisplayname()
|
||||
} catch {
|
||||
MXLog.error("Failure checking canChangeDisplayName: \(error)")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol HomeserverCapabilitiesProxyProtocol {
|
||||
func refresh() async
|
||||
func canChangeAvatar() async -> Bool
|
||||
func canChangeDisplayName() async -> Bool
|
||||
}
|
||||
@@ -64,6 +64,9 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
let spaceService: SpaceServiceProxyProtocol
|
||||
|
||||
let capabilities: HomeserverCapabilitiesProxyProtocol
|
||||
private var capabilitiesRefreshTask: Task<Void, Never>?
|
||||
|
||||
let eventStringBuilder: RoomEventStringBuilder
|
||||
|
||||
private static var roomCreationPowerLevelOverrides: PowerLevels {
|
||||
@@ -208,6 +211,8 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
spaceService = await SpaceServiceProxy(spaceService: client.spaceService())
|
||||
|
||||
capabilities = HomeserverCapabilitiesProxy(underlyingCapabilities: client.homeserverCapabilities())
|
||||
|
||||
let configuredAppService = try await ClientProxyServices(client: client,
|
||||
actionsSubject: actionsSubject,
|
||||
notificationSettings: notificationSettings,
|
||||
@@ -1122,17 +1127,25 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
MXLog.info("Received room list update: \(state)")
|
||||
|
||||
guard state != .error,
|
||||
state != .terminated else {
|
||||
// The sync service is responsible of handling error and termination
|
||||
return
|
||||
}
|
||||
|
||||
// Hide the sync spinner as soon as we get any update back
|
||||
actionsSubject.send(.receivedSyncUpdate)
|
||||
|
||||
if ignoredUsersSubject.value == nil {
|
||||
updateIgnoredUsers()
|
||||
switch state {
|
||||
case .initial, .settingUp, .recovering:
|
||||
break // Don't do anything until we're actually running.
|
||||
case .running:
|
||||
// Hide the sync spinner as soon as we get any update back
|
||||
actionsSubject.send(.receivedSyncUpdate)
|
||||
|
||||
if ignoredUsersSubject.value == nil {
|
||||
updateIgnoredUsers()
|
||||
}
|
||||
|
||||
if capabilitiesRefreshTask == nil {
|
||||
capabilitiesRefreshTask = Task { [weak self] in
|
||||
await self?.capabilities.refresh()
|
||||
self?.capabilitiesRefreshTask = nil
|
||||
}
|
||||
}
|
||||
case .error, .terminated:
|
||||
break // The sync service is responsible for handling error and termination
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -145,6 +145,8 @@ protocol ClientProxyProtocol: AnyObject {
|
||||
|
||||
var spaceService: SpaceServiceProxyProtocol { get }
|
||||
|
||||
var capabilities: HomeserverCapabilitiesProxyProtocol { get }
|
||||
|
||||
var isReportRoomSupported: Bool { get async }
|
||||
|
||||
var isLiveKitRTCSupported: Bool { get async }
|
||||
|
||||
Reference in New Issue
Block a user