diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 577b35e5fa..cec0557a60 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -269,6 +269,9 @@ 659E5B766F76FDEC1BF393A4 /* RoomDetailsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */; }; 65EDA77363BEDC40CDE43B43 /* InvitesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42ADEA322D2089391E049535 /* InvitesScreen.swift */; }; 663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; }; + 6676B1732A4CCBE900F8F6E7 /* CollapsibleReactionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6676B1722A4CCBE900F8F6E7 /* CollapsibleReactionLayout.swift */; }; + 66E30E002A5D733E00F71B89 /* LayoutMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E30DFE2A5D733E00F71B89 /* LayoutMocks.swift */; }; + 66E30E012A5D733E00F71B89 /* CollapsibleFlowLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E30DFF2A5D733E00F71B89 /* CollapsibleFlowLayoutTests.swift */; }; 6713835120D94BAA8ED7E3E5 /* MessageForwardingScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59846FA04E1DBBFDD8829C2A /* MessageForwardingScreenUITests.swift */; }; 67160204A8D362BB7D4AD259 /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693E16574C6F7F9FA1015A8C /* Search.swift */; }; 6786C4B0936AC84D993B20BF /* NotificationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */; }; @@ -511,7 +514,6 @@ B245583C63F8F90357B87FAE /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = A2AE110B053B55E38F8D10C7 /* KZFileWatchers */; }; B27D3190784F85916DA1C394 /* SessionVerificationScreenStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B1EE0908B2BF9212436AD3E /* SessionVerificationScreenStateMachine.swift */; }; B2F8E01ABA1BA30265B4ECBE /* RoundedCornerShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */; }; - B3490F3DB563A543C73CD663 /* CollapsibleFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD349FC7BB07EFCA64851D2 /* CollapsibleFlowLayout.swift */; }; B3D652AA1654270742072FB3 /* DeveloperOptionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86A6F283BC574FDB96ABBB07 /* DeveloperOptionsScreenViewModel.swift */; }; B3EDDEC1839BB5A3747624BB /* FormButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A1CCDEE545CB6453B084BF /* FormButtonStyles.swift */; }; B402708F8728DD0DB7C324E2 /* StartChatScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */; }; @@ -520,7 +522,6 @@ B46EBC7B96CCB64FF8E110DC /* MigrationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CD0FAE9EA761DA175D31CC7 /* MigrationScreenModels.swift */; }; B4A0C69370E6008A971463E7 /* BugReportScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */; }; B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; }; - B5321A1F5B26A0F3EC54909E /* CollapsibleFlowLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5F5209279A752D98AAC4B2 /* CollapsibleFlowLayoutTests.swift */; }; B5479997ECC516C121E6625E /* LocationMarkerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFECCE59967018204876D0A5 /* LocationMarkerView.swift */; }; B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */; }; B5E455C9689EA600EDB3E9E0 /* NavigationRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */; }; @@ -596,7 +597,6 @@ CBA9EDF305036039166E76FF /* StartChatScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */; }; CBD2ABE4C1A47ECD99E1488E /* NotificationSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421FA93BCC2840E66E4F306F /* NotificationSettingsScreenViewModelProtocol.swift */; }; CC0D088F505F33A20DC5590F /* RoomStateEventStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */; }; - CC961529F9F1854BEC3272C9 /* LayoutMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */; }; CCAA0671B46EAFD0BB528E2C /* apple_emojis_data.json in Resources */ = {isa = PBXBuildFile; fileRef = 8FC26871038FB0E4AAE22605 /* apple_emojis_data.json */; }; CCBEC2100CAF2EEBE9DB4156 /* TemplateScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */; }; CCC3802A3C019A6FFAAA547A /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E65E613F057697A1A0BC03 /* NotificationViewController.swift */; }; @@ -855,7 +855,7 @@ 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = ""; }; 12EDAFB64FA5F6812D54F39A /* MigrationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModel.swift; sourceTree = ""; }; 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.swift; sourceTree = ""; }; - 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; + 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; @@ -995,7 +995,7 @@ 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = ""; }; 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = ""; }; - 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DesignKit; path = DesignKit; sourceTree = SOURCE_ROOT; }; + 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DesignKit; sourceTree = SOURCE_ROOT; }; 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = ""; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = ""; }; @@ -1075,8 +1075,11 @@ 65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryService.swift; sourceTree = ""; }; 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationServiceProxy.swift; sourceTree = ""; }; 6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = ""; }; + 6676B1722A4CCBE900F8F6E7 /* CollapsibleReactionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleReactionLayout.swift; sourceTree = ""; }; 667DD3A9D932D7D9EB380CAA /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sk; path = sk.lproj/Localizable.stringsdict; sourceTree = ""; }; 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadingPreprocessor.swift; sourceTree = ""; }; + 66E30DFE2A5D733E00F71B89 /* LayoutMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = ""; }; + 66E30DFF2A5D733E00F71B89 /* CollapsibleFlowLayoutTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollapsibleFlowLayoutTests.swift; sourceTree = ""; }; 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = ""; }; 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = ""; }; 686BCFA37AC6C67FF973CE67 /* OnboardingBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingBackgroundImage.swift; sourceTree = ""; }; @@ -1171,7 +1174,7 @@ 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; 8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenModels.swift; sourceTree = ""; }; 8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = ""; }; 8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = ""; }; @@ -1252,7 +1255,6 @@ ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelProtocol.swift; sourceTree = ""; }; AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImageCache.swift; sourceTree = ""; }; AC3F82523D6F48B926D6AF68 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; - AC5F5209279A752D98AAC4B2 /* CollapsibleFlowLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleFlowLayoutTests.swift; sourceTree = ""; }; ACCC1874C122E2BBE648B8F5 /* LegalInformationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenUITests.swift; sourceTree = ""; }; AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; AD6B522BD637845AB9570B10 /* RoomNotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxy.swift; sourceTree = ""; }; @@ -1280,7 +1282,7 @@ B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+SwiftUI.swift"; sourceTree = ""; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; - B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = ""; }; B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = ""; }; B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelProtocol.swift; sourceTree = ""; }; B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; @@ -1301,14 +1303,12 @@ BB3073CCD77D906B330BC1D6 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; BB33A751BFDA223BDD106EC0 /* OnboardingModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingModels.swift; sourceTree = ""; }; BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = ""; }; - BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = ""; }; BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationContent.swift; sourceTree = ""; }; BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerTextField.swift; sourceTree = ""; }; BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemReplyDetails.swift; sourceTree = ""; }; BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenCoordinator.swift; sourceTree = ""; }; BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreProtocol.swift; sourceTree = ""; }; BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderScreenCoordinator.swift; sourceTree = ""; }; - BFD349FC7BB07EFCA64851D2 /* CollapsibleFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleFlowLayout.swift; sourceTree = ""; }; BFDCAC6CAAD65A2C24EA9C4B /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = ""; }; BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreen.swift; sourceTree = ""; }; C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXAttributeScope.swift; sourceTree = ""; }; @@ -1362,7 +1362,7 @@ CD6B0C4639E066915B5E6463 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = ""; }; D0140615D2232612C813FD6C /* EncryptedHistoryRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedHistoryRoomTimelineItem.swift; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; @@ -1434,7 +1434,7 @@ ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = ""; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; - ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = ""; }; EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; @@ -2041,6 +2041,7 @@ B04B538A859CD012755DC19C /* NSE */, 9413F680ECDFB2B0DDB0DEF2 /* Packages */, 681566846AF307E9BA4C72C6 /* Products */, + 66E30DFC2A5D6F3A00F71B89 /* Recovered References */, ); sourceTree = ""; }; @@ -2302,6 +2303,14 @@ path = View; sourceTree = ""; }; + 6676B1742A4CCC4400F8F6E7 /* CollapsibleFlowLayout */ = { + isa = PBXGroup; + children = ( + 6676B1722A4CCBE900F8F6E7 /* CollapsibleReactionLayout.swift */, + ); + path = CollapsibleFlowLayout; + sourceTree = ""; + }; 669239C03835CD8B51E0FFDB /* AnalyticsPromptScreen */ = { isa = PBXGroup; children = ( @@ -2314,6 +2323,22 @@ path = AnalyticsPromptScreen; sourceTree = ""; }; + 66E30DFC2A5D6F3A00F71B89 /* Recovered References */ = { + isa = PBXGroup; + children = ( + ); + name = "Recovered References"; + sourceTree = ""; + }; + 66E30DFD2A5D733E00F71B89 /* LayoutTests */ = { + isa = PBXGroup; + children = ( + 66E30DFE2A5D733E00F71B89 /* LayoutMocks.swift */, + 66E30DFF2A5D733E00F71B89 /* CollapsibleFlowLayoutTests.swift */, + ); + path = LayoutTests; + sourceTree = ""; + }; 6765932445C053E15E63C29A /* SupportingFiles */ = { isa = PBXGroup; children = ( @@ -2500,7 +2525,7 @@ 851EF6258DF8B7EF129DC3AC /* WelcomeScreenScreenViewModelTests.swift */, 53280D2292E6C9C7821773FD /* UserSession */, 70C5B842301AC281DF374E41 /* Extensions */, - A6AA0A048CAE428A5CA4CBBB /* LayoutTests */, + 66E30DFD2A5D733E00F71B89 /* LayoutTests */, 7583EAC171059A86B767209F /* MediaProvider */, 7DBC911559934065993A5FF4 /* NotificationManager */, 1C62F5382CC9D9F7DCEC344A /* UserDiscoveryService */, @@ -2918,14 +2943,6 @@ path = View; sourceTree = ""; }; - 9F7C2D63C42828D8931D5286 /* CollapsibleFlowLayout */ = { - isa = PBXGroup; - children = ( - BFD349FC7BB07EFCA64851D2 /* CollapsibleFlowLayout.swift */, - ); - path = CollapsibleFlowLayout; - sourceTree = ""; - }; 9FD8D798D879069243A7E7F7 /* View */ = { isa = PBXGroup; children = ( @@ -2998,15 +3015,6 @@ path = WaitlistScreen; sourceTree = ""; }; - A6AA0A048CAE428A5CA4CBBB /* LayoutTests */ = { - isa = PBXGroup; - children = ( - AC5F5209279A752D98AAC4B2 /* CollapsibleFlowLayoutTests.swift */, - BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */, - ); - path = LayoutTests; - sourceTree = ""; - }; A78C2592419CA4C76FBA8FD2 /* Application */ = { isa = PBXGroup; children = ( @@ -3233,8 +3241,8 @@ 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */, 35FA991289149D31F4286747 /* UserPreference.swift */, 7431C962E314ADAE38B6D708 /* Analytics */, - 9F7C2D63C42828D8931D5286 /* CollapsibleFlowLayout */, 349FE0C25B41C7AC9B7C623F /* EffectsScene */, + 6676B1742A4CCC4400F8F6E7 /* CollapsibleFlowLayout */, 44BBB96FAA2F0D53C507396B /* Extensions */, 8F9A844EB44B6AD7CA18FD96 /* HTMLParsing */, 06501F0E978B2D5C92771DC7 /* Logging */, @@ -4087,7 +4095,6 @@ 0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */, 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */, C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */, - B5321A1F5B26A0F3EC54909E /* CollapsibleFlowLayoutTests.swift in Sources */, D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */, CD0088B763CD970CF1CBF8CB /* DateTests.swift in Sources */, 864C69CF951BF36D25BE0C03 /* DeveloperOptionsScreenViewModelTests.swift in Sources */, @@ -4102,7 +4109,6 @@ A216C83ADCF32BA5EF8A6FBC /* InviteUsersViewModelTests.swift in Sources */, 266C4DF893F2947DCCEF327B /* InvitesScreenViewModelTests.swift in Sources */, EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */, - CC961529F9F1854BEC3272C9 /* LayoutMocks.swift in Sources */, 8AC256AF0EC54658321C9241 /* LegalInformationScreenViewModelTests.swift in Sources */, 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */, 149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */, @@ -4131,6 +4137,7 @@ EA974337FA7D040E7C74FE6E /* RoomDetailsViewModelTests.swift in Sources */, 095D3906CF2F940C2D2D17CC /* RoomFlowCoordinatorTests.swift in Sources */, 6B31508C6334C617360C2EAB /* RoomMemberDetailsViewModelTests.swift in Sources */, + 66E30E012A5D733E00F71B89 /* CollapsibleFlowLayoutTests.swift in Sources */, CAF8755E152204F55F8D6B5B /* RoomMembersListViewModelTests.swift in Sources */, 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, CC0D088F505F33A20DC5590F /* RoomStateEventStringBuilderTests.swift in Sources */, @@ -4151,6 +4158,7 @@ AF33B9044498211C3D82F1E1 /* UNTextInputNotificationResponse+Creator.swift in Sources */, 8D3E1FADD78E72504DE0E402 /* UserAgentBuilderTests.swift in Sources */, E313BDD2B8813144139B2E00 /* UserDiscoveryServiceTest.swift in Sources */, + 66E30E002A5D733E00F71B89 /* LayoutMocks.swift in Sources */, A1DF0E1E526A981ED6D5DF44 /* UserIndicatorControllerTests.swift in Sources */, 04F17DE71A50206336749BAC /* UserPreferenceTests.swift in Sources */, 81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */, @@ -4239,7 +4247,6 @@ 520EEDAFBC778AB0B41F2F53 /* ClientMock.swift in Sources */, 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */, 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */, - B3490F3DB563A543C73CD663 /* CollapsibleFlowLayout.swift in Sources */, 9FAF6DA7E8E85C9699757764 /* CollapsibleRoomTimelineView.swift in Sources */, 0DC815CA24E1BD7F408F37D3 /* CollapsibleTimelineItem.swift in Sources */, 663E198678778F7426A9B27D /* Collection.swift in Sources */, @@ -4468,6 +4475,7 @@ 8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */, A494741843F087881299ACF0 /* RestorationToken.swift in Sources */, 755EE5B0998C6A4D764D86E5 /* RoomAttachmentPicker.swift in Sources */, + 6676B1732A4CCBE900F8F6E7 /* CollapsibleReactionLayout.swift in Sources */, 0BDA19079FD6E17C5AC62E22 /* RoomDetailsEditScreen.swift in Sources */, E78D429F18071545BF661A52 /* RoomDetailsEditScreenCoordinator.swift in Sources */, A6D4C5EEA85A6A0ABA1559D6 /* RoomDetailsEditScreenModels.swift in Sources */, diff --git a/ElementX/Resources/Assets.xcassets/images/timeline/Contents.json b/ElementX/Resources/Assets.xcassets/images/timeline/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/timeline/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ElementX/Resources/Assets.xcassets/images/timeline-composer-send-message.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/images/timeline/timeline-composer-send-message.imageset/Contents.json similarity index 100% rename from ElementX/Resources/Assets.xcassets/images/timeline-composer-send-message.imageset/Contents.json rename to ElementX/Resources/Assets.xcassets/images/timeline/timeline-composer-send-message.imageset/Contents.json diff --git a/ElementX/Resources/Assets.xcassets/images/timeline-composer-send-message.imageset/send.svg b/ElementX/Resources/Assets.xcassets/images/timeline/timeline-composer-send-message.imageset/send.svg similarity index 100% rename from ElementX/Resources/Assets.xcassets/images/timeline-composer-send-message.imageset/send.svg rename to ElementX/Resources/Assets.xcassets/images/timeline/timeline-composer-send-message.imageset/send.svg diff --git a/ElementX/Resources/Assets.xcassets/images/timeline/timeline-reaction-add-more.imageset/Contents.json b/ElementX/Resources/Assets.xcassets/images/timeline/timeline-reaction-add-more.imageset/Contents.json new file mode 100644 index 0000000000..beeaf3fb99 --- /dev/null +++ b/ElementX/Resources/Assets.xcassets/images/timeline/timeline-reaction-add-more.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "timeline-reaction-add-more.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/ElementX/Resources/Assets.xcassets/images/timeline/timeline-reaction-add-more.imageset/timeline-reaction-add-more.pdf b/ElementX/Resources/Assets.xcassets/images/timeline/timeline-reaction-add-more.imageset/timeline-reaction-add-more.pdf new file mode 100644 index 0000000000..69eacbc62e Binary files /dev/null and b/ElementX/Resources/Assets.xcassets/images/timeline/timeline-reaction-add-more.imageset/timeline-reaction-add-more.pdf differ diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index fd93eac79e..09ae2e1422 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -449,6 +449,8 @@ "screen_room_details_topic_title" = "Topic"; "screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again."; "screen_room_retry_send_menu_remove_action" = "Remove"; +"screen_room_reactions_show_more" = "Show more"; +"screen_room_reactions_show_less" = "Show less"; "screen_session_verification_cancelled_title" = "Verification cancelled"; "screen_session_verification_positive_button_ready" = "Start"; "screen_signout_confirmation_dialog_submit" = "Sign out"; diff --git a/ElementX/Sources/Generated/Assets.swift b/ElementX/Sources/Generated/Assets.swift index 6dfa4250e5..7e0c88a246 100644 --- a/ElementX/Sources/Generated/Assets.swift +++ b/ElementX/Sources/Generated/Assets.swift @@ -42,6 +42,7 @@ internal enum Asset { internal static let locationPointerFull = ImageAsset(name: "images/location-pointer-full") internal static let locationPointer = ImageAsset(name: "images/location-pointer") internal static let timelineComposerSendMessage = ImageAsset(name: "images/timeline-composer-send-message") + internal static let timelineReactionAddMore = ImageAsset(name: "images/timeline-reaction-add-more") internal static let waitingGradient = ImageAsset(name: "images/waiting-gradient") } } diff --git a/ElementX/Sources/Other/CollapsibleFlowLayout/CollapsibleFlowLayout.swift b/ElementX/Sources/Other/CollapsibleFlowLayout/CollapsibleReactionLayout.swift similarity index 50% rename from ElementX/Sources/Other/CollapsibleFlowLayout/CollapsibleFlowLayout.swift rename to ElementX/Sources/Other/CollapsibleFlowLayout/CollapsibleReactionLayout.swift index c09bee33e2..7234efbad3 100644 --- a/ElementX/Sources/Other/CollapsibleFlowLayout/CollapsibleFlowLayout.swift +++ b/ElementX/Sources/Other/CollapsibleFlowLayout/CollapsibleReactionLayout.swift @@ -15,11 +15,12 @@ // import SwiftUI -/// A flow layout that will show a collapse/expand button when the layout wraps over a defined number of rows. -/// With n subviews passed to the layout, n-1 first views represent the main views to be laid out. -/// The nth subview is the collapse/expand button which is only shown when the layout overflows `rowsBeforeCollapsible` number of rows. -/// When the button is shown it is tagged on the end of the collapsed or expanded layout. -struct CollapsibleFlowLayout: Layout { +/// A flow layout for reactions that will show a collapse/expand button when the layout wraps over a defined number of rows. +/// It displays an add more button when there are greater than 0 reactions and always displays the reaction and add more button +/// on the same row (moving them both to a new row if necessary). +/// Each subview should be marked with the appropriate `ReactionLayoutItem` using the `reactionLayoutItem` modified +/// so the layout can appropriately treat each type of item. +struct CollapsibleReactionLayout: Layout { static let pointOffscreen = CGPoint(x: -10000, y: -10000) /// The horizontal spacing between items var itemSpacing: CGFloat = 0 @@ -31,61 +32,76 @@ struct CollapsibleFlowLayout: Layout { var rowsBeforeCollapsible: Int? func sizeThatFits(proposal: ProposedViewSize, subviews: some FlowLayoutSubviews, cache: inout ()) -> CGSize { - let collapseButton = subviews[subviews.count - 1] - var subviewsWithoutCollapseButton = subviews - subviewsWithoutCollapseButton.removeLast() - // Calculate the layout of the rows without the button - let rowsNoButton = calculateRows(proposal: proposal, subviews: Array(subviewsWithoutCollapseButton)) + guard let subviewsByType = getSubviewsByItemType(subviews: Array(subviews)), subviewsByType.reactions.count > 0 else { + return .zero + } + // Calculate the layout of the rows with the reactions button and add more button + let reactionsAndAddMore = calculateRows(proposal: proposal, subviews: Array(subviewsByType.reactions + [subviewsByType.addMoreButton])) // If we have extended beyond the defined number of rows we are showing the expand/collapse ui - if let rowsBeforeCollapsible, rowsNoButton.count > rowsBeforeCollapsible { + if let rowsBeforeCollapsible, reactionsAndAddMore.count > rowsBeforeCollapsible { if collapsed { // Truncate to `rowsBeforeCollapsible` number of rows and replace the item at the end of the last row with the button - let collapsedRows = Array(rowsNoButton.prefix(rowsBeforeCollapsible)) - let (collapsedRowsWithButton, _) = replaceTrailingItemsWithButton(rowWidth: proposal.width ?? 0, rows: collapsedRows, button: collapseButton) - let size = sizeThatFits(proposal: proposal, rows: collapsedRowsWithButton) + let collapsedRows = Array(reactionsAndAddMore.prefix(rowsBeforeCollapsible)) + let (collapsedRowsWithButtons, _) = replaceTrailingItemsWithButtons(rowWidth: proposal.width ?? 0, + rows: collapsedRows, + collapseButton: subviewsByType.collapseButton, + addMoreButton: subviewsByType.addMoreButton) + let size = sizeThatFits(proposal: proposal, rows: collapsedRowsWithButtons) return size } else { // Show all subviews with the button at the end - let rowsWithButton = calculateRows(proposal: proposal, subviews: Array(subviews)) - let size = sizeThatFits(proposal: proposal, rows: rowsWithButton) + var rowsWithButtons = calculateRows(proposal: proposal, subviews: Array(subviews)) + ensureCollapseAndAddMoreButtonsAreOnTheSameRow(&rowsWithButtons) + let size = sizeThatFits(proposal: proposal, rows: rowsWithButtons) return size } } else { // Otherwise we are just calculating the size of all items without the button - return sizeThatFits(proposal: proposal, rows: rowsNoButton) + return sizeThatFits(proposal: proposal, rows: reactionsAndAddMore) } } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: some FlowLayoutSubviews, cache: inout ()) { - let collapseButton = subviews[subviews.count - 1] - var subviewsWithoutCollapseButton = subviews - subviewsWithoutCollapseButton.removeLast() - // Calculate the layout of the rows without the button - let rowsNoButton = calculateRows(proposal: ProposedViewSize(bounds.size), subviews: Array(subviewsWithoutCollapseButton)) + guard let subviewsByType = getSubviewsByItemType(subviews: Array(subviews)), subviewsByType.reactions.count > 0 else { + subviews.forEach { subview in + subview.place(at: Self.pointOffscreen, anchor: .leading, proposal: .zero) + } + return + } + + // Calculate the layout of the rows with the reactions button and add more button + let reactionsAndAddMore = calculateRows(proposal: ProposedViewSize(bounds.size), subviews: Array(subviewsByType.reactions + [subviewsByType.addMoreButton])) // If we have extended beyond the defined number of rows we are showing the expand/collapse ui - if let rowsBeforeCollapsible, rowsNoButton.count > rowsBeforeCollapsible { + if let rowsBeforeCollapsible, reactionsAndAddMore.count > rowsBeforeCollapsible { if collapsed { // Truncate to `rowsBeforeCollapsible` number of rows and replace the item at the end of the last row with the button - let collapsedRows = Array(rowsNoButton.prefix(rowsBeforeCollapsible)) - let (collapsedRowsWithButton, subviewsToHide) = replaceTrailingItemsWithButton(rowWidth: bounds.width, rows: collapsedRows, button: collapseButton) - let remainingSubviews = subviewsToHide + Array(rowsNoButton.suffix(rowsNoButton.count - rowsBeforeCollapsible)).joined() - placeSubviews(in: bounds, rows: collapsedRowsWithButton) + let collapsedRows = Array(reactionsAndAddMore.prefix(rowsBeforeCollapsible)) + let (collapsedRowsWithButtons, subviewsToHide) = replaceTrailingItemsWithButtons(rowWidth: bounds.width, + rows: collapsedRows, + collapseButton: subviewsByType.collapseButton, + addMoreButton: subviewsByType.addMoreButton) + + var remainingSubviews = subviewsToHide + Array(reactionsAndAddMore.suffix(reactionsAndAddMore.count - rowsBeforeCollapsible)).joined() + // remove the add button which was in initial rows calculation + remainingSubviews.removeLast() + placeSubviews(in: bounds, rows: collapsedRowsWithButtons) // "Remove" (place with a proposed zero frame) any additional subviews remainingSubviews.forEach { subview in subview.place(at: Self.pointOffscreen, anchor: .leading, proposal: .zero) } } else { - // Show all subviews with the button at the end - let rowsWithButton = calculateRows(proposal: ProposedViewSize(bounds.size), subviews: Array(subviews)) - placeSubviews(in: bounds, rows: rowsWithButton) + // Show all subviews with the buttons at the end + var rowsWithButtons = calculateRows(proposal: ProposedViewSize(bounds.size), subviews: Array(subviews)) + ensureCollapseAndAddMoreButtonsAreOnTheSameRow(&rowsWithButtons) + placeSubviews(in: bounds, rows: rowsWithButtons) } } else { - // Otherwise we are just calculating the size of all items without the button - placeSubviews(in: bounds, rows: rowsNoButton) - // "Remove"(place with a proposed zero frame) the button - collapseButton.place(at: Self.pointOffscreen, anchor: .leading, proposal: .zero) + // Otherwise we are just placing the reactions and add button + placeSubviews(in: bounds, rows: reactionsAndAddMore) + // "Remove"(place with a proposed zero frame) the collapse button + subviewsByType.collapseButton.place(at: Self.pointOffscreen, anchor: .leading, proposal: .zero) } } @@ -145,27 +161,40 @@ struct CollapsibleFlowLayout: Layout { /// - rows: The input list of rows /// - button: The button to replace the trailing items /// - Returns: The new rows structure with button replaced and the subviews remove from the input to make space for the button - private func replaceTrailingItemsWithButton(rowWidth: CGFloat, rows: [[FlowLayoutSubview]], button: FlowLayoutSubview) -> ([[FlowLayoutSubview]], [FlowLayoutSubview]) { + private func replaceTrailingItemsWithButtons(rowWidth: CGFloat, rows: [[FlowLayoutSubview]], collapseButton: FlowLayoutSubview, addMoreButton: FlowLayoutSubview) -> ([[FlowLayoutSubview]], [FlowLayoutSubview]) { var rows = rows let lastRow = rows[rows.count - 1] - let buttonSize = button.sizeThatFits(.unspecified) + let collapseButtonSize = collapseButton.sizeThatFits(.unspecified) + let addMoreButtonSize = addMoreButton.sizeThatFits(.unspecified) + let buttonsWidth = collapseButtonSize.width + itemSpacing + addMoreButtonSize.width var rowX: CGFloat = 0 for (i, subview) in lastRow.enumerated() { let size = subview.sizeThatFits(.unspecified) let horizontalSpacing = i == 0 ? 0 : itemSpacing rowX += size.width + horizontalSpacing - if rowX > (rowWidth - (buttonSize.width + horizontalSpacing)) { - let lastRowWithButton = Array(lastRow.prefix(i)) + [button] + if rowX > (rowWidth - (buttonsWidth + horizontalSpacing)) { + let lastRowWithButton = Array(lastRow.prefix(i)) + [collapseButton, addMoreButton] let subviewsToHide = Array(lastRow.suffix(lastRow.count - i)) rows[rows.count - 1] = lastRowWithButton return (rows, subviewsToHide) } } - let lastRowWithButton = Array(lastRow) + [button] + let lastRowWithButton = Array(lastRow) + [collapseButton, addMoreButton] rows[rows.count - 1] = lastRowWithButton return (rows, []) } + private func ensureCollapseAndAddMoreButtonsAreOnTheSameRow(_ rows: inout [[FlowLayoutSubview]]) { + guard var lastRow = rows.last, lastRow.count == 1 else { + return + } + var secondLastRow = rows[rows.count - 2] + let collapseButton = secondLastRow.removeLast() + lastRow.prepend(collapseButton) + rows[rows.count - 2] = secondLastRow + rows[rows.count - 1] = lastRow + } + /// Given a list of rows place them in the layout. /// - Parameters: /// - bounds: The bounds of the parent @@ -173,20 +202,60 @@ struct CollapsibleFlowLayout: Layout { private func placeSubviews(in bounds: CGRect, rows: [[FlowLayoutSubview]]) { var rowY: CGFloat = bounds.minY var rowHeight: CGFloat = 0 - for (i, row) in rows.enumerated() { + + let sizes = rows.map { row in + row.map { subview in + subview.sizeThatFits(.unspecified) + } + } + + let maxHeight = sizes.joined().reduce(0) { partialResult, size in + max(partialResult, size.height) + } + + for (i, row) in sizes.enumerated() { var rowX: CGFloat = bounds.minX let verticalSpacing = i == 0 ? 0 : rowSpacing - for (j, subview) in row.enumerated() { - let size = subview.sizeThatFits(.unspecified) + for (j, size) in row.enumerated() { + let subview = rows[i][j] let horizontalSpacing = j == 0 ? 0 : itemSpacing - let point = CGPoint(x: rowX + horizontalSpacing, y: rowY + verticalSpacing + (size.height / 2)) - subview.place(at: point, anchor: .leading, proposal: ProposedViewSize(size)) - rowHeight = max(rowHeight, size.height) + let point = CGPoint(x: rowX + horizontalSpacing, y: rowY + verticalSpacing + (maxHeight / 2)) + subview.place(at: point, anchor: .leading, proposal: ProposedViewSize(CGSize(width: size.width, height: maxHeight))) + rowHeight = max(rowHeight, maxHeight) rowX += size.width + horizontalSpacing } rowY += rowHeight + verticalSpacing } } + + /// Group the subviews by type using `ReactionLayoutItemType` + /// - Parameter subviews: A flat list of all the subviews + /// - Returns: The subviews organised by type + private func getSubviewsByItemType(subviews: [FlowLayoutSubview]) -> ReactionSubviews? { + var collapseButton: FlowLayoutSubview? + var addMoreButton: FlowLayoutSubview? + var reactions: [FlowLayoutSubview] = [] + for subview in subviews { + switch subview[ReactionLayoutItemType.self] { + case .reaction: + reactions.append(subview) + case .expandCollapse: + collapseButton = subview + case .addMore: + addMoreButton = subview + } + } + guard let collapseButton, let addMoreButton, reactions.count > 0 else { + return nil + } + return ReactionSubviews(reactions: reactions, collapseButton: collapseButton, addMoreButton: addMoreButton) + } +} + +struct ReactionSubviews { + var reactions: [FlowLayoutSubview] + var collapseButton: FlowLayoutSubview + var addMoreButton: FlowLayoutSubview } /// A protocol representing subviews so that we can inject mocks in unit tests. @@ -198,6 +267,23 @@ extension LayoutSubviews: FlowLayoutSubviews { } protocol FlowLayoutSubview { func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize func place(at position: CGPoint, anchor: UnitPoint, proposal: ProposedViewSize) + subscript(key: K.Type) -> K.Value where K: LayoutValueKey { get } } extension LayoutSubview: FlowLayoutSubview { } + +enum ReactionLayoutItem { + case reaction + case expandCollapse + case addMore +} + +struct ReactionLayoutItemType: LayoutValueKey { + static let defaultValue: ReactionLayoutItem = .reaction +} + +extension View { + func reactionLayoutItem(_ value: ReactionLayoutItem) -> some View { + layoutValue(key: ReactionLayoutItemType.self, value: value) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift index dd73274a93..65ff1137e8 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift @@ -24,25 +24,36 @@ struct TimelineReactionsView: View { private static let verticalSpacing: CGFloat = 4 @EnvironmentObject private var context: RoomScreenViewModel.Context @Environment(\.layoutDirection) private var layoutDirection: LayoutDirection + @Namespace private var animation let itemID: TimelineItemIdentifier let reactions: [AggregatedReaction] @Binding var collapsed: Bool var body: some View { - CollapsibleFlowLayout(itemSpacing: 4, rowSpacing: 4, collapsed: collapsed, rowsBeforeCollapsible: 2) { + CollapsibleReactionLayout(itemSpacing: 4, rowSpacing: 4, collapsed: collapsed, rowsBeforeCollapsible: 2) { ForEach(reactions, id: \.self) { reaction in - TimelineReactionButton(itemID: itemID, reaction: reaction) { key in + TimelineReactionButton(reaction: reaction) { key in context.send(viewAction: .toggleReaction(key: key, itemID: itemID)) } showReactionSummary: { key in context.send(viewAction: .reactionSummary(itemID: itemID, key: key)) } + .reactionLayoutItem(.reaction) } Button { collapsed.toggle() } label: { TimelineCollapseButtonLabel(collapsed: collapsed) } + .reactionLayoutItem(.expandCollapse) + .animation(.easeOut, value: collapsed) + Button { + context.send(viewAction: .displayEmojiPicker(itemID: itemID)) + } label: { + TimelineReactionAddMoreButtonLabel() + } + .animation(.easeOut, value: collapsed) + .reactionLayoutItem(.addMore) } .coordinateSpace(name: Self.flowCoordinateSpace) } @@ -54,15 +65,11 @@ struct TimelineReactionButtonLabel: View { @ViewBuilder var content: () -> Content var body: some View { - HStack(spacing: 4) { - content() - } - .padding(.vertical, 6) - .padding(.horizontal, 8) - .background(backgroundShape.inset(by: 1).fill(overlayBackgroundColor)) - .overlay(backgroundShape.inset(by: 2.0).strokeBorder(overlayBorderColor)) - .overlay(backgroundShape.strokeBorder(Color.compound.bgCanvasDefault, lineWidth: 2)) - .accessibilityElement(children: .combine) + content() + .background(backgroundShape.inset(by: 1).fill(overlayBackgroundColor)) + .overlay(backgroundShape.inset(by: 2.0).strokeBorder(overlayBorderColor)) + .overlay(backgroundShape.strokeBorder(Color.compound.bgCanvasDefault, lineWidth: 2)) + .accessibilityElement(children: .combine) } var backgroundShape: some InsettableShape { @@ -84,6 +91,8 @@ struct TimelineCollapseButtonLabel: View { var body: some View { TimelineReactionButtonLabel { Text(collapsed ? L10n.screenRoomReactionsShowMore : L10n.screenRoomReactionsShowLess) + .padding(.vertical, 6) + .padding(.horizontal, 8) .layoutPriority(1) .drawingGroup() .font(.compound.bodyMD) @@ -93,7 +102,6 @@ struct TimelineCollapseButtonLabel: View { } struct TimelineReactionButton: View { - let itemID: TimelineItemIdentifier let reaction: AggregatedReaction let toggleReaction: (String) -> Void let showReactionSummary: (String) -> Void @@ -110,13 +118,17 @@ struct TimelineReactionButton: View { var label: some View { TimelineReactionButtonLabel(isHighlighted: reaction.isHighlighted) { - Text(reaction.key) - .font(.compound.bodyMD) - if reaction.count > 1 { - Text(String(reaction.count)) + HStack(spacing: 4) { + Text(reaction.key) .font(.compound.bodyMD) - .foregroundColor(textColor) + if reaction.count > 1 { + Text(String(reaction.count)) + .font(.compound.bodyMD) + .foregroundColor(textColor) + } } + .padding(.vertical, 6) + .padding(.horizontal, 8) } } @@ -125,6 +137,22 @@ struct TimelineReactionButton: View { } } +struct TimelineReactionAddMoreButtonLabel: View { + @ScaledMetric private var addMoreButtonIconSize = 16 + + var body: some View { + TimelineReactionButtonLabel { + Image(asset: Asset.Images.timelineReactionAddMore) + .resizable() + .frame(width: addMoreButtonIconSize, height: addMoreButtonIconSize) + // Vertical sizing is done by the layout so that the add more button + // matches the height of the text based buttons. + .padding(.horizontal, 10) + .frame(maxHeight: .infinity, alignment: .center) + } + } +} + struct TimelineReactionViewPreviewsContainer: View { @State private var collapseState1 = false @State private var collapseState2 = true diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomEncryptedWithAvatar.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomEncryptedWithAvatar.png index a65290f73b..edcf0dd983 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomEncryptedWithAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomEncryptedWithAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2536457833f1e02d6526f3cce18bfd89629f715cb28266bbc4c2c4117ea65caa -size 292394 +oid sha256:1de36174221c11e943109f468dca4ee795cb8ca90313d7ed9c01814a736f32c1 +size 296863 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPlainNoAvatar.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPlainNoAvatar.png index 4cfa03141e..eae2e3bb52 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPlainNoAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomPlainNoAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f8ff0d944bce8228a6c656d0a4e694be4262237c956545eb02bf4ccefd99524 -size 292274 +oid sha256:28731b6aa52a65348cdb8f985e9458b8df8a276b467e88fc81be06019072909f +size 296744 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomEncryptedWithAvatar.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomEncryptedWithAvatar.png index c5263fd73e..2dc9f3c903 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomEncryptedWithAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomEncryptedWithAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:53bb76fe68c4191683679d2640447f1197ca332c9f6d54baaece8892da4d6e65 -size 374657 +oid sha256:4d8d79e503071161a1c4f93ddb5eceaa8093a5f03190f67674996a968403b9ac +size 373443 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPlainNoAvatar.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPlainNoAvatar.png index 6a70bf6959..f14f773930 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPlainNoAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomPlainNoAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:295992c13e2831693a810b7034c1ab41cb81f969de802a238e691599f4718950 -size 374348 +oid sha256:a346d75dabdd2aa5bfffa9864f8f6ea03df2706aaf41b89cc8bbfaef813911c0 +size 373150 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomEncryptedWithAvatar.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomEncryptedWithAvatar.png index 068c5a1d60..aee68cc4e4 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomEncryptedWithAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomEncryptedWithAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c15292dae68fb8d0680fe6dcffba981801016df4e5601a9cb0bcca5748bc7fd9 -size 293483 +oid sha256:bed1e8ca4a425d95b51012d1f1071a4cae29e52d86b977fc2b4939907a10ed52 +size 297947 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPlainNoAvatar.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPlainNoAvatar.png index 4ccc9bbe05..a49f89f0b5 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPlainNoAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomPlainNoAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40cf545f40203e7e77231aa44cf43756f00f9a0860c1260b7db13d4a69b6399b -size 293363 +oid sha256:1850339aab3fe5dcea8a3208bd2ff6b10bbee1af9f33f79c8346467365d47c1a +size 297828 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomEncryptedWithAvatar.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomEncryptedWithAvatar.png index bcdf475b2d..b1db42b186 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomEncryptedWithAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomEncryptedWithAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd9b2eca15f4756abb3865f7488de286ce8e210e61c0e15ac2aa3f1899246964 -size 367914 +oid sha256:6ed2478dfe1709d4bf89c14e4ef260bfbbb842c03d0cea6d582e2f83d4138aa4 +size 365987 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPlainNoAvatar.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPlainNoAvatar.png index 25dc04ce28..800cfec4f6 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPlainNoAvatar.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomPlainNoAvatar.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:277ffaa4c038e5e41e0092190c045ce886a757ff6fc478feec7c0ee2c8c5282b -size 367605 +oid sha256:4f1675bb9365c8c1f7e13ce74ea8b6c63cafb300ecfc61d73ed6d47358a8a877 +size 365694 diff --git a/UnitTests/Sources/LayoutTests/CollapsibleFlowLayoutTests.swift b/UnitTests/Sources/LayoutTests/CollapsibleFlowLayoutTests.swift index 421c5e7168..d914b69b9e 100644 --- a/UnitTests/Sources/LayoutTests/CollapsibleFlowLayoutTests.swift +++ b/UnitTests/Sources/LayoutTests/CollapsibleFlowLayoutTests.swift @@ -21,22 +21,13 @@ import XCTest final class CollapsibleFlowLayoutTests: XCTestCase { func testFlowLayoutWithExpandAndCollapse() { let containerSize = CGSize(width: 250, height: 400) - var flowLayout = CollapsibleFlowLayout(itemSpacing: 5, rowSpacing: 5, rowsBeforeCollapsible: 2) + var flowLayout = CollapsibleReactionLayout(itemSpacing: 5, rowSpacing: 5, rowsBeforeCollapsible: 2) var placedViews: [CGRect] = [] let placedViewsCallback = { rect in placedViews.append(rect) } - let subviews: [LayoutSubviewMock] = [ - LayoutSubviewMock(size: CGSize(width: 100, height: 50), placedPositionCallback: placedViewsCallback), - LayoutSubviewMock(size: CGSize(width: 100, height: 50), placedPositionCallback: placedViewsCallback), - LayoutSubviewMock(size: CGSize(width: 100, height: 50), placedPositionCallback: placedViewsCallback), - LayoutSubviewMock(size: CGSize(width: 100, height: 50), placedPositionCallback: placedViewsCallback), - LayoutSubviewMock(size: CGSize(width: 100, height: 50), placedPositionCallback: placedViewsCallback), - LayoutSubviewMock(size: CGSize(width: 100, height: 50), placedPositionCallback: placedViewsCallback), - // The expand/collapse button - LayoutSubviewMock(size: CGSize(width: 100, height: 50), placedPositionCallback: placedViewsCallback) - ] + let subviews = createReactionLayoutSubviews(with: Array(repeating: CGSize(width: 100, height: 50), count: 6), placedPositionCallback: placedViewsCallback) let subviewsMock = LayoutSubviewsMock(subviews: subviews) var a: () = () var size = flowLayout.sizeThatFits(proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a) @@ -45,7 +36,7 @@ final class CollapsibleFlowLayoutTests: XCTestCase { XCTAssertEqual(size, CGSize(width: 205, height: 105)) flowLayout.placeSubviews(in: CGRect(origin: .zero, size: size), proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a) - // 3 items are hidden in the collapsed state (put in the centre with zero size) + // 4 items are hidden in the collapsed state (put in the centre with zero size) var targetPlacements: [CGRect] = [ CGRect(x: 0, y: 25, width: 100, height: 50), CGRect(x: 105, y: 25, width: 100, height: 50), @@ -53,6 +44,7 @@ final class CollapsibleFlowLayoutTests: XCTestCase { CGRect(x: 105, y: 80, width: 100, height: 50), CGRect(x: -10000, y: -10000, width: 0, height: 0), CGRect(x: -10000, y: -10000, width: 0, height: 0), + CGRect(x: -10000, y: -10000, width: 0, height: 0), CGRect(x: -10000, y: -10000, width: 0, height: 0) ] XCTAssertEqual(placedViews, targetPlacements) @@ -74,25 +66,22 @@ final class CollapsibleFlowLayoutTests: XCTestCase { CGRect(x: 105, y: 80, width: 100, height: 50), CGRect(x: 0, y: 135, width: 100, height: 50), CGRect(x: 105, y: 135, width: 100, height: 50), - CGRect(x: 0, y: 190, width: 100, height: 50) + CGRect(x: 0, y: 190, width: 100, height: 50), + CGRect(x: 105.0, y: 190, width: 100, height: 50) ] XCTAssertEqual(placedViews, targetPlacements) } - func testFlowLayoutWithExpandButtonIsHidden() { + func testFlowLayoutWithExpandButtonAndAddMoreIsHidden() { let containerSize = CGSize(width: 250, height: 400) - let flowLayout = CollapsibleFlowLayout(itemSpacing: 5, rowSpacing: 5, rowsBeforeCollapsible: 2) + let flowLayout = CollapsibleReactionLayout(itemSpacing: 5, rowSpacing: 5, rowsBeforeCollapsible: 2) var placedViews: [CGRect] = [] let placedViewsCallback = { rect in placedViews.append(rect) } - let subviews: [LayoutSubviewMock] = [ - LayoutSubviewMock(size: CGSize(width: 100, height: 50), placedPositionCallback: placedViewsCallback), - LayoutSubviewMock(size: CGSize(width: 100, height: 50), placedPositionCallback: placedViewsCallback), - LayoutSubviewMock(size: CGSize(width: 100, height: 50), placedPositionCallback: placedViewsCallback), - LayoutSubviewMock(size: CGSize(width: 100, height: 50), placedPositionCallback: placedViewsCallback) - ] + + let subviews = createReactionLayoutSubviews(with: Array(repeating: CGSize(width: 100, height: 50), count: 3), placedPositionCallback: placedViewsCallback) let subviewsMock = LayoutSubviewsMock(subviews: subviews) var a: () = () let size = flowLayout.sizeThatFits(proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a) @@ -104,7 +93,9 @@ final class CollapsibleFlowLayoutTests: XCTestCase { CGRect(x: 0, y: 25, width: 100, height: 50), CGRect(x: 105, y: 25, width: 100, height: 50), CGRect(x: 0, y: 80, width: 100, height: 50), - // Button is hidden + // Add more button + CGRect(x: 105.0, y: 80, width: 100, height: 50), + // Expand/Collapse button is hidden CGRect(x: -10000, y: -10000, width: 0, height: 0) ] XCTAssertEqual(placedViews, targetPlacements) @@ -112,16 +103,13 @@ final class CollapsibleFlowLayoutTests: XCTestCase { func testFlowLayoutEmptyState() { let containerSize = CGSize(width: 250, height: 400) - let flowLayout = CollapsibleFlowLayout(itemSpacing: 5, rowSpacing: 5, rowsBeforeCollapsible: 2) + let flowLayout = CollapsibleReactionLayout(itemSpacing: 5, rowSpacing: 5, rowsBeforeCollapsible: 2) var placedViews: [CGRect] = [] let placedViewsCallback = { rect in placedViews.append(rect) } - let subviews: [LayoutSubviewMock] = [ - // No subviews to layout just the expand/collapse button - LayoutSubviewMock(size: CGSize(width: 100, height: 50), placedPositionCallback: placedViewsCallback) - ] + let subviews = createReactionLayoutSubviews(with: [], placedPositionCallback: placedViewsCallback) let subviewsMock = LayoutSubviewsMock(subviews: subviews) var a: () = () let size = flowLayout.sizeThatFits(proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a) @@ -130,8 +118,25 @@ final class CollapsibleFlowLayoutTests: XCTestCase { flowLayout.placeSubviews(in: CGRect(origin: .zero, size: size), proposal: ProposedViewSize(containerSize), subviews: subviewsMock, cache: &a) let targetPlacements: [CGRect] = [ + // both buttons are not displayed + CGRect(x: -10000, y: -10000, width: 0, height: 0), CGRect(x: -10000, y: -10000, width: 0, height: 0) ] XCTAssertEqual(placedViews, targetPlacements) } + + func createReactionLayoutSubviews(with sizes: [CGSize], placedPositionCallback: @escaping (CGRect) -> Void) -> [LayoutSubviewMock] { + sizes.map { size in + LayoutSubviewMock(size: size, + layoutValues: [String(describing: ReactionLayoutItemType.self): ReactionLayoutItem.reaction], + placedPositionCallback: placedPositionCallback) + } + [ + LayoutSubviewMock(size: CGSize(width: 100, height: 50), + layoutValues: [String(describing: ReactionLayoutItemType.self): ReactionLayoutItem.expandCollapse], + placedPositionCallback: placedPositionCallback), + LayoutSubviewMock(size: CGSize(width: 100, height: 50), + layoutValues: [String(describing: ReactionLayoutItemType.self): ReactionLayoutItem.addMore], + placedPositionCallback: placedPositionCallback) + ] + } } diff --git a/UnitTests/Sources/LayoutTests/LayoutMocks.swift b/UnitTests/Sources/LayoutTests/LayoutMocks.swift index 508743f928..bf521bc577 100644 --- a/UnitTests/Sources/LayoutTests/LayoutMocks.swift +++ b/UnitTests/Sources/LayoutTests/LayoutMocks.swift @@ -60,9 +60,16 @@ struct LayoutSubviewsMock: Equatable, RandomAccessCollection { /// A mock of the SwiftUI `LayoutSubview` struct struct LayoutSubviewMock: FlowLayoutSubview, Equatable { var size: CGSize - + var layoutValues = [String: Any]() var placedPositionCallback: (CGRect) -> Void + subscript(key: K.Type) -> K.Value where K: LayoutValueKey { + guard let value = layoutValues[String(describing: key.self)] as? K.Value else { + fatalError("There is no value for the provided layout key.") + } + return value + } + func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize { size }