diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 0dc7a2ef1b..008954ba12 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -558,7 +558,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { guard let roomProxy else { fatalError() } - let params = RoomMemberDetailsScreenCoordinatorParameters(roomProxy: roomProxy, roomMemberProxy: member, mediaProvider: userSession.mediaProvider) + let params = RoomMemberDetailsScreenCoordinatorParameters(roomProxy: roomProxy, + roomMemberProxy: member, + mediaProvider: userSession.mediaProvider, + userIndicatorController: userIndicatorController) let coordinator = RoomMemberDetailsScreenCoordinator(parameters: params) navigationStackCoordinator.push(coordinator) { [weak self] in diff --git a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift index 3ac27eb502..a13d4db085 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift @@ -23,15 +23,20 @@ struct AvatarHeaderView: View { let avatarSize: AvatarSize let imageProvider: ImageProviderProtocol? let subtitle: String? + var onAvatarTap: (() -> Void)? @ViewBuilder var footer: () -> Footer var body: some View { VStack(spacing: 8.0) { - LoadableAvatarImage(url: avatarUrl, - name: name, - contentID: id, - avatarSize: avatarSize, - imageProvider: imageProvider) + Button { + onAvatarTap?() + } label: { + LoadableAvatarImage(url: avatarUrl, + name: name, + contentID: id, + avatarSize: avatarSize, + imageProvider: imageProvider) + } Text(name ?? id) .foregroundColor(.compound.textPrimary) diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift index 0bc1a5a913..2b7a3944fb 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift @@ -137,6 +137,9 @@ struct RoomDetailsScreenViewStateBindings { var alertInfo: AlertInfo? var leaveRoomAlertItem: LeaveRoomAlertItem? var ignoreUserRoomAlertItem: IgnoreUserAlertItem? + + /// A media item that will be previewed with QuickLook. + var mediaPreviewItem: MediaPreviewItem? } struct LeaveRoomAlertItem: AlertProtocol { @@ -174,6 +177,7 @@ enum RoomDetailsScreenViewAction { case unignoreConfirmed case processTapNotifications case processToogleMuteNotifications + case displayAvatar } enum RoomDetailsScreenViewShortcut { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index c2b3fd09a0..9cffcf5376 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -22,6 +22,7 @@ typealias RoomDetailsScreenViewModelType = StateStoreViewModel? + + /// A media item that will be previewed with QuickLook. + var mediaPreviewItem: MediaPreviewItem? } enum RoomMemberDetailsScreenViewAction { @@ -73,6 +76,7 @@ enum RoomMemberDetailsScreenViewAction { case showIgnoreAlert case ignoreConfirmed case unignoreConfirmed + case displayAvatar } enum RoomMemberDetailsScreenError: Hashable { diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift index 4735a700e8..d0ffbb3096 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift @@ -21,14 +21,23 @@ typealias RoomMemberDetailsScreenViewModelType = StateStoreViewModel Void)? - init(roomProxy: RoomProxyProtocol, roomMemberProxy: RoomMemberProxyProtocol, mediaProvider: MediaProviderProtocol) { + init(roomProxy: RoomProxyProtocol, + roomMemberProxy: RoomMemberProxyProtocol, + mediaProvider: MediaProviderProtocol, + userIndicatorController: UserIndicatorControllerProtocol) { self.roomProxy = roomProxy self.roomMemberProxy = roomMemberProxy + self.mediaProvider = mediaProvider + self.userIndicatorController = userIndicatorController + let initialViewState = RoomMemberDetailsScreenViewState(details: RoomMemberDetails(withProxy: roomMemberProxy), bindings: .init()) + super.init(initialViewState: initialViewState, imageProvider: mediaProvider) } @@ -44,6 +53,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro Task { await ignoreUser() } case .unignoreConfirmed: Task { await unignoreUser() } + case .displayAvatar: + displayFullScreenAvatar() } } @@ -82,4 +93,24 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro await self.roomProxy.updateMembers() } } + + private func displayFullScreenAvatar() { + guard let avatarURL = roomMemberProxy.avatarURL else { + return + } + + let loadingIndicatorIdentifier = "roomMemberAvatarLoadingIndicator" + userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true)) + + Task { + 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")) { + 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 b5a5c58d58..fb1563a743 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift @@ -31,6 +31,7 @@ struct RoomMemberDetailsScreen: View { .alert(item: $context.ignoreUserAlert, actions: blockUserAlertActions, message: blockUserAlertMessage) .alert(item: $context.alertInfo) .track(screen: .user) + .interactiveQuickLook(item: $context.mediaPreviewItem) } // MARK: - Private @@ -43,6 +44,8 @@ struct RoomMemberDetailsScreen: View { avatarSize: .user(on: .memberDetails), imageProvider: context.imageProvider, subtitle: context.viewState.details.id) { + context.send(viewAction: .displayAvatar) + } footer: { if let permalink = context.viewState.details.permalink { HStack(spacing: 32) { ShareLink(item: permalink) { @@ -101,17 +104,26 @@ struct RoomMemberDetailsScreen_Previews: PreviewProvider { static let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) static let otherUserViewModel = { let member = RoomMemberProxyMock.mockDan - return RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, roomMemberProxy: member, mediaProvider: MockMediaProvider()) + return RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, + roomMemberProxy: member, + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) }() static let accountOwnerViewModel = { let member = RoomMemberProxyMock.mockMe - return RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, roomMemberProxy: member, mediaProvider: MockMediaProvider()) + return RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, + roomMemberProxy: member, + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) }() static let ignoredUserViewModel = { let member = RoomMemberProxyMock.mockIgnored - return RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, roomMemberProxy: member, mediaProvider: MockMediaProvider()) + return RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, + roomMemberProxy: member, + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) }() static var previews: some View { diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift index 124813a76d..1be8dfdc0a 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift @@ -63,7 +63,10 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol { // MARK: - Private private func selectMember(_ member: RoomMemberProxyProtocol) { - let parameters = RoomMemberDetailsScreenCoordinatorParameters(roomProxy: parameters.roomProxy, roomMemberProxy: member, mediaProvider: parameters.mediaProvider) + let parameters = RoomMemberDetailsScreenCoordinatorParameters(roomProxy: parameters.roomProxy, + roomMemberProxy: member, + mediaProvider: parameters.mediaProvider, + userIndicatorController: ServiceLocator.shared.userIndicatorController) let coordinator = RoomMemberDetailsScreenCoordinator(parameters: parameters) navigationStackCoordinator?.push(coordinator) diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index ae08da6740..694c63e191 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -525,17 +525,26 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .roomMemberDetailsAccountOwner: let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomProxy: RoomProxyMock(with: .init(displayName: "")), roomMemberProxy: RoomMemberProxyMock.mockMe, mediaProvider: MockMediaProvider())) + let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomProxy: RoomProxyMock(with: .init(displayName: "")), + roomMemberProxy: RoomMemberProxyMock.mockMe, + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomMemberDetails: let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomProxy: RoomProxyMock(with: .init(displayName: "")), roomMemberProxy: RoomMemberProxyMock.mockAlice, mediaProvider: MockMediaProvider())) + let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomProxy: RoomProxyMock(with: .init(displayName: "")), + roomMemberProxy: RoomMemberProxyMock.mockAlice, + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomMemberDetailsIgnoredUser: let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomProxy: RoomProxyMock(with: .init(displayName: "")), roomMemberProxy: RoomMemberProxyMock.mockIgnored, mediaProvider: MockMediaProvider())) + let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomProxy: RoomProxyMock(with: .init(displayName: "")), + roomMemberProxy: RoomMemberProxyMock.mockIgnored, + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .invitesWithBadges: diff --git a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift index 057042ae11..279d00ec05 100644 --- a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift @@ -33,7 +33,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase { roomMemberProxyMock = RoomMemberProxyMock.mockAlice viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, roomMemberProxy: roomMemberProxyMock, - mediaProvider: MockMediaProvider()) + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) XCTAssertEqual(context.viewState.details, RoomMemberDetails(withProxy: roomMemberProxyMock)) XCTAssertNil(context.ignoreUserAlert) @@ -48,7 +49,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase { } viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, roomMemberProxy: roomMemberProxyMock, - mediaProvider: MockMediaProvider()) + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) context.send(viewAction: .showIgnoreAlert) XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore)) @@ -74,7 +76,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase { } viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, roomMemberProxy: roomMemberProxyMock, - mediaProvider: MockMediaProvider()) + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) context.send(viewAction: .showIgnoreAlert) XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore)) @@ -99,7 +102,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase { } viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, roomMemberProxy: roomMemberProxyMock, - mediaProvider: MockMediaProvider()) + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) context.send(viewAction: .showUnignoreAlert) XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore)) @@ -125,7 +129,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase { } viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, roomMemberProxy: roomMemberProxyMock, - mediaProvider: MockMediaProvider()) + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) context.send(viewAction: .showUnignoreAlert) XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore)) @@ -147,7 +152,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase { roomMemberProxyMock = RoomMemberProxyMock.mockMe viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, roomMemberProxy: roomMemberProxyMock, - mediaProvider: MockMediaProvider()) + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) XCTAssertEqual(context.viewState.details, RoomMemberDetails(withProxy: roomMemberProxyMock)) XCTAssertNil(context.ignoreUserAlert) @@ -158,7 +164,8 @@ class RoomMemberDetailsViewModelTests: XCTestCase { roomMemberProxyMock = RoomMemberProxyMock.mockIgnored viewModel = RoomMemberDetailsScreenViewModel(roomProxy: roomProxyMock, roomMemberProxy: roomMemberProxyMock, - mediaProvider: MockMediaProvider()) + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) XCTAssertEqual(context.viewState.details, RoomMemberDetails(withProxy: roomMemberProxyMock)) XCTAssertNil(context.ignoreUserAlert) diff --git a/changelog.d/pr-1448.feature b/changelog.d/pr-1448.feature new file mode 100644 index 0000000000..d59c66eb0c --- /dev/null +++ b/changelog.d/pr-1448.feature @@ -0,0 +1 @@ +Display avatars full screen when tapping on them from the room or member detail screens \ No newline at end of file