From 2c10dfdb7f0c1837c623db663e3c319ca0da116a Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 27 Sep 2024 15:23:20 +0300 Subject: [PATCH] Allow focusing the different avatars making up a DM details cluster separately. (#3341) --- .../SwiftUI/Views/AvatarHeaderView.swift | 24 +++++++++---------- .../SwiftUI/Views/LoadableAvatarImage.swift | 23 ++++++++++++++++-- .../Other/SwiftUI/Views/RoomAvatarImage.swift | 14 +++++++---- .../RoomDetailsScreenModels.swift | 2 +- .../RoomDetailsScreenViewModel.swift | 12 ++++------ .../View/RoomDetailsScreen.swift | 8 +++---- .../RoomMemberDetailsScreenModels.swift | 2 +- .../RoomMemberDetailsScreenViewModel.swift | 12 ++++------ .../View/RoomMemberDetailsScreen.swift | 4 ++-- .../UserProfileScreenModels.swift | 2 +- .../UserProfileScreenViewModel.swift | 9 ++++--- .../View/UserProfileScreen.swift | 4 ++-- 12 files changed, 65 insertions(+), 51 deletions(-) diff --git a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift index 4b63f20ca..4705b76a1 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift @@ -26,13 +26,13 @@ struct AvatarHeaderView: View { private let avatarSize: AvatarSize private let mediaProvider: MediaProviderProtocol? - private var onAvatarTap: (() -> Void)? + private var onAvatarTap: ((URL) -> Void)? @ViewBuilder private var footer: () -> Footer init(room: RoomDetails, avatarSize: AvatarSize, mediaProvider: MediaProviderProtocol? = nil, - onAvatarTap: (() -> Void)? = nil, + onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { avatarInfo = .room(room.avatar) title = room.name ?? room.id @@ -54,7 +54,7 @@ struct AvatarHeaderView: View { init(accountOwner: RoomMemberDetails, dmRecipient: RoomMemberDetails, mediaProvider: MediaProviderProtocol? = nil, - onAvatarTap: (() -> Void)? = nil, + onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { let dmRecipientProfile = UserProfileProxy(member: dmRecipient) avatarInfo = .room(.heroes([dmRecipientProfile, UserProfileProxy(member: accountOwner)])) @@ -72,7 +72,7 @@ struct AvatarHeaderView: View { init(member: RoomMemberDetails, avatarSize: AvatarSize, mediaProvider: MediaProviderProtocol? = nil, - onAvatarTap: (() -> Void)? = nil, + onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { let profile = UserProfileProxy(member: member) @@ -86,7 +86,7 @@ struct AvatarHeaderView: View { init(user: UserProfileProxy, avatarSize: AvatarSize, mediaProvider: MediaProviderProtocol? = nil, - onAvatarTap: (() -> Void)? = nil, + onAvatarTap: ((URL) -> Void)? = nil, @ViewBuilder footer: @escaping () -> Footer) { avatarInfo = .user(user) title = user.displayName ?? user.userID @@ -128,24 +128,22 @@ struct AvatarHeaderView: View { case .room(let roomAvatar): RoomAvatarImage(avatar: roomAvatar, avatarSize: avatarSize, - mediaProvider: mediaProvider) + mediaProvider: mediaProvider, + onAvatarTap: onAvatarTap) + case .user(let userProfile): LoadableAvatarImage(url: userProfile.avatarURL, name: userProfile.displayName, contentID: userProfile.userID, avatarSize: avatarSize, - mediaProvider: mediaProvider) + mediaProvider: mediaProvider, + onTap: onAvatarTap) } } var body: some View { VStack(spacing: 8.0) { - Button { - onAvatarTap?() - } label: { - avatar - } - .buttonStyle(.borderless) // Add a button style to stop the whole row being tappable. + avatar Spacer() .frame(height: 9) diff --git a/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift index f311b321f..2c35c9a5e 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/LoadableAvatarImage.swift @@ -13,20 +13,39 @@ struct LoadableAvatarImage: View { private let contentID: String? private let avatarSize: AvatarSize private let mediaProvider: MediaProviderProtocol? + private let onTap: ((URL) -> Void)? @ScaledMetric private var frameSize: CGFloat - init(url: URL?, name: String?, contentID: String?, avatarSize: AvatarSize, mediaProvider: MediaProviderProtocol?) { + init(url: URL?, name: String?, + contentID: String?, + avatarSize: AvatarSize, + mediaProvider: MediaProviderProtocol?, + onTap: ((URL) -> Void)? = nil) { self.url = url self.name = name self.contentID = contentID self.avatarSize = avatarSize self.mediaProvider = mediaProvider + self.onTap = onTap _frameSize = ScaledMetric(wrappedValue: avatarSize.value) } var body: some View { + if let onTap, let url { + Button { + onTap(url) + } label: { + clippedAvatar + } + .buttonStyle(.borderless) // Add a button style to stop the whole row being tappable. + } else { + clippedAvatar + } + } + + private var clippedAvatar: some View { avatar .frame(width: frameSize, height: frameSize) .background(Color.compound.bgCanvasDefault) @@ -34,7 +53,7 @@ struct LoadableAvatarImage: View { } @ViewBuilder - var avatar: some View { + private var avatar: some View { if let url { LoadableImage(url: url, mediaType: .avatar, diff --git a/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift index c7f77b116..78d4c96e1 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/RoomAvatarImage.swift @@ -25,6 +25,8 @@ struct RoomAvatarImage: View { let avatarSize: AvatarSize let mediaProvider: MediaProviderProtocol? + private(set) var onAvatarTap: ((URL) -> Void)? + var body: some View { switch avatar { case .room(let id, let name, let avatarURL): @@ -32,7 +34,8 @@ struct RoomAvatarImage: View { name: name, contentID: id, avatarSize: avatarSize, - mediaProvider: mediaProvider) + mediaProvider: mediaProvider, + onTap: onAvatarTap) case .heroes(let users): // We will expand upon this with more stack sizes in the future. if users.count == 0 { @@ -45,14 +48,16 @@ struct RoomAvatarImage: View { name: users[0].displayName, contentID: users[0].userID, avatarSize: avatarSize, - mediaProvider: mediaProvider) + mediaProvider: mediaProvider, + onTap: onAvatarTap) .scaledFrame(size: clusterSize, alignment: .topTrailing) LoadableAvatarImage(url: users[1].avatarURL, name: users[1].displayName, contentID: users[1].userID, avatarSize: avatarSize, - mediaProvider: mediaProvider) + mediaProvider: mediaProvider, + onTap: onAvatarTap) .mask { Rectangle() .fill(Color.white) @@ -74,7 +79,8 @@ struct RoomAvatarImage: View { name: users[0].displayName, contentID: users[0].userID, avatarSize: avatarSize, - mediaProvider: mediaProvider) + mediaProvider: mediaProvider, + onTap: onAvatarTap) } } } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift index 77a328956..72ff876c9 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift @@ -183,7 +183,7 @@ enum RoomDetailsScreenViewAction { case unignoreConfirmed case processTapNotifications case processToggleMuteNotifications - case displayAvatar + case displayAvatar(URL) case processTapPolls case toggleFavourite(isFavourite: Bool) case processTapRolesAndPermissions diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 79e83484e..b435eeb23 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -150,8 +150,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } case .processToggleMuteNotifications: Task { await toggleMuteNotifications() } - case .displayAvatar: - displayFullScreenAvatar() + case .displayAvatar(let url): + displayFullScreenAvatar(url) case .processTapPolls: actionsSubject.send(.requestPollsHistoryPresentation) case .toggleFavourite(let isFavourite): @@ -346,11 +346,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } } - private func displayFullScreenAvatar() { - guard let avatarURL = roomProxy.avatarURL else { - return - } - + private func displayFullScreenAvatar(_ url: URL) { let loadingIndicatorIdentifier = "roomAvatarLoadingIndicator" userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true)) @@ -360,7 +356,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } // We don't actually know the mime type here, assume it's an image. - if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: avatarURL, mimeType: "image/jpeg")) { + if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) { state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomProxy.roomTitle) } } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift index 73a8446f9..3b682e8b2 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift @@ -65,8 +65,8 @@ struct RoomDetailsScreen: View { private var normalRoomHeaderSection: some View { AvatarHeaderView(room: context.viewState.details, avatarSize: .room(on: .details), - mediaProvider: context.mediaProvider) { - context.send(viewAction: .displayAvatar) + mediaProvider: context.mediaProvider) { url in + context.send(viewAction: .displayAvatar(url)) } footer: { if !context.viewState.shortcuts.isEmpty { headerSectionShortcuts @@ -78,8 +78,8 @@ struct RoomDetailsScreen: View { private func dmHeaderSection(accountOwner: RoomMemberDetails, recipient: RoomMemberDetails) -> some View { AvatarHeaderView(accountOwner: accountOwner, dmRecipient: recipient, - mediaProvider: context.mediaProvider) { - context.send(viewAction: .displayAvatar) + mediaProvider: context.mediaProvider) { url in + context.send(viewAction: .displayAvatar(url)) } footer: { if !context.viewState.shortcuts.isEmpty { headerSectionShortcuts diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift index fb444c2a1..0b33b653c 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift @@ -74,7 +74,7 @@ enum RoomMemberDetailsScreenViewAction { case showIgnoreAlert case ignoreConfirmed case unignoreConfirmed - case displayAvatar + case displayAvatar(URL) case openDirectChat case startCall(roomID: String) } diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift index 6249d7da6..1bd2dc81d 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift @@ -84,8 +84,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro Task { await ignoreUser() } case .unignoreConfirmed: Task { await unignoreUser() } - case .displayAvatar: - Task { await displayFullScreenAvatar() } + case .displayAvatar(let url): + Task { await displayFullScreenAvatar(url) } case .openDirectChat: Task { await openDirectChat() } case .startCall(let roomID): @@ -143,21 +143,17 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro } } - private func displayFullScreenAvatar() async { + private func displayFullScreenAvatar(_ url: URL) async { guard let roomMemberProxy else { fatalError() } - guard let avatarURL = roomMemberProxy.avatarURL else { - return - } - let loadingIndicatorIdentifier = "roomMemberAvatarLoadingIndicator" userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true)) defer { userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier) } // We don't actually know the mime type here, assume it's an image. - if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: avatarURL, mimeType: "image/jpeg")) { + if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) { state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomMemberProxy.displayName) } } diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift index 1861cd1de..358b19d96 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift @@ -66,8 +66,8 @@ struct RoomMemberDetailsScreen: View { if let memberDetails = context.viewState.memberDetails { AvatarHeaderView(member: memberDetails, avatarSize: .user(on: .memberDetails), - mediaProvider: context.mediaProvider) { - context.send(viewAction: .displayAvatar) + mediaProvider: context.mediaProvider) { url in + context.send(viewAction: .displayAvatar(url)) } footer: { otherUserFooter } diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift index 83a95a65f..4cb0647dd 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift @@ -33,7 +33,7 @@ struct UserProfileScreenViewStateBindings { } enum UserProfileScreenViewAction { - case displayAvatar + case displayAvatar(URL) case openDirectChat case startCall(roomID: String) case dismiss diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift index 8abf9be3e..c409a7289 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift @@ -74,8 +74,8 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr override func process(viewAction: UserProfileScreenViewAction) { switch viewAction { - case .displayAvatar: - Task { await displayFullScreenAvatar() } + case .displayAvatar(let url): + Task { await displayFullScreenAvatar(url) } case .openDirectChat: Task { await openDirectChat() } case .startCall(let roomID): @@ -87,15 +87,14 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr // MARK: - Private - private func displayFullScreenAvatar() async { + private func displayFullScreenAvatar(_ url: URL) async { guard let userProfile = state.userProfile else { fatalError() } - guard let avatarURL = userProfile.avatarURL else { return } showLoadingIndicator(allowsInteraction: false) defer { hideLoadingIndicator() } // We don't actually know the mime type here, assume it's an image. - if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: avatarURL, mimeType: "image/jpeg")) { + if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) { state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: userProfile.displayName) } } diff --git a/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift b/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift index 43201a3bb..a635e0731 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift @@ -63,8 +63,8 @@ struct UserProfileScreen: View { if let userProfile = context.viewState.userProfile { AvatarHeaderView(user: userProfile, avatarSize: .user(on: .memberDetails), - mediaProvider: context.mediaProvider) { - context.send(viewAction: .displayAvatar) + mediaProvider: context.mediaProvider) { url in + context.send(viewAction: .displayAvatar(url)) } footer: { otherUserFooter }