diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index c46b69148..a667f442b 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -115,6 +115,7 @@ 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; }; 19DED23340D0855B59693ED2 /* VoiceMessageRecorderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45C9EAA86423D7D3126DE4F /* VoiceMessageRecorderProtocol.swift */; }; 19FE025AE9BA2959B6589B0D /* RoomMemberDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */; }; + 1A3B073568D1DC8F76F1F3A0 /* UserProfileScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EE69982BBA18C6D51AD08E /* UserProfileScreen.swift */; }; 1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = A678E40E917620059695F067 /* MatrixRustSDK */; }; 1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */; }; 1AB3D8563AB12635250A6A6E /* StaticLocationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C15E0017717EAE3A1D02D005 /* StaticLocationScreenCoordinator.swift */; }; @@ -293,6 +294,7 @@ 4557192F5B15A8D9BB920232 /* AdvancedSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E492690C8B27A892C194CC4 /* AdvancedSettingsScreenCoordinator.swift */; }; 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; }; 4681820102DAC8BA586357D4 /* VoiceMessageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB8D7926A5684E18196B538 /* VoiceMessageCache.swift */; }; + 46A183C6125A669AEB005699 /* UserProfileScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F134D2D91DFF732FB75B2CB7 /* UserProfileScreenViewModelProtocol.swift */; }; 46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCE3636E3D01477C8B2E9D0 /* ReportContentScreenModels.swift */; }; 46A6DB0F78FB399BD59E2D41 /* EncryptionKeyProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E78FC546F28E045A560F2963 /* EncryptionKeyProviderProtocol.swift */; }; 46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565F1B2B300597C616B37888 /* FullscreenDialog.swift */; }; @@ -445,6 +447,7 @@ 69DE29C3E3180BB17D840690 /* ProgressCursorModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97C8E13A1FBA717B0C277ECC /* ProgressCursorModifier.swift */; }; 6A0E7551E0D1793245F34CDD /* ClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09A267106B9585D3D0CFC0D /* ClientError.swift */; }; 6AD722DD92E465E56D2885AB /* BugReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */; }; + 6AEB650311F694A5702255C9 /* UserProfileScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B4932E4EFBC8FAC10972CD /* UserProfileScreenCoordinator.swift */; }; 6AECC84BE14A13440120FED8 /* NSESettingsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB4F169D653296023ED65E6 /* NSESettingsProtocol.swift */; }; 6B05AA5D9BBCD6D8D63B80EB /* TimelineItemAccessibilityModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C6F3DAD167F972702C8893 /* TimelineItemAccessibilityModifier.swift */; }; 6B31508C6334C617360C2EAB /* RoomMemberDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */; }; @@ -722,6 +725,7 @@ AADE7C2497A7B55D8BED7BD6 /* IdentityConfirmedScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8319173DD66C07F45DC48848 /* IdentityConfirmedScreenViewModelProtocol.swift */; }; AAF0BBED840DF4A53EE85E77 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C2C69B8BA5A9702E7A8BC08F /* MatrixRustSDK */; }; ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; }; + AC1DB27A4134470846BE49F6 /* UserProfileScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BD116096CAA9139B95EEA9C /* UserProfileScreenViewModel.swift */; }; AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */; }; AC7AA215D60FBC307F984028 /* Consumable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127A57D053CE8C87B5EFB089 /* Consumable.swift */; }; AC90434798E7894370E80E66 /* SecureBackupScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79BB714D28C9F588DD69353 /* SecureBackupScreenViewModelProtocol.swift */; }; @@ -890,6 +894,7 @@ D415764645491F10344FC6AC /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60F18AECC9D38C2B6D85F99C /* Publisher.swift */; }; D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */; }; D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */; }; + D4CB979EB4FE26AAD9F9A72B /* UserProfileScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604A69C081B935D6A38DE6D8 /* UserProfileScreenModels.swift */; }; D4D5595C4A2A702CFF4E94FF /* HeroImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EC2F1622C5BBABED6012E12 /* HeroImage.swift */; }; D4D7CCECC6C0AAFC42E165BB /* NotificationPermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */; }; D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */; }; @@ -1169,6 +1174,7 @@ 0BB05221D7D941CC82DC8480 /* LogViewerScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenViewModel.swift; sourceTree = ""; }; 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSenderAvatarView.swift; sourceTree = ""; }; 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModel.swift; sourceTree = ""; }; + 0BD116096CAA9139B95EEA9C /* UserProfileScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenViewModel.swift; sourceTree = ""; }; 0C34667458773B02AB5FB0B2 /* LegalInformationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModel.swift; sourceTree = ""; }; 0C62E07C1164F5120727A2A8 /* AppLockSetupBiometricsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenCoordinator.swift; sourceTree = ""; }; 0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1268,6 +1274,7 @@ 2389732B0E115A999A069083 /* NotificationSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenCoordinator.swift; sourceTree = ""; }; 23AA3F4B285570805CB0CCDD /* MapTiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTiler.swift; sourceTree = ""; }; 23E6EB7960BC9D0F7396B3BD /* RoomChangeRolesScreenRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenRow.swift; sourceTree = ""; }; + 23EE69982BBA18C6D51AD08E /* UserProfileScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreen.swift; sourceTree = ""; }; 240610DF32F3213BEC5611D7 /* BlockedUsersScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModelTests.swift; sourceTree = ""; }; 24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenInvitesButton.swift; sourceTree = ""; }; 2429224EB0EEA34D35CE9249 /* UserIndicatorControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerTests.swift; sourceTree = ""; }; @@ -1499,6 +1506,7 @@ 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = ""; }; 5FACD034DB52525A3CEF2BDF /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = ""; }; 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyProtocol.swift; sourceTree = ""; }; + 604A69C081B935D6A38DE6D8 /* UserProfileScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenModels.swift; sourceTree = ""; }; 60F18AECC9D38C2B6D85F99C /* Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publisher.swift; sourceTree = ""; }; 612EF972F2A1800682D32C5E /* StickerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerRoomTimelineView.swift; sourceTree = ""; }; 61B33F23681660E940BA57F4 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/SAS.strings; sourceTree = ""; }; @@ -1965,6 +1973,7 @@ D529B976F8B2AA654D923422 /* VoiceMessageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomTimelineItem.swift; sourceTree = ""; }; D54E12B98252F6C527E31FEE /* MediaUploadPreviewScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelProtocol.swift; sourceTree = ""; }; D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = ""; }; + D5B4932E4EFBC8FAC10972CD /* UserProfileScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenCoordinator.swift; sourceTree = ""; }; D5E26C54362206BBDD096D83 /* test_audio.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = test_audio.mp3; sourceTree = ""; }; D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionServiceTests.swift; sourceTree = ""; }; D622EC7898469BB1D0881CDD /* PollFormScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreen.swift; sourceTree = ""; }; @@ -2075,6 +2084,7 @@ F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelTests.swift; sourceTree = ""; }; F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStyle.swift; sourceTree = ""; }; F0E14FF533D25A0692F7CEB0 /* RoomPollsHistoryScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModel.swift; sourceTree = ""; }; + F134D2D91DFF732FB75B2CB7 /* UserProfileScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenViewModelProtocol.swift; sourceTree = ""; }; F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = ""; }; F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = ""; }; @@ -3863,6 +3873,14 @@ path = Session; sourceTree = ""; }; + 832EB453B3A5D04C18D86117 /* View */ = { + isa = PBXGroup; + children = ( + 23EE69982BBA18C6D51AD08E /* UserProfileScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 832FC81F760220239E285294 /* Proxy */ = { isa = PBXGroup; children = ( @@ -4015,6 +4033,18 @@ path = ElementCall; sourceTree = ""; }; + 93C7520ED23C9598BB144DBB /* UserProfileScreen */ = { + isa = PBXGroup; + children = ( + D5B4932E4EFBC8FAC10972CD /* UserProfileScreenCoordinator.swift */, + 604A69C081B935D6A38DE6D8 /* UserProfileScreenModels.swift */, + 0BD116096CAA9139B95EEA9C /* UserProfileScreenViewModel.swift */, + F134D2D91DFF732FB75B2CB7 /* UserProfileScreenViewModelProtocol.swift */, + 832EB453B3A5D04C18D86117 /* View */, + ); + path = UserProfileScreen; + sourceTree = ""; + }; 948DD12A5533BE1BC260E437 /* LocationSharing */ = { isa = PBXGroup; children = ( @@ -4873,6 +4903,7 @@ 2565414373E6F68005966B8E /* SecureBackup */, 70B74A432C241E56A7ACE610 /* Settings */, EC4545C7E37E8294D3FE6800 /* StartChatScreen */, + 93C7520ED23C9598BB144DBB /* UserProfileScreen */, ); path = Screens; sourceTree = ""; @@ -6504,6 +6535,11 @@ 80DEA2A4B20F9E279EAE6B2B /* UserProfile+Mock.swift in Sources */, ED90A59F068FD0CA27E602ED /* UserProfileListRow.swift in Sources */, E21FE4C5B614F311C0955859 /* UserProfileProxy.swift in Sources */, + 1A3B073568D1DC8F76F1F3A0 /* UserProfileScreen.swift in Sources */, + 6AEB650311F694A5702255C9 /* UserProfileScreenCoordinator.swift in Sources */, + D4CB979EB4FE26AAD9F9A72B /* UserProfileScreenModels.swift in Sources */, + AC1DB27A4134470846BE49F6 /* UserProfileScreenViewModel.swift in Sources */, + 46A183C6125A669AEB005699 /* UserProfileScreenViewModelProtocol.swift in Sources */, 8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */, 4A618590DEB72C4F186BFED4 /* UserSessionFlowCoordinator.swift in Sources */, 3113065AABBC14CEAE6843FA /* UserSessionFlowCoordinatorStateMachine.swift in Sources */, diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index b783e278b..a789f5854 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -185,6 +185,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { return .roomMemberDetails(userID: userID, fromRoomMembersList: true) case (.roomMemberDetails(_, let fromRoomMembersList), .dismissRoomMemberDetails): return fromRoomMembersList ? .roomMembersList : .room + + case (.roomMemberDetails(_, fromRoomMembersList: false), .presentUserProfile(let userID)): + return .userProfile(userID: userID) + case (.userProfile, .dismissUserProfile): + return .room case (.roomDetails, .presentInviteUsersScreen): return .inviteUsersScreen(fromRoomMembersList: false) @@ -304,6 +309,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { presentRoomMemberDetails(userID: userID) case (.roomMemberDetails, .dismissRoomMemberDetails, .roomMembersList): break + + case (.roomMemberDetails, .presentUserProfile(let userID), .userProfile): + replaceRoomMemberDetailsWithUserProfile(userID: userID) + case (.userProfile, .dismissUserProfile, .room): + break case (.roomDetails, .presentInviteUsersScreen, .inviteUsersScreen): presentInviteUsersScreen() @@ -874,35 +884,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { coordinator.actions.sink { [weak self] action in guard let self else { return } switch action { + case .openUserProfile: + stateMachine.tryEvent(.presentUserProfile(userID: userID)) case .openDirectChat(let displayName): - let loadingIndicatorIdentifier = "OpenDirectChatLoadingIndicator" - - userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, - type: .modal(progress: .indeterminate, interactiveDismissDisabled: true, allowsInteraction: false), - title: L10n.commonLoading, - persistent: true)) - - Task { [weak self] in - guard let self else { return } - - let currentDirectRoom = await userSession.clientProxy.directRoomForUserID(userID) - switch currentDirectRoom { - case .success(.some(let roomID)): - stateMachine.tryEvent(.presentChildRoom(roomID: roomID)) - case .success(nil): - switch await userSession.clientProxy.createDirectRoom(with: userID, expectedRoomName: displayName) { - case .success(let roomID): - analytics.trackCreatedRoom(isDM: true) - stateMachine.tryEvent(.presentChildRoom(roomID: roomID)) - case .failure: - userIndicatorController.alertInfo = .init(id: UUID()) - } - case .failure: - userIndicatorController.alertInfo = .init(id: UUID()) - } - - userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier) - } + openDirectChat(with: userID, displayName: displayName) } } .store(in: &cancellables) @@ -912,6 +897,60 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } + private func replaceRoomMemberDetailsWithUserProfile(userID: String) { + let parameters = UserProfileScreenCoordinatorParameters(userID: userID, + clientProxy: userSession.clientProxy, + mediaProvider: userSession.mediaProvider, + userIndicatorController: userIndicatorController) + let coordinator = UserProfileScreenCoordinator(parameters: parameters) + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + + switch action { + case .openDirectChat(let displayName): + openDirectChat(with: userID, displayName: displayName) + } + } + .store(in: &cancellables) + + // Replace the RoomMemberDetailsScreen without any animation. + navigationStackCoordinator.pop(animated: false) + navigationStackCoordinator.push(coordinator, animated: false) { [weak self] in + self?.stateMachine.tryEvent(.dismissUserProfile) + } + } + + private func openDirectChat(with userID: String, displayName: String?) { + let loadingIndicatorIdentifier = "OpenDirectChatLoadingIndicator" + + userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, + type: .modal(progress: .indeterminate, interactiveDismissDisabled: true, allowsInteraction: false), + title: L10n.commonLoading, + persistent: true)) + + Task { [weak self] in + guard let self else { return } + + let currentDirectRoom = await userSession.clientProxy.directRoomForUserID(userID) + switch currentDirectRoom { + case .success(.some(let roomID)): + stateMachine.tryEvent(.presentChildRoom(roomID: roomID)) + case .success(nil): + switch await userSession.clientProxy.createDirectRoom(with: userID, expectedRoomName: displayName) { + case .success(let roomID): + analytics.trackCreatedRoom(isDM: true) + stateMachine.tryEvent(.presentChildRoom(roomID: roomID)) + case .failure: + userIndicatorController.alertInfo = .init(id: UUID()) + } + case .failure: + userIndicatorController.alertInfo = .init(id: UUID()) + } + + userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier) + } + } + private func presentMessageForwarding(for itemID: TimelineItemIdentifier) { guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider, let eventID = itemID.eventID else { fatalError() @@ -1160,6 +1199,7 @@ private extension RoomFlowCoordinator { case globalNotificationSettings case roomMembersList case roomMemberDetails(userID: String, fromRoomMembersList: Bool) + case userProfile(userID: String) case inviteUsersScreen(fromRoomMembersList: Bool) case mediaUploadPicker(source: MediaPickerScreenSource) case mediaUploadPreview(fileURL: URL) @@ -1207,6 +1247,9 @@ private extension RoomFlowCoordinator { case presentRoomMemberDetails(userID: String) case dismissRoomMemberDetails + case presentUserProfile(userID: String) + case dismissUserProfile + case presentInviteUsersScreen case dismissInviteUsersScreen diff --git a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift index abf981a03..84eefce5b 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/AvatarHeaderView.swift @@ -58,6 +58,22 @@ struct AvatarHeaderView: View { self.onAvatarTap = onAvatarTap self.footer = footer } + + init(user: UserProfileProxy, + avatarSize: AvatarSize, + imageProvider: ImageProviderProtocol? = nil, + onAvatarTap: (() -> Void)? = nil, + @ViewBuilder footer: @escaping () -> Footer) { + id = user.userID + name = user.displayName + subtitle = user.displayName == nil ? nil : user.userID + avatarURL = user.avatarURL + + self.avatarSize = avatarSize + self.imageProvider = imageProvider + self.onAvatarTap = onAvatarTap + self.footer = footer + } var body: some View { VStack(spacing: 8.0) { diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift index 9018c2da8..a8b749748 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift @@ -26,6 +26,7 @@ struct RoomMemberDetailsScreenCoordinatorParameters { } enum RoomMemberDetailsScreenCoordinatorAction { + case openUserProfile case openDirectChat(displayName: String?) } @@ -52,6 +53,8 @@ final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol { guard let self else { return } switch action { + case .openUserProfile: + actionsSubject.send(.openUserProfile) case .openDirectChat(let displayName): actionsSubject.send(.openDirectChat(displayName: displayName)) } diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift index f5255ef71..7c312a819 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift @@ -17,6 +17,7 @@ import Foundation enum RoomMemberDetailsScreenViewModelAction { + case openUserProfile case openDirectChat(displayName: String?) } diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift index 7e5c305a8..9a6b5593b 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift @@ -59,8 +59,8 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro state.memberDetails = RoomMemberDetails(withProxy: member) state.isOwnMemberDetails = member.userID == roomProxy.ownUserID case .failure(let error): - state.bindings.alertInfo = .init(id: .unknown) - MXLog.error("[RoomFlowCoordinator] Failed to get member: \(error)") + MXLog.warning("Failed to find member: \(error)") + actionsSubject.send(.openUserProfile) } } } diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift index 10159ec7a..87f18625a 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift @@ -21,19 +21,6 @@ struct RoomMemberDetailsScreen: View { @ObservedObject var context: RoomMemberDetailsScreenViewModel.Context var body: some View { - content - .compoundList() - .navigationTitle(L10n.screenRoomMemberDetailsTitle) - .alert(item: $context.ignoreUserAlert, actions: blockUserAlertActions, message: blockUserAlertMessage) - .alert(item: $context.alertInfo) - .track(screen: .User) - .interactiveQuickLook(item: $context.mediaPreviewItem, shouldHideControls: true) - } - - // MARK: - Private - - @ViewBuilder - private var content: some View { Form { headerSection @@ -42,8 +29,16 @@ struct RoomMemberDetailsScreen: View { blockUserSection } } + .compoundList() + .navigationTitle(L10n.screenRoomMemberDetailsTitle) + .alert(item: $context.ignoreUserAlert, actions: blockUserAlertActions, message: blockUserAlertMessage) + .alert(item: $context.alertInfo) + .track(screen: .User) + .interactiveQuickLook(item: $context.mediaPreviewItem, shouldHideControls: true) } + // MARK: - Private + @ViewBuilder private var headerSection: some View { if let memberDetails = context.viewState.memberDetails { @@ -63,7 +58,7 @@ struct RoomMemberDetailsScreen: View { } } } else { - AvatarHeaderView(member: .init(loading: context.viewState.userID), + AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID), avatarSize: .user(on: .memberDetails), imageProvider: context.imageProvider, footer: { }) diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenCoordinator.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenCoordinator.swift new file mode 100644 index 000000000..d435bcced --- /dev/null +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenCoordinator.swift @@ -0,0 +1,67 @@ +// +// Copyright 2022 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 Combine +import SwiftUI + +struct UserProfileScreenCoordinatorParameters { + let userID: String + let clientProxy: ClientProxyProtocol + let mediaProvider: MediaProviderProtocol + let userIndicatorController: UserIndicatorControllerProtocol +} + +enum UserProfileScreenCoordinatorAction { + case openDirectChat(displayName: String?) +} + +final class UserProfileScreenCoordinator: CoordinatorProtocol { + private var viewModel: UserProfileScreenViewModelProtocol + + private var cancellables = Set() + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: UserProfileScreenCoordinatorParameters) { + viewModel = UserProfileScreenViewModel(userID: parameters.userID, + clientProxy: parameters.clientProxy, + mediaProvider: parameters.mediaProvider, + userIndicatorController: parameters.userIndicatorController) + } + + func start() { + viewModel.actionsPublisher.sink { [weak self] action in + guard let self else { return } + + switch action { + case .openDirectChat(let displayName): + actionsSubject.send(.openDirectChat(displayName: displayName)) + } + } + .store(in: &cancellables) + } + + func stop() { + viewModel.stop() + } + + func toPresentable() -> AnyView { + AnyView(UserProfileScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift new file mode 100644 index 000000000..9f911d41b --- /dev/null +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift @@ -0,0 +1,47 @@ +// +// Copyright 2022 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 Foundation + +enum UserProfileScreenViewModelAction { + case openDirectChat(displayName: String?) +} + +struct UserProfileScreenViewState: BindableState { + let userID: String + let isOwnUser: Bool + + var userProfile: UserProfileProxy? + var permalink: URL? + + var bindings: UserProfileScreenViewStateBindings +} + +struct UserProfileScreenViewStateBindings { + var alertInfo: AlertInfo? + + /// A media item that will be previewed with QuickLook. + var mediaPreviewItem: MediaPreviewItem? +} + +enum UserProfileScreenViewAction { + case displayAvatar + case openDirectChat +} + +enum UserProfileScreenError: Hashable { + case unknown +} diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift new file mode 100644 index 000000000..817037ee9 --- /dev/null +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift @@ -0,0 +1,119 @@ +// +// Copyright 2022 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 Combine +import MatrixRustSDK +import SwiftUI + +typealias UserProfileScreenViewModelType = StateStoreViewModel + +class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScreenViewModelProtocol { + private let clientProxy: ClientProxyProtocol + private let mediaProvider: MediaProviderProtocol + private let userIndicatorController: UserIndicatorControllerProtocol + + private var actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(userID: String, + clientProxy: ClientProxyProtocol, + mediaProvider: MediaProviderProtocol, + userIndicatorController: UserIndicatorControllerProtocol) { + self.clientProxy = clientProxy + self.mediaProvider = mediaProvider + self.userIndicatorController = userIndicatorController + + let initialViewState = UserProfileScreenViewState(userID: userID, + isOwnUser: userID == clientProxy.userID, + bindings: .init()) + + super.init(initialViewState: initialViewState, imageProvider: mediaProvider) + + showMemberLoadingIndicator() + Task { + defer { + hideMemberLoadingIndicator() + } + + switch await clientProxy.profile(for: userID) { + case .success(let userProfile): + state.userProfile = userProfile + state.permalink = (try? matrixToUserPermalink(userId: userID)).flatMap(URL.init(string:)) + case .failure(let error): + state.bindings.alertInfo = .init(id: .unknown) + MXLog.error("Failed to find user profile: \(error)") + } + } + } + + // MARK: - Public + + func stop() { + // Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier. + state.bindings.mediaPreviewItem = nil + + hideMemberLoadingIndicator() + } + + override func process(viewAction: UserProfileScreenViewAction) { + switch viewAction { + case .displayAvatar: + displayFullScreenAvatar() + case .openDirectChat: + guard let userProfile = state.userProfile else { fatalError() } + actionsSubject.send(.openDirectChat(displayName: userProfile.displayName)) + } + } + + // MARK: - Private + + private func displayFullScreenAvatar() { + guard let userProfile = state.userProfile else { fatalError() } + guard let avatarURL = userProfile.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: userProfile.displayName) + } + } + } + + // MARK: Loading indicator + + private static let loadingIndicatorIdentifier = "\(UserProfileScreenViewModel.self)-Loading" + + private func showMemberLoadingIndicator() { + userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier, + type: .modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: true), + title: L10n.commonLoading, + persistent: true), + delay: .milliseconds(100)) + } + + private func hideMemberLoadingIndicator() { + userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) + } +} diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModelProtocol.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModelProtocol.swift new file mode 100644 index 000000000..c88587899 --- /dev/null +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModelProtocol.swift @@ -0,0 +1,25 @@ +// +// Copyright 2022 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 Combine + +@MainActor +protocol UserProfileScreenViewModelProtocol { + var actionsPublisher: AnyPublisher { get } + var context: UserProfileScreenViewModelType.Context { get } + + func stop() +} diff --git a/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift b/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift new file mode 100644 index 000000000..1f33afe7e --- /dev/null +++ b/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift @@ -0,0 +1,99 @@ +// +// Copyright 2022 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 UserProfileScreen: View { + @ObservedObject var context: UserProfileScreenViewModel.Context + + var body: some View { + Form { + headerSection + + if context.viewState.userProfile != nil, !context.viewState.isOwnUser { + directChatSection + } + } + .compoundList() + .navigationTitle(L10n.screenRoomMemberDetailsTitle) + .alert(item: $context.alertInfo) + .track(screen: .User) + .interactiveQuickLook(item: $context.mediaPreviewItem, shouldHideControls: true) + } + + // MARK: - Private + + @ViewBuilder + private var headerSection: some View { + if let userProfile = context.viewState.userProfile { + AvatarHeaderView(user: userProfile, + avatarSize: .user(on: .memberDetails), + imageProvider: context.imageProvider) { + context.send(viewAction: .displayAvatar) + } footer: { + if let permalink = context.viewState.permalink { + HStack(spacing: 32) { + ShareLink(item: permalink) { + CompoundIcon(\.shareIos) + } + .buttonStyle(FormActionButtonStyle(title: L10n.actionShare)) + } + .padding(.top, 32) + } + } + } else { + AvatarHeaderView(user: UserProfileProxy(userID: context.viewState.userID), + avatarSize: .user(on: .memberDetails), + imageProvider: context.imageProvider, + footer: { }) + } + } + + private var directChatSection: some View { + Section { + ListRow(label: .default(title: L10n.commonDirectChat, + icon: \.chat), + kind: .button { + context.send(viewAction: .openDirectChat) + }) + .accessibilityIdentifier(A11yIdentifiers.roomMemberDetailsScreen.directChat) + } + } +} + +// MARK: - Previews + +struct UserProfileScreen_Previews: PreviewProvider, TestablePreview { + static let otherUserViewModel = makeViewModel(userID: RoomMemberProxyMock.mockDan.userID) + static let accountOwnerViewModel = makeViewModel(userID: RoomMemberProxyMock.mockMe.userID) + + static var previews: some View { + UserProfileScreen(context: otherUserViewModel.context) + .previewDisplayName("Other User") + .snapshot(delay: 0.25) + UserProfileScreen(context: accountOwnerViewModel.context) + .previewDisplayName("Account Owner") + .snapshot(delay: 0.25) + } + + static func makeViewModel(userID: String) -> UserProfileScreenViewModel { + UserProfileScreenViewModel(userID: userID, + clientProxy: ClientProxyMock(.init()), + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) + } +} diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift index 4c643ba8c..671602f7b 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift @@ -48,18 +48,6 @@ extension RoomMemberDetails { isBanned = proxy.membership == .ban role = .init(proxy.role) } - - init(loading id: String) { - self.id = id - name = nil - avatarURL = nil - permalink = nil - - isInvited = false - isIgnored = false - isBanned = false - role = .user - } } extension RoomMemberDetails.Role { diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-en-GB.Account-Owner.png b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-en-GB.Account-Owner.png new file mode 100644 index 000000000..ec29371fb --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-en-GB.Account-Owner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e42a43a6820048238e353125067d7efc2c32c77dc5633a50a2060b468eceeefb +size 102403 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-en-GB.Other-User.png b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-en-GB.Other-User.png new file mode 100644 index 000000000..17f1c3e7a --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-en-GB.Other-User.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1cf3ad4bcc2e661971a8019be778757b640a85bed8debf7b6d31003325f2e05 +size 112587 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-pseudo.Account-Owner.png b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-pseudo.Account-Owner.png new file mode 100644 index 000000000..7d94084f0 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-pseudo.Account-Owner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f9378f6ee8fe77ec5038cf87a77fad3261a79c1addb785e04db12b8a9b59003 +size 105673 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-pseudo.Other-User.png b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-pseudo.Other-User.png new file mode 100644 index 000000000..6e2704471 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPad-pseudo.Other-User.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9a78887dd1a44c5d39c92c4e474c9bab5c5a858768bb990300cc40d80501561 +size 121404 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-15-en-GB.Account-Owner.png b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-15-en-GB.Account-Owner.png new file mode 100644 index 000000000..675814f84 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-15-en-GB.Account-Owner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcb33f529761684c7564df444d2808ea72a1e6eee290c349fce6bda2e239344d +size 54899 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-15-en-GB.Other-User.png b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-15-en-GB.Other-User.png new file mode 100644 index 000000000..65b2bc17e --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-15-en-GB.Other-User.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d555f65dd5a7296e1da5798ed7ff710b6bbd652b2dc801d1aa4decbe3a064ba3 +size 62749 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-15-pseudo.Account-Owner.png b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-15-pseudo.Account-Owner.png new file mode 100644 index 000000000..0a328ef68 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-15-pseudo.Account-Owner.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c7715d26bb104bb82c18ccc6ea4d363e833fe91f84219ecee57f2b66ca857c7 +size 57234 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-15-pseudo.Other-User.png b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-15-pseudo.Other-User.png new file mode 100644 index 000000000..7d866f3ed --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_userProfileScreen-iPhone-15-pseudo.Other-User.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0d240b2ab4fbaaa18ea8817dcaf6ac33725cc2f73dea06c372336288bc04a0a +size 69921 diff --git a/changelog.d/2634.change b/changelog.d/2634.change new file mode 100644 index 000000000..2c9f901e5 --- /dev/null +++ b/changelog.d/2634.change @@ -0,0 +1 @@ +Add a UserProfileScreen to handle permalinks for users that aren't in the current room. \ No newline at end of file