From 1a47fd1a8d7e0cea88da4d423504abf3c4768b61 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 19 Dec 2022 15:39:33 +0300 Subject: [PATCH] Room details screen (#348) * Create screen module * Display details on header tap * Add strings * Add room avatar size on details * Update members method signature * Use room proxy values * Create room members screen * Display room members on tap * Update previews and mock room proxy * Commit project file * Various tweaks and cleanup following code review * More small tweaks + UI and screenshot tests Co-authored-by: Stefan Ceriu --- ElementX.xcodeproj/project.pbxproj | 96 ++++++++++ .../en.lproj/Untranslated.strings | 4 + .../Generated/Strings+Untranslated.swift | 4 + ElementX/Sources/Other/AvatarSize.swift | 6 + .../RoomDetails/RoomDetailsCoordinator.swift | 72 ++++++++ .../RoomDetails/RoomDetailsModels.swift | 73 ++++++++ .../RoomDetails/RoomDetailsViewModel.swift | 75 ++++++++ .../RoomDetailsViewModelProtocol.swift | 23 +++ .../RoomDetails/View/RoomDetailsScreen.swift | 171 ++++++++++++++++++ .../RoomMembers/RoomMembersCoordinator.swift | 55 ++++++ .../RoomMembers/RoomMembersModels.swift | 50 +++++ .../RoomMembers/RoomMembersViewModel.swift | 78 ++++++++ .../RoomMembersViewModelProtocol.swift | 23 +++ .../View/RoomMembersMemberCell.swift | 84 +++++++++ .../RoomMembers/View/RoomMembersScreen.swift | 61 +++++++ .../RoomScreen/RoomScreenCoordinator.swift | 26 ++- .../Screens/RoomScreen/RoomScreenModels.swift | 2 + .../RoomScreen/RoomScreenViewModel.swift | 2 + .../RoomScreen/View/RoomHeaderView.swift | 16 +- .../Sources/Services/Room/MockRoomProxy.swift | 25 ++- .../Services/Room/RoomMemberProxy.swift | 93 ++++++++++ .../Sources/Services/Room/RoomProxy.swift | 11 ++ .../Services/Room/RoomProxyProtocol.swift | 3 + .../RoomTimelineController.swift | 2 +- .../UserSessionFlowCoordinator.swift | 3 +- .../UITests/UITestScreenIdentifier.swift | 2 + .../UITests/UITestsAppCoordinator.swift | 31 +++- .../Sources/RoomDetailsScreenUITests.swift | 27 +++ .../Sources/RoomMembersScreenUITests.swift | 27 +++ ...-iPad-9th-generation.roomDetailsScreen.png | 3 + ...-iPad-9th-generation.roomMembersScreen.png | 3 + .../de-DE-iPhone-14.roomDetailsScreen.png | 3 + .../de-DE-iPhone-14.roomMembersScreen.png | 3 + ...-iPad-9th-generation.roomDetailsScreen.png | 3 + ...-iPad-9th-generation.roomMembersScreen.png | 3 + .../en-GB-iPhone-14.roomDetailsScreen.png | 3 + .../en-GB-iPhone-14.roomMembersScreen.png | 3 + ...-iPad-9th-generation.roomDetailsScreen.png | 3 + ...-iPad-9th-generation.roomMembersScreen.png | 3 + .../fr-FR-iPhone-14.roomDetailsScreen.png | 3 + .../fr-FR-iPhone-14.roomMembersScreen.png | 3 + .../Sources/RoomDetailsViewModelTests.swift | 22 +++ .../Sources/RoomMembersViewModelTests.swift | 22 +++ 43 files changed, 1192 insertions(+), 33 deletions(-) create mode 100644 ElementX/Sources/Screens/RoomDetails/RoomDetailsCoordinator.swift create mode 100644 ElementX/Sources/Screens/RoomDetails/RoomDetailsModels.swift create mode 100644 ElementX/Sources/Screens/RoomDetails/RoomDetailsViewModel.swift create mode 100644 ElementX/Sources/Screens/RoomDetails/RoomDetailsViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/RoomDetails/View/RoomDetailsScreen.swift create mode 100644 ElementX/Sources/Screens/RoomMembers/RoomMembersCoordinator.swift create mode 100644 ElementX/Sources/Screens/RoomMembers/RoomMembersModels.swift create mode 100644 ElementX/Sources/Screens/RoomMembers/RoomMembersViewModel.swift create mode 100644 ElementX/Sources/Screens/RoomMembers/RoomMembersViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/RoomMembers/View/RoomMembersMemberCell.swift create mode 100644 ElementX/Sources/Screens/RoomMembers/View/RoomMembersScreen.swift create mode 100644 ElementX/Sources/Services/Room/RoomMemberProxy.swift create mode 100644 UITests/Sources/RoomDetailsScreenUITests.swift create mode 100644 UITests/Sources/RoomMembersScreenUITests.swift create mode 100644 UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.roomDetailsScreen.png create mode 100644 UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.roomMembersScreen.png create mode 100644 UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.roomDetailsScreen.png create mode 100644 UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.roomMembersScreen.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreen.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMembersScreen.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreen.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMembersScreen.png create mode 100644 UITests/Sources/__Snapshots__/Application/fr-FR-iPad-9th-generation.roomDetailsScreen.png create mode 100644 UITests/Sources/__Snapshots__/Application/fr-FR-iPad-9th-generation.roomMembersScreen.png create mode 100644 UITests/Sources/__Snapshots__/Application/fr-FR-iPhone-14.roomDetailsScreen.png create mode 100644 UITests/Sources/__Snapshots__/Application/fr-FR-iPhone-14.roomMembersScreen.png create mode 100644 UnitTests/Sources/RoomDetailsViewModelTests.swift create mode 100644 UnitTests/Sources/RoomMembersViewModelTests.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 00d0cb5f8..769e124ad 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -66,6 +66,7 @@ 1B4B3E847BF944DB2C1C217F /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; }; 1CF18DE71D5D23C61BD88852 /* DebugScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9238D3A3A00F45E841FE4EFF /* DebugScreen.swift */; }; 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */; }; + 1E2298F15121667E36378F32 /* RoomDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38B7319C1D6508702B98A8F6 /* RoomDetailsScreen.swift */; }; 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; }; 1F04C63D4FA95948E3F52147 /* FileRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */; }; 1F3232BD368DF430AB433907 /* DesignKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5A56C4F47C368EBE5C5E870 /* DesignKit */; }; @@ -76,6 +77,7 @@ 214CDBF0C783155242FFE4A0 /* NotificationItemProxy+NSE.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1FBF8CA40199B8058B1F08 /* NotificationItemProxy+NSE.swift */; }; 2276870A19F34B3FFFDA690F /* SoftLogoutCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AEA20A6B4883E60469ACF8F /* SoftLogoutCoordinator.swift */; }; 2352C541AF857241489756FF /* MockRoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */; }; + 235819701B166DFBA21994F1 /* RoomMembersViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33AECDFE83A880328CCFD0B1 /* RoomMembersViewModelTests.swift */; }; 237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; }; 23B2CD5A06B16055BDDD0994 /* ApplicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D8C8431416EB8DFEC7E235 /* ApplicationTests.swift */; }; 24906A1E82D0046655958536 /* MessageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CF12478983A5EB390FB26 /* MessageComposer.swift */; }; @@ -179,9 +181,11 @@ 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; 5F1FDE49DFD0C680386E48F9 /* TemplateViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B80895CE021B49847BD7D74 /* TemplateViewModelProtocol.swift */; }; 5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; }; + 60E838D870A4969D9BADF1BC /* RoomMembersViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD38C315F6576DB898EA9529 /* RoomMembersViewModelProtocol.swift */; }; 60ED66E63A169E47489348A8 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 2B788C81F6369D164ADEB917 /* GZIP */; }; 6126CC51654E159804999E6A /* UNMutableNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5741CD0691019B32FE74CE9E /* UNMutableNotificationContent.swift */; }; 617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */; }; + 626367FC2416CA4E25E15383 /* RoomMembersScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F910035944E5F38F4F801E /* RoomMembersScreenUITests.swift */; }; 6298AB0906DDD3525CD78C6B /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 9573B94B1C86C6DF751AF3FD /* SwiftState */; }; 630E89EBB0F791208EEE6D11 /* FileRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00A7110B937C6AE2EF5D7D6 /* FileRoomTimelineItem.swift */; }; 63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB10E673916D2B8D21FD197 /* TemplateModels.swift */; }; @@ -223,6 +227,7 @@ 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; }; 758BF44CA565AB0AB84F2185 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; }; 75EA4ABBFAA810AFF289D6F4 /* TemplateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB6E40BAD4504D899FAAC9A /* TemplateViewModel.swift */; }; + 77119672143B2BF0C9838DDC /* RoomDetailsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC031D32CED2CBE122E5038 /* RoomDetailsModels.swift */; }; 7732B2F635626BE1C1CD92A4 /* UIActivityViewControllerWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A57A4AFA6A068668AFBD070 /* UIActivityViewControllerWrapper.swift */; }; 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; }; 77D7DAA41AAB36800C1F2E2D /* RoomTimelineProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */; }; @@ -246,6 +251,7 @@ 8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */; }; 80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */; }; 8196A2E71ACC902DD69F24EE /* UserNotificationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */; }; + 829062DD3C3F7016FE1A6476 /* RoomDetailsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */; }; 83E5054739949181CA981193 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */; }; 85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */; }; 86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */; }; @@ -269,6 +275,7 @@ 91DFCB641FBA03EE2DA0189E /* FilePreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB27E1BE894F9F9F0134372 /* FilePreviewScreen.swift */; }; 9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; }; 92B95779840CD749117B3615 /* EmojiMartStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38AE3617D7619EF30CDD229 /* EmojiMartStore.swift */; }; + 930556A6E30010A551A9DB50 /* RoomDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91FB6F5ECCF51ECE98ACFEEC /* RoomDetailsViewModel.swift */; }; 9377D6BE1511E1529EA1662B /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3F82523D6F48B926D6AF68 /* AppSettings.swift */; }; 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */; }; 93BA4A81B6D893271101F9F0 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */; }; @@ -307,10 +314,13 @@ A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; A371629728E597C5FCA3C2B2 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73FC861755C6388F62B9280A /* Analytics.swift */; }; A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287FC98AF2664EAD79C0D902 /* UIDevice.swift */; }; + A382B0EA99888F74B6952019 /* RoomMembersMemberCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22FECA5919BE59C4BB5AEB22 /* RoomMembersMemberCell.swift */; }; + A3CE203A923A4DD0E9371FB9 /* RoomMembersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E254EA67FB16AB33F9F9B18D /* RoomMembersScreen.swift */; }; A440D4BC02088482EC633A88 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; }; A494741843F087881299ACF0 /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; }; A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; }; A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */; }; + A57A62859AE46AE07281B4AE /* RoomMembersModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5734DF8967F3595F8C784D /* RoomMembersModels.swift */; }; A663FE6704CB500EBE782AE1 /* AnalyticsPromptCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4DE1CF8F5EFD353B1A5E36F /* AnalyticsPromptCoordinator.swift */; }; A69A54FF11A3F9EA0660E6BF /* NSE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 0D8F620C8B314840D8602E3F /* NSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; }; @@ -355,12 +365,14 @@ BB6B0B91CE11E06330017000 /* SessionVerificationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */; }; BCEC41FB1F2BB663183863E4 /* LoginServerInfoSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D379E13DD9D987470A3C70C /* LoginServerInfoSection.swift */; }; BD782053BE4C3D2F0BDE5699 /* ServiceLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */; }; + BEEEB659A0BA510D7BE6345C /* RoomMemberProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D1790942BE4FE0D8273191B /* RoomMemberProxy.swift */; }; BFB534E338A3D949944FB2F5 /* NotificationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B490675B8E31423AF116BDA /* NotificationServiceProxy.swift */; }; C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */; }; C3522917C0C367C403429EEC /* CoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */; }; C35CF4DAB1467FE1BBDC204B /* MessageTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAF1C75771D9DC75877F4B4 /* MessageTimelineItem.swift */; }; C4180F418235DAD9DD173951 /* TemplateScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9873076F224E4CE09D8BD47D /* TemplateScreenUITests.swift */; }; C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; }; + C4F784AABFF44E4716E7A8BC /* RoomDetailsViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B3A76EA6AB67910C11330F /* RoomDetailsViewModelProtocol.swift */; }; C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */; }; C74EE50257ED925C2B8EFCE6 /* MockSoftLogoutScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B869438A1B52836F912A702 /* MockSoftLogoutScreenState.swift */; }; C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; }; @@ -374,6 +386,7 @@ CB498F4E27AA0545DCEF0F6F /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4003BC24B24C9E63D3304177 /* DeviceKit */; }; CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; }; CBF64DE774298D773DBD5354 /* VideoPlayerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB634B42CFE667112369D57 /* VideoPlayerScreen.swift */; }; + CC2A6B71E12DDF1EE6ECD299 /* RoomMembersCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F118CF7C5548099AACF7E90C /* RoomMembersCoordinator.swift */; }; CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */; }; CCAA0671B46EAFD0BB528E2C /* apple_emojis_data.json in Resources */ = {isa = PBXBuildFile; fileRef = 8FC26871038FB0E4AAE22605 /* apple_emojis_data.json */; }; CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */; }; @@ -387,6 +400,7 @@ D0619D2E6B9C511190FBEB95 /* RoomMessageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */; }; D2D70B5DB1A5E4AF0CD88330 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 033DB41C51865A2E83174E87 /* target.yml */; }; D3E603A5E9D529CF293E1BF9 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1651A532305027D3F605E2B /* VideoPlayerCoordinator.swift */; }; + D59F046B15AA8E971053C1A6 /* RoomDetailsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813B198AE8833FD12E5A9C78 /* RoomDetailsCoordinator.swift */; }; D5C805F49B2C75DC3793E780 /* EmojiItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */; }; D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; }; D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */; }; @@ -395,6 +409,7 @@ D85D4FA590305180B4A41795 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3073CCD77D906B330BC1D6 /* Tests.swift */; }; D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */; }; + DC2D20609B9B612F6946C3F6 /* RoomMembersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4CF2FC815D26B337E78DA45 /* RoomMembersViewModel.swift */; }; DD9B70DE54B24E0694A35D8A /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */; }; DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */; }; @@ -418,6 +433,7 @@ EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885D8C42DD17625B5261BEFF /* MediaProvider.swift */; }; EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */; }; EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; }; + EA974337FA7D040E7C74FE6E /* RoomDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */; }; EBE13FAB4E29738AC41BD3E5 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; EC280623A42904341363EAAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 886A0A498FA01E8EDD451D05 /* Sentry */; }; EC4C31963E755EEC77BD778C /* AnalyticsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */; }; @@ -577,6 +593,7 @@ 227AC5D71A4CE43512062243 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 22B384D54464FA39C6C7F6E7 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ca; path = ca.lproj/Localizable.stringsdict; sourceTree = ""; }; 22D46DB0CC6C55EBA7AE67A3 /* SoftLogoutViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModel.swift; sourceTree = ""; }; + 22FECA5919BE59C4BB5AEB22 /* RoomMembersMemberCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersMemberCell.swift; sourceTree = ""; }; 233D5F7E5E9F49ABF3413291 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hr; path = hr.lproj/Localizable.stringsdict; sourceTree = ""; }; 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = ""; }; 2583416C8974272ADBADDBE1 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = ""; }; @@ -602,11 +619,13 @@ 2D256FEE2F1AF1E51D39B622 /* LoginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginTests.swift; sourceTree = ""; }; 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemProxy.swift; sourceTree = ""; }; 2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertInfo.swift; sourceTree = ""; }; + 2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsViewModelTests.swift; sourceTree = ""; }; 2F1B28C596DE541DA0AFD16C /* lo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lo; path = lo.lproj/Localizable.stringsdict; sourceTree = ""; }; 31B01468022EC826CB2FD2C0 /* LoginModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginModels.swift; sourceTree = ""; }; 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = ""; }; 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelTests.swift; sourceTree = ""; }; 3340ABAE3A4647E80163AE18 /* TemplateViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewModelTests.swift; sourceTree = ""; }; + 33AECDFE83A880328CCFD0B1 /* RoomMembersViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersViewModelTests.swift; sourceTree = ""; }; 33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; 3429142FE11930422E7CC1A0 /* UserSessionFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinatorStateMachine.swift; sourceTree = ""; }; 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReactionsView.swift; sourceTree = ""; }; @@ -615,12 +634,14 @@ 3747C96188856006F784BF49 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = ko.lproj/Localizable.stringsdict; sourceTree = ""; }; 3782C506F4FF1AADF61B6212 /* tlh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tlh; path = tlh.lproj/Localizable.strings; sourceTree = ""; }; 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItem.swift; sourceTree = ""; }; + 38B7319C1D6508702B98A8F6 /* RoomDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreen.swift; sourceTree = ""; }; 39001365B76B89983FDB7AD8 /* EmojiMartJSONLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartJSONLoader.swift; sourceTree = ""; }; 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadableFrameModifier.swift; sourceTree = ""; }; 399427358A80BA2848E698A2 /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-MX"; path = "es-MX.lproj/Localizable.strings"; sourceTree = ""; }; 39EBB6903EFD4236B8D11A42 /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-CA"; path = "fr-CA.lproj/Localizable.stringsdict"; sourceTree = ""; }; 3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomProxy.swift; sourceTree = ""; }; 3B5B535DA49C54523FF7A412 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nn; path = nn.lproj/Localizable.strings; sourceTree = ""; }; + 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenUITests.swift; sourceTree = ""; }; 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCategory.swift; sourceTree = ""; }; 3CDF9E55650D6035D6536538 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "nb-NO"; path = "nb-NO.lproj/Localizable.stringsdict"; sourceTree = ""; }; 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = ""; }; @@ -719,6 +740,7 @@ 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = ""; }; 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = ""; }; 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModel.swift; sourceTree = ""; }; + 6F5734DF8967F3595F8C784D /* RoomMembersModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersModels.swift; sourceTree = ""; }; 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermalinkBuilderTests.swift; sourceTree = ""; }; 6FC5015B9634698BDB8701AF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = ""; }; 71556206CD5E8B1F53F07178 /* MockRoomTimelineControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineControllerFactory.swift; sourceTree = ""; }; @@ -726,6 +748,7 @@ 71D52BAA5BADB06E5E8C295D /* Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; 72D03D36422177EF01905D20 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = ""; }; 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = ""; }; + 72F910035944E5F38F4F801E /* RoomMembersScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersScreenUITests.swift; sourceTree = ""; }; 73FC861755C6388F62B9280A /* Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = ""; }; 748AE77AC3B0A01223033B87 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 799A3A11C434296ED28F87C8 /* iw */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = iw; path = iw.lproj/Localizable.strings; sourceTree = ""; }; @@ -740,6 +763,7 @@ 7E154FEA1E6FE964D3DF7859 /* fy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fy; path = fy.lproj/Localizable.strings; sourceTree = ""; }; 7E532D95330139D118A9BF88 /* BugReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModel.swift; sourceTree = ""; }; 7FB27E1BE894F9F9F0134372 /* FilePreviewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewScreen.swift; sourceTree = ""; }; + 813B198AE8833FD12E5A9C78 /* RoomDetailsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsCoordinator.swift; sourceTree = ""; }; 8140010A796DB2C7977B6643 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; 8166F121C79C7B62BF01D508 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pt; path = pt.lproj/Localizable.stringsdict; sourceTree = ""; }; 81B17DB1BC3B0C62AF84D230 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; @@ -751,6 +775,7 @@ 873718F8BD17B778C5141C45 /* ta */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ta; path = ta.lproj/Localizable.strings; sourceTree = ""; }; 874A1842477895F199567BD7 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; }; 878B7C1885486FB4BE41631D /* iw */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = iw; path = iw.lproj/Localizable.stringsdict; sourceTree = ""; }; + 87B3A76EA6AB67910C11330F /* RoomDetailsViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsViewModelProtocol.swift; sourceTree = ""; }; 885D8C42DD17625B5261BEFF /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; 8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignedScrollView.swift; sourceTree = ""; }; 8888D13645C04AC9818F5778 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -768,6 +793,7 @@ 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 9010EE0CC913D095887EF36E /* OIDCService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCService.swift; sourceTree = ""; }; 9080CDD3881D0D1B2F280A7C /* MockUserNotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserNotificationController.swift; sourceTree = ""; }; + 91FB6F5ECCF51ECE98ACFEEC /* RoomDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsViewModel.swift; sourceTree = ""; }; 9238D3A3A00F45E841FE4EFF /* DebugScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugScreen.swift; sourceTree = ""; }; 92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTask.swift; sourceTree = ""; }; 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginHomeserver.swift; sourceTree = ""; }; @@ -796,6 +822,7 @@ 9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStackCoordinatorTests.swift; sourceTree = ""; }; 9C7F7DE62D33C6A26CBFCD72 /* IntegrationTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModel.swift; sourceTree = ""; }; + 9D1790942BE4FE0D8273191B /* RoomMemberProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxy.swift; sourceTree = ""; }; 9D7D706FFF438CAF16F44D8C /* ServerSelectionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionCoordinator.swift; sourceTree = ""; }; 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = ""; }; 9F85164F9475FF2867F71AAA /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = ""; }; @@ -815,6 +842,7 @@ A443FAE2EE820A5790C35C8D /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserSession.swift; sourceTree = ""; }; A4B5B19A10D3F7C2BC5315DF /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = ""; }; + A4CF2FC815D26B337E78DA45 /* RoomMembersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersViewModel.swift; sourceTree = ""; }; A5B0B1226DA8DB55918B34CD /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = ""; }; A63A32A0627A03F00A2900FE /* NavigationSplitCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSplitCoordinator.swift; sourceTree = ""; }; A64F0DB78E0AC23C91AD89EF /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = mk.lproj/Localizable.strings; sourceTree = ""; }; @@ -934,6 +962,7 @@ DC77FC5C4F2000133047AA27 /* SoftLogoutModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutModels.swift; sourceTree = ""; }; DCD5FEE195446A9E458DDDAF /* NotificationServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceProxyProtocol.swift; sourceTree = ""; }; DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginCoordinator.swift; sourceTree = ""; }; + DEC031D32CED2CBE122E5038 /* RoomDetailsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsModels.swift; sourceTree = ""; }; DED59F9EFF273BFA2055FFDF /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = ""; }; DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModelTests.swift; sourceTree = ""; }; DF38B69D2C331A499276F400 /* FilePreviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewViewModelTests.swift; sourceTree = ""; }; @@ -942,6 +971,7 @@ E157152B11E347F735C3FD6E /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Localizable.stringsdict; sourceTree = ""; }; E18CF12478983A5EB390FB26 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = ""; }; + E254EA67FB16AB33F9F9B18D /* RoomMembersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersScreen.swift; sourceTree = ""; }; E26747B3154A5DBC3A7E24A5 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = ""; }; E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = ""; }; @@ -978,6 +1008,7 @@ F012CB5EE3F2B67359F6CC52 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetectorTests.swift; sourceTree = ""; }; F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBugReportService.swift; sourceTree = ""; }; + F118CF7C5548099AACF7E90C /* RoomMembersCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersCoordinator.swift; sourceTree = ""; }; F15BE37BE2FB86E00C8D150A /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = ""; }; F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = ""; }; F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1000,6 +1031,7 @@ FAB10E673916D2B8D21FD197 /* TemplateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModels.swift; sourceTree = ""; }; FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = ""; }; FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProxyProtocol.swift; sourceTree = ""; }; + FD38C315F6576DB898EA9529 /* RoomMembersViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersViewModelProtocol.swift; sourceTree = ""; }; FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1354,6 +1386,7 @@ isa = PBXGroup; children = ( 3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */, + 9D1790942BE4FE0D8273191B /* RoomMemberProxy.swift */, FA154570F693D93513E584C1 /* RoomMessageFactory.swift */, 96F37AB24AF5A006521D38D1 /* RoomMessageFactoryProtocol.swift */, A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */, @@ -1653,6 +1686,8 @@ 9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */, 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */, 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */, + 2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */, + 33AECDFE83A880328CCFD0B1 /* RoomMembersViewModelTests.swift */, 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */, EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */, @@ -1760,6 +1795,18 @@ path = View; sourceTree = ""; }; + 7B29CA1D663299262BEADF24 /* RoomDetails */ = { + isa = PBXGroup; + children = ( + 813B198AE8833FD12E5A9C78 /* RoomDetailsCoordinator.swift */, + DEC031D32CED2CBE122E5038 /* RoomDetailsModels.swift */, + 91FB6F5ECCF51ECE98ACFEEC /* RoomDetailsViewModel.swift */, + 87B3A76EA6AB67910C11330F /* RoomDetailsViewModelProtocol.swift */, + 9A67E5629C207A43043FAF20 /* View */, + ); + path = RoomDetails; + sourceTree = ""; + }; 8039515BAA53B7C3275AC64A /* Client */ = { isa = PBXGroup; children = ( @@ -1801,6 +1848,15 @@ path = Proxy; sourceTree = ""; }; + 83CA952B8D738B8E810F569D /* View */ = { + isa = PBXGroup; + children = ( + 22FECA5919BE59C4BB5AEB22 /* RoomMembersMemberCell.swift */, + E254EA67FB16AB33F9F9B18D /* RoomMembersScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 864330656491EBAADA4901D3 /* Sources */ = { isa = PBXGroup; children = ( @@ -1872,6 +1928,8 @@ 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */, 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */, 0C88046D6A070D9827181C4D /* OnboardingUITests.swift */, + 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */, + 72F910035944E5F38F4F801E /* RoomMembersScreenUITests.swift */, 086B997409328F091EBA43CE /* RoomScreenUITests.swift */, 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */, 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */, @@ -1916,6 +1974,14 @@ path = Templates; sourceTree = ""; }; + 9A67E5629C207A43043FAF20 /* View */ = { + isa = PBXGroup; + children = ( + 38B7319C1D6508702B98A8F6 /* RoomDetailsScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 9D54059E4E42176B3ABB729F /* View */ = { isa = PBXGroup; children = ( @@ -2214,6 +2280,8 @@ B53CA9BECD3F97805E1432D0 /* HomeScreen */, 3F38EAC92E2281990E65DAF2 /* OnboardingScreen */, A448A3A8F764174C60CD0CA1 /* Other */, + 7B29CA1D663299262BEADF24 /* RoomDetails */, + F363C3EEA04EE9F4584B060C /* RoomMembers */, 679E9837ECA8D6776079D16E /* RoomScreen */, D958761758AA1110476DE6A3 /* SessionVerification */, 70B74A432C241E56A7ACE610 /* Settings */, @@ -2268,6 +2336,18 @@ path = Background; sourceTree = ""; }; + F363C3EEA04EE9F4584B060C /* RoomMembers */ = { + isa = PBXGroup; + children = ( + F118CF7C5548099AACF7E90C /* RoomMembersCoordinator.swift */, + 6F5734DF8967F3595F8C784D /* RoomMembersModels.swift */, + A4CF2FC815D26B337E78DA45 /* RoomMembersViewModel.swift */, + FD38C315F6576DB898EA9529 /* RoomMembersViewModelProtocol.swift */, + 83CA952B8D738B8E810F569D /* View */, + ); + path = RoomMembers; + sourceTree = ""; + }; F5A65D1D3B83593598DC278D /* EmojiPickerScreen */ = { isa = PBXGroup; children = ( @@ -2766,6 +2846,8 @@ 4BB282209EA82015D0DF8F89 /* NavigationStackCoordinatorTests.swift in Sources */, F9F6D2883BBEBB9A3789A137 /* OnboardingViewModelTests.swift in Sources */, 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */, + EA974337FA7D040E7C74FE6E /* RoomDetailsViewModelTests.swift in Sources */, + 235819701B166DFBA21994F1 /* RoomMembersViewModelTests.swift in Sources */, 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */, 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */, @@ -2950,7 +3032,19 @@ 00EA14F62DCEF62CDE4808D6 /* RedactedRoomTimelineItem.swift in Sources */, 13853973A5E24374FCEDE8A3 /* RedactedRoomTimelineView.swift in Sources */, A494741843F087881299ACF0 /* RestorationToken.swift in Sources */, + D59F046B15AA8E971053C1A6 /* RoomDetailsCoordinator.swift in Sources */, + 77119672143B2BF0C9838DDC /* RoomDetailsModels.swift in Sources */, + 1E2298F15121667E36378F32 /* RoomDetailsScreen.swift in Sources */, + 930556A6E30010A551A9DB50 /* RoomDetailsViewModel.swift in Sources */, + C4F784AABFF44E4716E7A8BC /* RoomDetailsViewModelProtocol.swift in Sources */, 04A16B45228F7678A027C079 /* RoomHeaderView.swift in Sources */, + BEEEB659A0BA510D7BE6345C /* RoomMemberProxy.swift in Sources */, + CC2A6B71E12DDF1EE6ECD299 /* RoomMembersCoordinator.swift in Sources */, + A382B0EA99888F74B6952019 /* RoomMembersMemberCell.swift in Sources */, + A57A62859AE46AE07281B4AE /* RoomMembersModels.swift in Sources */, + A3CE203A923A4DD0E9371FB9 /* RoomMembersScreen.swift in Sources */, + DC2D20609B9B612F6946C3F6 /* RoomMembersViewModel.swift in Sources */, + 60E838D870A4969D9BADF1BC /* RoomMembersViewModelProtocol.swift in Sources */, FE79E2BCCF69E8BF4D21E15A /* RoomMessageFactory.swift in Sources */, 8D9F646387DF656EF91EE4CB /* RoomMessageFactoryProtocol.swift in Sources */, D0619D2E6B9C511190FBEB95 /* RoomMessageProtocol.swift in Sources */, @@ -3087,6 +3181,8 @@ A823A4E8CB71D7D9743E7E95 /* InfoPlistReader.swift in Sources */, 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */, 6B15FF984906AAFCF9DC4F58 /* OnboardingUITests.swift in Sources */, + 829062DD3C3F7016FE1A6476 /* RoomDetailsScreenUITests.swift in Sources */, + 626367FC2416CA4E25E15383 /* RoomMembersScreenUITests.swift in Sources */, 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */, 77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */, 05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index 586fba321..bf627bb40 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -34,3 +34,7 @@ "default_session_display_name" = "%@ iOS"; "Notification" = "Notification"; + +// Room Details +"room_details_title" = "Info"; +"room_details_about_section_title" = "About"; diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index 8565ec9f2..3fd686bfe 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -26,6 +26,10 @@ extension ElementL10n { public static let loginTabletDevice = ElementL10n.tr("Untranslated", "login_tablet_device") /// Notification public static let notification = ElementL10n.tr("Untranslated", "Notification") + /// About + public static let roomDetailsAboutSectionTitle = ElementL10n.tr("Untranslated", "room_details_about_section_title") + /// Info + public static let roomDetailsTitle = ElementL10n.tr("Untranslated", "room_details_title") /// Editing public static let roomTimelineEditing = ElementL10n.tr("Untranslated", "room_timeline_editing") /// Failed creating the permalink diff --git a/ElementX/Sources/Other/AvatarSize.swift b/ElementX/Sources/Other/AvatarSize.swift index 3f308c253..27e39ee7a 100644 --- a/ElementX/Sources/Other/AvatarSize.swift +++ b/ElementX/Sources/Other/AvatarSize.swift @@ -45,6 +45,7 @@ enum UserAvatarSizeOnScreen { case timeline case home case settings + case roomDetails var value: CGFloat { switch self { @@ -54,6 +55,8 @@ enum UserAvatarSizeOnScreen { return 32 case .settings: return 60 + case .roomDetails: + return 32 } } } @@ -61,6 +64,7 @@ enum UserAvatarSizeOnScreen { enum RoomAvatarSizeOnScreen { case timeline case home + case details var value: CGFloat { switch self { @@ -68,6 +72,8 @@ enum RoomAvatarSizeOnScreen { return 32 case .home: return 44 + case .details: + return 100 } } } diff --git a/ElementX/Sources/Screens/RoomDetails/RoomDetailsCoordinator.swift b/ElementX/Sources/Screens/RoomDetails/RoomDetailsCoordinator.swift new file mode 100644 index 000000000..e7de6f187 --- /dev/null +++ b/ElementX/Sources/Screens/RoomDetails/RoomDetailsCoordinator.swift @@ -0,0 +1,72 @@ +// +// 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 SwiftUI + +struct RoomDetailsCoordinatorParameters { + let navigationStackCoordinator: NavigationStackCoordinator + let roomProxy: RoomProxyProtocol + let mediaProvider: MediaProviderProtocol +} + +enum RoomDetailsCoordinatorAction { + case cancel +} + +final class RoomDetailsCoordinator: CoordinatorProtocol { + private let parameters: RoomDetailsCoordinatorParameters + private var viewModel: RoomDetailsViewModelProtocol + private var navigationStackCoordinator: NavigationStackCoordinator { parameters.navigationStackCoordinator } + + var callback: ((RoomDetailsCoordinatorAction) -> Void)? + + init(parameters: RoomDetailsCoordinatorParameters) { + self.parameters = parameters + + viewModel = RoomDetailsViewModel(roomProxy: parameters.roomProxy, + mediaProvider: parameters.mediaProvider) + } + + // MARK: - Public + + func start() { + viewModel.callback = { [weak self] action in + guard let self else { return } + MXLog.debug("RoomDetailsViewModel did complete with result: \(action).") + switch action { + case .peopleTapped: + self.showPeople() + case .cancel: + self.callback?(.cancel) + } + } + } + + func toPresentable() -> AnyView { + AnyView(RoomDetailsScreen(context: viewModel.context)) + } + + private func showPeople() { + let params = RoomMembersCoordinatorParameters(roomProxy: parameters.roomProxy, + mediaProvider: parameters.mediaProvider) + let coordinator = RoomMembersCoordinator(parameters: params) + coordinator.callback = { [weak self] _ in + self?.navigationStackCoordinator.pop() + } + + navigationStackCoordinator.push(coordinator) + } +} diff --git a/ElementX/Sources/Screens/RoomDetails/RoomDetailsModels.swift b/ElementX/Sources/Screens/RoomDetails/RoomDetailsModels.swift new file mode 100644 index 000000000..1060f483f --- /dev/null +++ b/ElementX/Sources/Screens/RoomDetails/RoomDetailsModels.swift @@ -0,0 +1,73 @@ +// +// 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 +import UIKit + +// MARK: - Coordinator + +// MARK: View model + +enum RoomDetailsViewModelAction { + case peopleTapped + case cancel +} + +// MARK: View + +struct RoomDetailsViewState: BindableState { + let roomId: String + let isEncrypted: Bool + let isDirect: Bool + var roomTitle = "" + var roomTopic: String? + var roomAvatar: UIImage? + var members: [RoomDetailsMember] + + var isLoadingMembers: Bool { + members.isEmpty + } + + var bindings: RoomDetailsViewStateBindings +} + +struct RoomDetailsViewStateBindings { + /// Information describing the currently displayed alert. + var alertInfo: AlertInfo? +} + +enum RoomDetailsErrorType: Hashable { + /// A specific error message shown in an alert. + case alert(String) +} + +enum RoomDetailsViewAction { + case processTapPeople +} + +struct RoomDetailsMember: Identifiable, Equatable { + let id: String + let name: String? + let avatarUrl: String? + // cached + var avatar: UIImage? + + init(withProxy proxy: RoomMemberProxy) { + id = proxy.userId + name = proxy.displayName + avatarUrl = proxy.avatarUrl + } +} diff --git a/ElementX/Sources/Screens/RoomDetails/RoomDetailsViewModel.swift b/ElementX/Sources/Screens/RoomDetails/RoomDetailsViewModel.swift new file mode 100644 index 000000000..58163e55a --- /dev/null +++ b/ElementX/Sources/Screens/RoomDetails/RoomDetailsViewModel.swift @@ -0,0 +1,75 @@ +// +// 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 SwiftUI + +typealias RoomDetailsViewModelType = StateStoreViewModel + +class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtocol { + // MARK: - Properties + + // MARK: Private + + private let roomProxy: RoomProxyProtocol + private let mediaProvider: MediaProviderProtocol + + // MARK: Public + + var callback: ((RoomDetailsViewModelAction) -> Void)? + + // MARK: - Setup + + init(roomProxy: RoomProxyProtocol, + mediaProvider: MediaProviderProtocol) { + self.roomProxy = roomProxy + self.mediaProvider = mediaProvider + super.init(initialViewState: .init(roomId: roomProxy.id, + isEncrypted: roomProxy.isEncrypted, + isDirect: roomProxy.isDirect, + roomTitle: roomProxy.displayName ?? roomProxy.name ?? "Unknown Room", + roomTopic: roomProxy.topic, + members: [], + bindings: .init())) + + Task { + switch await roomProxy.members() { + case .success(let members): + state.members = members.map { RoomDetailsMember(withProxy: $0) } + case .failure(let error): + MXLog.debug("Failed to retrieve room members: \(error)") + state.bindings.alertInfo = AlertInfo(id: .alert(ElementL10n.unknownError)) + } + } + + if let avatarURL = roomProxy.avatarURL { + Task { + if case let .success(avatar) = await mediaProvider.loadImageFromURLString(avatarURL, + avatarSize: .room(on: .details)) { + state.roomAvatar = avatar + } + } + } + } + + // MARK: - Public + + override func process(viewAction: RoomDetailsViewAction) async { + switch viewAction { + case .processTapPeople: + callback?(.peopleTapped) + } + } +} diff --git a/ElementX/Sources/Screens/RoomDetails/RoomDetailsViewModelProtocol.swift b/ElementX/Sources/Screens/RoomDetails/RoomDetailsViewModelProtocol.swift new file mode 100644 index 000000000..0d4abc7f2 --- /dev/null +++ b/ElementX/Sources/Screens/RoomDetails/RoomDetailsViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// 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 + +@MainActor +protocol RoomDetailsViewModelProtocol { + var callback: ((RoomDetailsViewModelAction) -> Void)? { get set } + var context: RoomDetailsViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/RoomDetails/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetails/View/RoomDetailsScreen.swift new file mode 100644 index 000000000..d655f28b0 --- /dev/null +++ b/ElementX/Sources/Screens/RoomDetails/View/RoomDetailsScreen.swift @@ -0,0 +1,171 @@ +// +// 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 SwiftUI + +struct RoomDetailsScreen: View { + // MARK: Private + + @Environment(\.colorScheme) private var colorScheme + @ScaledMetric private var avatarSize = AvatarSize.room(on: .details).value + @ScaledMetric private var menuIconSize = 30.0 + private let listRowInsets = EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16) + + // MARK: Public + + @ObservedObject var context: RoomDetailsViewModel.Context + + // MARK: Views + + var body: some View { + Form { + headerSection + + if let topic = context.viewState.roomTopic { + topicSection(with: topic) + } + + aboutSection + + if context.viewState.isEncrypted { + securitySection + } + } + .alert(item: $context.alertInfo) { $0.alert } + .navigationTitle(ElementL10n.roomDetailsTitle) + } + + private var headerSection: some View { + VStack(spacing: 16.0) { + roomAvatarImage + Text(context.viewState.roomTitle) + .foregroundColor(.element.primaryContent) + .font(.element.headline) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity, alignment: .center) + .listRowBackground(Color.clear) + .padding(.top, 18) + .padding(.bottom, 42) + } + + private func topicSection(with topic: String) -> some View { + Section(ElementL10n.roomSettingsTopic) { + Text(topic) + .foregroundColor(.element.secondaryContent) + .font(.element.footnote) + } + } + + private var aboutSection: some View { + Section(ElementL10n.roomDetailsAboutSectionTitle) { + Button { + context.send(viewAction: .processTapPeople) + } label: { + HStack { + Image(systemName: "person") + .foregroundColor(.element.systemGray) + .padding(4) + .background(Color.element.systemGray6) + .clipShape(Circle()) + .frame(width: menuIconSize, height: menuIconSize) + Text(ElementL10n.bottomActionPeople) + .foregroundColor(.element.primaryContent) + .font(.body) + Spacer() + + if context.viewState.isLoadingMembers { + ProgressView() + } else { + Text(String(context.viewState.members.count)) + .foregroundColor(.element.secondaryContent) + .font(.element.body) + Image(systemName: "chevron.forward") + .foregroundColor(.element.secondaryContent) + } + } + } + .listRowInsets(listRowInsets) + .foregroundColor(.element.primaryContent) + .accessibilityIdentifier("peopleButton") + .disabled(context.viewState.isLoadingMembers) + } + } + + private var securitySection: some View { + Section(ElementL10n.roomProfileSectionSecurity) { + HStack(alignment: .center) { + Image(systemName: "lock.shield") + .foregroundColor(.element.systemGray) + .padding(4) + .background(Color.element.systemGray6) + .clipShape(Circle()) + .frame(width: menuIconSize, height: menuIconSize) + VStack(alignment: .leading) { + Text(ElementL10n.encryptionEnabled) + .foregroundColor(.element.primaryContent) + .font(.element.body) + Text(ElementL10n.encryptionEnabledTileDescription) + .foregroundColor(.element.secondaryContent) + .font(.element.footnote) + } + Spacer() + Image(systemName: "checkmark") + .foregroundColor(.element.secondaryContent) + } + } + } + + @ViewBuilder private var roomAvatarImage: some View { + if let avatar = context.viewState.roomAvatar { + Image(uiImage: avatar) + .resizable() + .scaledToFill() + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + .accessibilityIdentifier("roomAvatarImage") + } else { + PlaceholderAvatarImage(text: context.viewState.roomTitle, + contentId: context.viewState.roomId) + .clipShape(Circle()) + .frame(width: avatarSize, height: avatarSize) + .accessibilityIdentifier("roomAvatarPlaceholderImage") + } + } +} + +// MARK: - Previews + +struct RoomDetails_Previews: PreviewProvider { + static var previews: some View { + Group { + let members: [RoomMemberProxy] = [ + .mockAlice, + .mockBob, + .mockCharlie + ] + let roomProxy = MockRoomProxy(displayName: "Room A", + topic: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + isDirect: false, + isEncrypted: true, + members: members) + let viewModel = RoomDetailsViewModel(roomProxy: roomProxy, + mediaProvider: MockMediaProvider()) + RoomDetailsScreen(context: viewModel.context) + } + .tint(.element.accent) + } +} diff --git a/ElementX/Sources/Screens/RoomMembers/RoomMembersCoordinator.swift b/ElementX/Sources/Screens/RoomMembers/RoomMembersCoordinator.swift new file mode 100644 index 000000000..e48aca242 --- /dev/null +++ b/ElementX/Sources/Screens/RoomMembers/RoomMembersCoordinator.swift @@ -0,0 +1,55 @@ +// +// 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 SwiftUI + +struct RoomMembersCoordinatorParameters { + let roomProxy: RoomProxyProtocol + let mediaProvider: MediaProviderProtocol +} + +enum RoomMembersCoordinatorAction { + case cancel +} + +final class RoomMembersCoordinator: CoordinatorProtocol { + private let parameters: RoomMembersCoordinatorParameters + private var viewModel: RoomMembersViewModelProtocol + + var callback: ((RoomMembersCoordinatorAction) -> Void)? + + init(parameters: RoomMembersCoordinatorParameters) { + self.parameters = parameters + + viewModel = RoomMembersViewModel(roomProxy: parameters.roomProxy, + mediaProvider: parameters.mediaProvider) + } + + func start() { + viewModel.callback = { [weak self] action in + guard let self else { return } + MXLog.debug("RoomMembersViewModel did complete with result: \(action).") + switch action { + case .cancel: + self.callback?(.cancel) + } + } + } + + func toPresentable() -> AnyView { + AnyView(RoomMembersScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/RoomMembers/RoomMembersModels.swift b/ElementX/Sources/Screens/RoomMembers/RoomMembersModels.swift new file mode 100644 index 000000000..369e20331 --- /dev/null +++ b/ElementX/Sources/Screens/RoomMembers/RoomMembersModels.swift @@ -0,0 +1,50 @@ +// +// 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 RoomMembersViewModelAction { + case cancel +} + +struct RoomMembersViewState: BindableState { + var members: [RoomDetailsMember] + + var bindings: RoomMembersViewStateBindings + + var visibleMembers: [RoomDetailsMember] { + if bindings.searchQuery.isEmpty { + return members + } + + return members.lazy.filter { member in + member.id.localizedCaseInsensitiveContains(bindings.searchQuery) || + member.name?.localizedCaseInsensitiveContains(bindings.searchQuery) ?? false + } + } +} + +struct RoomMembersViewStateBindings { + var searchQuery = "" + + /// Information describing the currently displayed alert. + var alertInfo: AlertInfo? +} + +enum RoomMembersViewAction { + case selectMember(id: String) + case loadMemberData(id: String) +} diff --git a/ElementX/Sources/Screens/RoomMembers/RoomMembersViewModel.swift b/ElementX/Sources/Screens/RoomMembers/RoomMembersViewModel.swift new file mode 100644 index 000000000..b59643762 --- /dev/null +++ b/ElementX/Sources/Screens/RoomMembers/RoomMembersViewModel.swift @@ -0,0 +1,78 @@ +// +// 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 SwiftUI + +typealias RoomMembersViewModelType = StateStoreViewModel + +class RoomMembersViewModel: RoomMembersViewModelType, RoomMembersViewModelProtocol { + private let roomProxy: RoomProxyProtocol + private let mediaProvider: MediaProviderProtocol + + var callback: ((RoomMembersViewModelAction) -> Void)? + + init(roomProxy: RoomProxyProtocol, + mediaProvider: MediaProviderProtocol) { + self.roomProxy = roomProxy + self.mediaProvider = mediaProvider + super.init(initialViewState: .init(members: [], + bindings: .init())) + + Task { + switch await roomProxy.members() { + case .success(let members): + state.members = members.map { RoomDetailsMember(withProxy: $0) } + case .failure(let error): + MXLog.debug("Failed to retrieve room members: \(error)") + state.bindings.alertInfo = AlertInfo(id: .alert(ElementL10n.unknownError)) + } + } + } + + // MARK: - Public + + override func process(viewAction: RoomMembersViewAction) async { + switch viewAction { + case .selectMember(let id): + MXLog.debug("Member selected: \(id)") + case .loadMemberData(let id): + await loadAvatar(forMember: id) + } + } + + private func loadAvatar(forMember memberId: String) async { + guard let member = state.members.first(where: { $0.id == memberId }) else { + return + } + if member.avatar != nil { + // already loaded + return + } + guard let avatarUrl = member.avatarUrl else { + // user has no avatar + return + } + + switch await mediaProvider.loadImageFromURLString(avatarUrl, avatarSize: .user(on: .roomDetails)) { + case .success(let image): + if let index = state.members.firstIndex(where: { $0.id == memberId }) { + state.members[index].avatar = image + } + case .failure(let error): + MXLog.debug("Failed to retrieve room member avatar: \(error)") + } + } +} diff --git a/ElementX/Sources/Screens/RoomMembers/RoomMembersViewModelProtocol.swift b/ElementX/Sources/Screens/RoomMembers/RoomMembersViewModelProtocol.swift new file mode 100644 index 000000000..3743223b7 --- /dev/null +++ b/ElementX/Sources/Screens/RoomMembers/RoomMembersViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// 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 + +@MainActor +protocol RoomMembersViewModelProtocol { + var callback: ((RoomMembersViewModelAction) -> Void)? { get set } + var context: RoomMembersViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/RoomMembers/View/RoomMembersMemberCell.swift b/ElementX/Sources/Screens/RoomMembers/View/RoomMembersMemberCell.swift new file mode 100644 index 000000000..3c9355c3b --- /dev/null +++ b/ElementX/Sources/Screens/RoomMembers/View/RoomMembersMemberCell.swift @@ -0,0 +1,84 @@ +// +// 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 SwiftUI + +struct RoomMembersMemberCell: View { + @ScaledMetric private var avatarSize = AvatarSize.user(on: .roomDetails).value + + let member: RoomDetailsMember + let context: RoomMembersViewModel.Context + + var body: some View { + Button { + context.send(viewAction: .selectMember(id: member.id)) + } label: { + HStack { + if let avatar = member.avatar { + Image(uiImage: avatar) + .resizable() + .scaledToFill() + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + .accessibilityHidden(true) + } else { + PlaceholderAvatarImage(text: member.name ?? "", contentId: member.id) + .clipShape(Circle()) + .frame(width: avatarSize, height: avatarSize) + .accessibilityHidden(true) + } + + Text(member.name ?? "") + .font(.element.callout.bold()) + .foregroundColor(.element.primaryContent) + .lineLimit(1) + + Spacer() + } + .accessibilityElement(children: .combine) + .task { + context.send(viewAction: .loadMemberData(id: member.id)) + } + } + } +} + +struct RoomMembersMemberCell_Previews: PreviewProvider { + static var previews: some View { + body.preferredColorScheme(.light) + .tint(.element.accent) + body.preferredColorScheme(.dark) + .tint(.element.accent) + } + + static var body: some View { + let members: [RoomMemberProxy] = [ + .mockAlice, + .mockBob, + .mockCharlie + ] + let roomProxy = MockRoomProxy(displayName: "Room A", + members: members) + let viewModel = RoomMembersViewModel(roomProxy: roomProxy, + mediaProvider: MockMediaProvider()) + + return VStack { + ForEach(members) { member in + RoomMembersMemberCell(member: .init(withProxy: member), context: viewModel.context) + } + } + } +} diff --git a/ElementX/Sources/Screens/RoomMembers/View/RoomMembersScreen.swift b/ElementX/Sources/Screens/RoomMembers/View/RoomMembersScreen.swift new file mode 100644 index 000000000..4ba42d521 --- /dev/null +++ b/ElementX/Sources/Screens/RoomMembers/View/RoomMembersScreen.swift @@ -0,0 +1,61 @@ +// +// 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 SwiftUI + +struct RoomMembersScreen: View { + @Environment(\.colorScheme) private var colorScheme + + @ObservedObject var context: RoomMembersViewModel.Context + + var body: some View { + Form { + Section { + ForEach(context.viewState.visibleMembers) { member in + RoomMembersMemberCell(member: member, context: context) + .id(member.id) + } + } footer: { + Text(ElementL10n.roomTitleMembers(context.viewState.members.count)) + .foregroundColor(.element.secondaryContent) + .font(.element.footnote) + } + } + .searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always)) + .alert(item: $context.alertInfo) { $0.alert } + .navigationTitle(ElementL10n.listMembers) + } +} + +// MARK: - Previews + +struct RoomMembers_Previews: PreviewProvider { + static var previews: some View { + Group { + let members: [RoomMemberProxy] = [ + .mockAlice, + .mockBob, + .mockCharlie + ] + let roomProxy = MockRoomProxy(displayName: "Room A", + members: members) + let viewModel = RoomMembersViewModel(roomProxy: roomProxy, + mediaProvider: MockMediaProvider()) + RoomMembersScreen(context: viewModel.context) + } + .tint(.element.accent) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 13454dce0..866dd9300 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -18,10 +18,9 @@ import SwiftUI struct RoomScreenCoordinatorParameters { let navigationStackCoordinator: NavigationStackCoordinator + let roomProxy: RoomProxyProtocol let timelineController: RoomTimelineControllerProtocol let mediaProvider: MediaProviderProtocol - let roomName: String? - let roomAvatarUrl: String? let emojiProvider: EmojiProviderProtocol } @@ -43,8 +42,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { viewModel = RoomScreenViewModel(timelineController: parameters.timelineController, timelineViewFactory: RoomTimelineViewFactory(), mediaProvider: parameters.mediaProvider, - roomName: parameters.roomName, - roomAvatarUrl: parameters.roomAvatarUrl) + roomName: parameters.roomProxy.displayName ?? parameters.roomProxy.name, + roomAvatarUrl: parameters.roomProxy.avatarURL) } // MARK: - Public @@ -54,6 +53,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { guard let self else { return } MXLog.debug("RoomScreenViewModel did complete with result: \(result).") switch result { + case .displayRoomDetails: + self.displayRoomDetails() case .displayVideo(let videoURL): self.displayVideo(for: videoURL) case .displayFile(let fileURL, let title): @@ -122,4 +123,21 @@ final class RoomScreenCoordinator: CoordinatorProtocol { navigationStackCoordinator.setSheetCoordinator(coordinator) } + + private func displayRoomDetails() { + guard let roomProxy = parameters?.roomProxy, + let mediaProvider = parameters?.mediaProvider else { + return + } + + let params = RoomDetailsCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator, + roomProxy: roomProxy, + mediaProvider: mediaProvider) + let coordinator = RoomDetailsCoordinator(parameters: params) + coordinator.callback = { [weak self] _ in + self?.navigationStackCoordinator.pop() + } + + navigationStackCoordinator.push(coordinator) + } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index bbd393a1e..01c067a7c 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -18,6 +18,7 @@ import Combine import UIKit enum RoomScreenViewModelAction { + case displayRoomDetails case displayVideo(videoURL: URL) case displayFile(fileURL: URL, title: String?) case displayEmojiPicker(itemId: String) @@ -30,6 +31,7 @@ enum RoomScreenComposerMode: Equatable { } enum RoomScreenViewAction { + case headerTapped case displayEmojiPicker(itemId: String) case emojiTapped(emoji: String, itemId: String) case paginateBackwards diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 3ef61a105..41c1f8b8c 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -98,6 +98,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol // swiftlint:disable:next cyclomatic_complexity override func process(viewAction: RoomScreenViewAction) async { switch viewAction { + case .headerTapped: + callback?(.displayRoomDetails) case .paginateBackwards: await paginateBackwards() case .itemAppeared(let id): diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift index 89061ddff..f1b5da4b7 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomHeaderView.swift @@ -24,12 +24,16 @@ struct RoomHeaderView: View { @ObservedObject var context: RoomScreenViewModel.Context var body: some View { - HStack(spacing: 8) { - roomAvatar - .accessibilityHidden(true) - Text(context.viewState.roomTitle) - .font(.element.headline) - .accessibilityIdentifier("roomNameLabel") + Button { + context.send(viewAction: .headerTapped) + } label: { + HStack(spacing: 8) { + roomAvatar + .accessibilityHidden(true) + Text(context.viewState.roomTitle) + .font(.element.headline) + .accessibilityIdentifier("roomNameLabel") + } } } diff --git a/ElementX/Sources/Services/Room/MockRoomProxy.swift b/ElementX/Sources/Services/Room/MockRoomProxy.swift index 8cf2a94cf..af87cec00 100644 --- a/ElementX/Sources/Services/Room/MockRoomProxy.swift +++ b/ElementX/Sources/Services/Room/MockRoomProxy.swift @@ -22,16 +22,14 @@ struct MockRoomProxy: RoomProxyProtocol { let id = UUID().uuidString let name: String? = nil let displayName: String? - - let topic: String? = nil - - let avatarURL: String? = nil - - let isDirect = Bool.random() - let isSpace = Bool.random() - let isPublic = Bool.random() - let isEncrypted = Bool.random() - let isTombstoned = Bool.random() + var topic: String? + var avatarURL: String? + var isDirect = Bool.random() + var isSpace = Bool.random() + var isPublic = Bool.random() + var isEncrypted = Bool.random() + var isTombstoned = Bool.random() + var members: [RoomMemberProxy]? let timelineProvider: RoomTimelineProviderProtocol = MockRoomTimelineProvider() @@ -76,4 +74,11 @@ struct MockRoomProxy: RoomProxyProtocol { func redact(_ eventID: String) async -> Result { .failure(.failedRedactingEvent) } + + func members() async -> Result<[RoomMemberProxy], RoomProxyError> { + if let members { + return .success(members) + } + return .failure(.failedRetrievingMembers) + } } diff --git a/ElementX/Sources/Services/Room/RoomMemberProxy.swift b/ElementX/Sources/Services/Room/RoomMemberProxy.swift new file mode 100644 index 000000000..6840b9b7b --- /dev/null +++ b/ElementX/Sources/Services/Room/RoomMemberProxy.swift @@ -0,0 +1,93 @@ +// +// 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 +import MatrixRustSDK + +struct RoomMemberProxy { + private let member: RoomMember + + init(with member: RoomMember) { + self.member = member + } + + var userId: String { + member.userId + } + + var displayName: String? { + member.displayName + } + + var avatarUrl: String? { + member.avatarUrl + } + + var membership: MembershipState { + member.membership + } + + var isNameAmbiguous: Bool { + member.isNameAmbiguous + } + + var powerLevel: Int64 { + member.powerLevel + } + + var normalizedPowerLevel: Int64 { + member.normalizedPowerLevel + } +} + +extension RoomMemberProxy: Identifiable { + var id: String { + userId + } +} + +// Mocks +extension RoomMemberProxy { + static var mockAlice: RoomMemberProxy { + RoomMemberProxy(with: .init(userId: "alice@matrix.org", + displayName: "Alice", + avatarUrl: nil, + membership: .join, + isNameAmbiguous: false, + powerLevel: 50, + normalizedPowerLevel: 50)) + } + + static var mockBob: RoomMemberProxy { + RoomMemberProxy(with: .init(userId: "bob@matrix.org", + displayName: "Bob", + avatarUrl: nil, + membership: .join, + isNameAmbiguous: false, + powerLevel: 50, + normalizedPowerLevel: 50)) + } + + static var mockCharlie: RoomMemberProxy { + RoomMemberProxy(with: .init(userId: "charlie@matrix.org", + displayName: "Charlie", + avatarUrl: nil, + membership: .join, + isNameAmbiguous: false, + powerLevel: 50, + normalizedPowerLevel: 50)) + } +} diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 5f323ed15..158880bca 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -224,6 +224,17 @@ class RoomProxy: RoomProxyProtocol { .value } + func members() async -> Result<[RoomMemberProxy], RoomProxyError> { + await Task.dispatch(on: .global()) { + do { + let members = try self.room.members() + return .success(members.map { RoomMemberProxy(with: $0) }) + } catch { + return .failure(.failedRetrievingMembers) + } + } + } + func update(avatarURL: String?, forUserId userId: String) { memberAvatars[userId] = avatarURL } diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index cbaae77c0..b5a842ce3 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -25,6 +25,7 @@ enum RoomProxyError: Error { case failedSendingMessage case failedRedactingEvent case failedAddingTimelineListener + case failedRetrievingMembers } @MainActor @@ -62,6 +63,8 @@ protocol RoomProxyProtocol { func editMessage(_ newMessage: String, originalEventId: String) async -> Result func redact(_ eventID: String) async -> Result + + func members() async -> Result<[RoomMemberProxy], RoomProxyError> } extension RoomProxyProtocol { diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index 3bcac2931..46a8f119c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -23,7 +23,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { private let timelineProvider: RoomTimelineProviderProtocol private let timelineItemFactory: RoomTimelineItemFactoryProtocol private let mediaProvider: MediaProviderProtocol - private let roomProxy: RoomProxyProtocol + let roomProxy: RoomProxyProtocol private var cancellables = Set() private var timelineItemsUpdateTask: Task? { diff --git a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift index ff6ecd3f7..92a3c1466 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift @@ -162,10 +162,9 @@ class UserSessionFlowCoordinator: CoordinatorProtocol { mediaProvider: userSession.mediaProvider) let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: detailNavigationStackCoordinator, + roomProxy: roomProxy, timelineController: timelineController, mediaProvider: userSession.mediaProvider, - roomName: roomProxy.displayName ?? roomProxy.name, - roomAvatarUrl: roomProxy.avatarURL, emojiProvider: emojiProvider) let coordinator = RoomScreenCoordinator(parameters: parameters) diff --git a/ElementX/Sources/UITests/UITestScreenIdentifier.swift b/ElementX/Sources/UITests/UITestScreenIdentifier.swift index 48376f580..46ab01d5a 100644 --- a/ElementX/Sources/UITests/UITestScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestScreenIdentifier.swift @@ -37,6 +37,8 @@ enum UITestScreenIdentifier: String { case roomSmallTimelineLargePagination case sessionVerification case userSessionScreen + case roomDetailsScreen + case roomMembersScreen } extension UITestScreenIdentifier: CustomStringConvertible { diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index c28481bd8..7a1520923 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -146,10 +146,9 @@ class MockScreen: Identifiable { case .roomPlainNoAvatar: let navigationStackCoordinator = NavigationStackCoordinator() let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator, + roomProxy: MockRoomProxy(displayName: "Some room name", avatarURL: nil), timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), - roomName: "Some room name", - roomAvatarUrl: nil, emojiProvider: EmojiProvider()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) @@ -157,10 +156,9 @@ class MockScreen: Identifiable { case .roomEncryptedWithAvatar: let navigationStackCoordinator = NavigationStackCoordinator() let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator, + roomProxy: MockRoomProxy(displayName: "Some room name", avatarURL: "mock_url"), timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), - roomName: "Some room name", - roomAvatarUrl: "mock_url", emojiProvider: EmojiProvider()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) @@ -170,10 +168,9 @@ class MockScreen: Identifiable { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator, + roomProxy: MockRoomProxy(displayName: "New room", avatarURL: "mock_url"), timelineController: timelineController, mediaProvider: MockMediaProvider(), - roomName: "New room", - roomAvatarUrl: "mock_url", emojiProvider: EmojiProvider()) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationStackCoordinator.setRootCoordinator(coordinator) @@ -187,10 +184,9 @@ class MockScreen: Identifiable { timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] timelineController.simulateIncomingItems() let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator, + roomProxy: MockRoomProxy(displayName: "Small timeline", avatarURL: "mock_url"), timelineController: timelineController, mediaProvider: MockMediaProvider(), - roomName: "Small timeline", - roomAvatarUrl: "mock_url", emojiProvider: EmojiProvider()) let coordinator = RoomScreenCoordinator(parameters: parameters) @@ -203,10 +199,9 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator, + roomProxy: MockRoomProxy(displayName: "Small timeline, paginating", avatarURL: "mock_url"), timelineController: timelineController, mediaProvider: MockMediaProvider(), - roomName: "Small timeline, paginating", - roomAvatarUrl: "mock_url", emojiProvider: EmojiProvider()) let coordinator = RoomScreenCoordinator(parameters: parameters) @@ -230,6 +225,22 @@ class MockScreen: Identifiable { retainedState.append(coordinator) return navigationSplitCoordinator + case .roomDetailsScreen: + let navigationStackCoordinator = NavigationStackCoordinator() + let coordinator = RoomDetailsCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, + roomProxy: MockRoomProxy(displayName: "Room", + isEncrypted: true, + members: [.mockAlice, .mockBob, .mockCharlie]), + mediaProvider: MockMediaProvider())) + navigationStackCoordinator.setRootCoordinator(coordinator) + return navigationStackCoordinator + case .roomMembersScreen: + let navigationStackCoordinator = NavigationStackCoordinator() + let coordinator = RoomMembersCoordinator(parameters: .init(roomProxy: MockRoomProxy(displayName: "Room", + members: [.mockAlice, .mockBob, .mockCharlie]), + mediaProvider: MockMediaProvider())) + navigationStackCoordinator.setRootCoordinator(coordinator) + return navigationStackCoordinator } }() } diff --git a/UITests/Sources/RoomDetailsScreenUITests.swift b/UITests/Sources/RoomDetailsScreenUITests.swift new file mode 100644 index 000000000..5da2a2c24 --- /dev/null +++ b/UITests/Sources/RoomDetailsScreenUITests.swift @@ -0,0 +1,27 @@ +// +// 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 ElementX +import XCTest + +class RoomDetailsScreenUITests: XCTestCase { + func testInitialStateComponents() { + let app = Application.launch() + app.goToScreenWithIdentifier(.roomDetailsScreen) + + app.assertScreenshot(.roomDetailsScreen) + } +} diff --git a/UITests/Sources/RoomMembersScreenUITests.swift b/UITests/Sources/RoomMembersScreenUITests.swift new file mode 100644 index 000000000..2ee99866e --- /dev/null +++ b/UITests/Sources/RoomMembersScreenUITests.swift @@ -0,0 +1,27 @@ +// +// 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 ElementX +import XCTest + +class RoomMembersScreenUITests: XCTestCase { + func testInitialStateComponents() { + let app = Application.launch() + app.goToScreenWithIdentifier(.roomMembersScreen) + + app.assertScreenshot(.roomMembersScreen) + } +} diff --git a/UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.roomDetailsScreen.png b/UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.roomDetailsScreen.png new file mode 100644 index 000000000..70fab95e9 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.roomDetailsScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4f9921f23e219abb45b496389854f5b371ee7463ee42866991b44a47b19bb59 +size 94836 diff --git a/UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.roomMembersScreen.png b/UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.roomMembersScreen.png new file mode 100644 index 000000000..39b12223c --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.roomMembersScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69a4679101f5ddd9db52d54767a9956d830d1f393c71d217fda9bb0adffc1a77 +size 77024 diff --git a/UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.roomDetailsScreen.png b/UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.roomDetailsScreen.png new file mode 100644 index 000000000..bf50b841e --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.roomDetailsScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7fd5fcb6ee8cdbbe6fae4c10344f05ff4461b9d15451c85f86338b3c176e626b +size 112092 diff --git a/UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.roomMembersScreen.png b/UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.roomMembersScreen.png new file mode 100644 index 000000000..32e7aab52 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.roomMembersScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2711f6f45346670181f5fec69d2250acfd55a6aa4e559b07b4a27035fec14ad +size 83589 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreen.png new file mode 100644 index 000000000..0dd70f02e --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c75cd037fa00ce8a313d71e5d27a7228ae9534327eb73e6d466edde1eed7cbc0 +size 89990 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMembersScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMembersScreen.png new file mode 100644 index 000000000..3726879e2 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomMembersScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cff44922cc9cd14c04e97b47d08ecec46868320cabc1ee19ccca1be20719a716 +size 75699 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreen.png new file mode 100644 index 000000000..31b24ea4f --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:988526a9b5eba885272dd1a0e881d40909a8fd04fc7edb6049288c6c0df6e8fb +size 109714 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMembersScreen.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMembersScreen.png new file mode 100644 index 000000000..897569b9b --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomMembersScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:baca919ad69856064e7aa57e6d9ec6e64e3bb69f0c7d47f2db292ed470219b30 +size 82252 diff --git a/UITests/Sources/__Snapshots__/Application/fr-FR-iPad-9th-generation.roomDetailsScreen.png b/UITests/Sources/__Snapshots__/Application/fr-FR-iPad-9th-generation.roomDetailsScreen.png new file mode 100644 index 000000000..d39619744 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/fr-FR-iPad-9th-generation.roomDetailsScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:599ffee0897b8eeaeee947b7554387a146fef965d693c8f61fcdbc068bf1ecf7 +size 94040 diff --git a/UITests/Sources/__Snapshots__/Application/fr-FR-iPad-9th-generation.roomMembersScreen.png b/UITests/Sources/__Snapshots__/Application/fr-FR-iPad-9th-generation.roomMembersScreen.png new file mode 100644 index 000000000..f67798b94 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/fr-FR-iPad-9th-generation.roomMembersScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfe9f8ca20ddcf3927585a03cab2eebbc8f055a751e9124641565b80eb813e40 +size 75828 diff --git a/UITests/Sources/__Snapshots__/Application/fr-FR-iPhone-14.roomDetailsScreen.png b/UITests/Sources/__Snapshots__/Application/fr-FR-iPhone-14.roomDetailsScreen.png new file mode 100644 index 000000000..a9f06e176 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/fr-FR-iPhone-14.roomDetailsScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:918a4daf2553227377954c2599f76f14f855f9df3576f8de79165657898e5ac0 +size 111063 diff --git a/UITests/Sources/__Snapshots__/Application/fr-FR-iPhone-14.roomMembersScreen.png b/UITests/Sources/__Snapshots__/Application/fr-FR-iPhone-14.roomMembersScreen.png new file mode 100644 index 000000000..48b9a18b8 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/fr-FR-iPhone-14.roomMembersScreen.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86bbe6582ad1215ccfd841fe04fa5e6ca4527daced1c8c3d2b081ba4cccd9302 +size 82112 diff --git a/UnitTests/Sources/RoomDetailsViewModelTests.swift b/UnitTests/Sources/RoomDetailsViewModelTests.swift new file mode 100644 index 000000000..46e0b7c8a --- /dev/null +++ b/UnitTests/Sources/RoomDetailsViewModelTests.swift @@ -0,0 +1,22 @@ +// +// 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 XCTest + +@testable import ElementX + +@MainActor +class RoomDetailsScreenViewModelTests: XCTestCase { } diff --git a/UnitTests/Sources/RoomMembersViewModelTests.swift b/UnitTests/Sources/RoomMembersViewModelTests.swift new file mode 100644 index 000000000..686e80959 --- /dev/null +++ b/UnitTests/Sources/RoomMembersViewModelTests.swift @@ -0,0 +1,22 @@ +// +// 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 XCTest + +@testable import ElementX + +@MainActor +class RoomMembersScreenViewModelTests: XCTestCase { }