diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 620d97264..cde37d631 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 */ @@ -26,7 +26,6 @@ 06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */; }; 071A017E415AD378F2961B11 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; }; 07240B7159A3990C4C2E8FFC /* LoginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D256FEE2F1AF1E51D39B622 /* LoginTests.swift */; }; - 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */; }; 07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */; }; 095C0ACFC234E0550A6404C5 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */; }; 095D3906CF2F940C2D2D17CC /* RoomFlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */; }; @@ -35,12 +34,14 @@ 09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7074991B3267B26D89B22 /* MockRoomTimelineController.swift */; }; 0AA0477E063E72B786A983CF /* AnalyticsPromptScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E1FF2DA52B1DE7CAEC5422 /* AnalyticsPromptScreenViewModel.swift */; }; 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; }; + 0B57C2399B9E1CE5CE0D8005 /* ComposerToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D121B4FCFC38DBCC17BCC6D6 /* ComposerToolbar.swift */; }; 0BDA19079FD6E17C5AC62E22 /* RoomDetailsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */; }; 0BE4D5CBF86956410F071F91 /* CreateRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */; }; 0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */; }; 0C47AE2CA7929CB3B0E2D793 /* ServerSelectionScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0685156EB62D7E243F097CFC /* ServerSelectionScreenViewModelProtocol.swift */; }; 0C58A846F61949B1D545D661 /* NoticeRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421E716C521F96D24ECE69B3 /* NoticeRoomTimelineItem.swift */; }; 0C797CD650DFD2876BEC5173 /* CollapsibleReactionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7C6DDBB5D12F6EF6A3D6E1 /* CollapsibleReactionLayout.swift */; }; + 0C932A5158C1D0604DFC5750 /* ComposerToolbarViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */; }; 0DC815CA24E1BD7F408F37D3 /* CollapsibleTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */; }; 0E8C480700870BB34A2A360F /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4003BC24B24C9E63D3304177 /* DeviceKit */; }; 0EA6537A07E2DC882AEA5962 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; @@ -65,6 +66,7 @@ 167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */; }; 1702981A8085BE4FB0EC001B /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33116993D54FADC0C721C1F /* Application.swift */; }; 172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */; }; + 1772AFA97DDA51CF1B293A78 /* RoomAttachmentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E6A9B9DFEE964962C179DE3 /* RoomAttachmentPicker.swift */; }; 17780569FB41E9BAC60D4710 /* UNUserNotificationCenter+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E685274772980BDEFF6691E /* UNUserNotificationCenter+Settings.swift */; }; 18867F4F1C8991EEC56EA932 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; }; @@ -88,6 +90,7 @@ 1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */; }; 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; }; 208C19811613F9A10F8A7B75 /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; }; + 20BB987875F99190A3E28632 /* MessageComposerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1756F24C1913F809A0039FD0 /* MessageComposerTextField.swift */; }; 2185C1F6724C78FFF355D6FA /* WelcomeScreenScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AB10FA6570DD08B3966C159 /* WelcomeScreenScreenUITests.swift */; }; 21BF2B7CEDFE3CA67C5355AD /* test_image.png in Resources */ = {isa = PBXBuildFile; fileRef = C733D11B421CFE3A657EF230 /* test_image.png */; }; 22882C710BC99EC34A5024A0 /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; @@ -95,7 +98,6 @@ 23701DE32ACD6FD40AA992C3 /* MediaUploadingPreprocessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */; }; 237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; }; 245F7FE5961BD10C145A26E0 /* UITimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA689E792E679F5E3956F21 /* UITimelineView.swift */; }; - 24906A1E82D0046655958536 /* MessageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18CF12478983A5EB390FB26 /* MessageComposer.swift */; }; 24A75F72EEB7561B82D726FD /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2141693488CE5446BB391964 /* Date.swift */; }; 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */; }; 25618589E0DE0F1E95FC7B5C /* EmojiProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */; }; @@ -232,12 +234,14 @@ 55CDD3968D95D1A820B5491E /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; }; 564BF06B3E93D6DD55F903B2 /* CreateRoomCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C618CA2B6C8758B06C88013C /* CreateRoomCoordinator.swift */; }; 565868808A1DA565707394ED /* CurrentValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */; }; + 56BAB81A0D03C2EF09B86294 /* ComposerToolbarModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA188BB0A216D763E46E3279 /* ComposerToolbarModels.swift */; }; 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; }; 5770C4906668C6D3008A2AC9 /* SessionVerificationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5046BB295AEAFA6FB81655 /* SessionVerificationScreenModels.swift */; }; 5780E444F405AA1304E1C23E /* DeveloperOptionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E521D6C2BF8DF0DFB35146 /* DeveloperOptionsScreen.swift */; }; 588411C8FD72B2A2DFE5F7DE /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E992D7B8BE54B2AB454613AF /* XCUIElement.swift */; }; 5894C2514400A4FBC9327632 /* ServerConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03277E40D0E0DE0712021A71 /* ServerConfirmationScreenCoordinator.swift */; }; 5897A59DDBD3592282092223 /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */; }; + 5995C63B1C61DE1373AA2BCE /* ComposerToolbarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD95B3714F806AC9CF9A557B /* ComposerToolbarViewModel.swift */; }; 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; }; 5B6E5AD224509E6C0B520D6E /* RoomMemberDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */; }; 5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; }; @@ -319,7 +323,6 @@ 7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */; }; 754602A7B2AAD443C4228ED4 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; }; - 755EE5B0998C6A4D764D86E5 /* RoomAttachmentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA85B02533375D19744EAA46 /* RoomAttachmentPicker.swift */; }; 764AFCC225B044CF5F9B41E5 /* PaginationIndicatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */; }; 76BA28216FBAF83B2D86A027 /* InvitesScreenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2A71915C1F075E403F559C /* InvitesScreenCell.swift */; }; 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; }; @@ -367,6 +370,7 @@ 854E82E064BA53CD0BC45600 /* LocationRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */; }; 85813D87DDD7F67A46BD9AF7 /* ImageProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E8A8047B50E3607ACD354E /* ImageProviderProtocol.swift */; }; 858276B19C7C0AD4CA98EA78 /* portrait_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = AF042B0FB2EE88977C91E330 /* portrait_test_image.jpg */; }; + 858B0A45257174AAFD448EA0 /* MessageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A01AECFF54281CF35909A6 /* MessageComposer.swift */; }; 858C04B62166B5BAFCD20F2D /* TimelineItemMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */; }; 85AFBB433AD56704A880F8A0 /* FramePreferenceKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */; }; 85F89F3F320F4FADCFFFE68B /* ServerSelectionScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */; }; @@ -443,6 +447,7 @@ 9B872FF37DBE6BE054903831 /* MediaUploadPreviewScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54E12B98252F6C527E31FEE /* MediaUploadPreviewScreenViewModelProtocol.swift */; }; 9BB91CABB10D8FE90C491BCD /* StaticLocationScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C833673B334A0651AB46F30B /* StaticLocationScreenViewModelTests.swift */; }; 9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */; }; + 9BEA56957B3AF954E7321658 /* ComposerToolbarViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44928D844E16EE48A311FCA /* ComposerToolbarViewModelProtocol.swift */; }; 9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */; }; 9C5A07E7C33F3F40287D7861 /* SettingsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */; }; 9CCC77C31CB399661A034739 /* UserProperties+Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A6C4BE591FE5C38CE9C7EF3 /* UserProperties+Element.swift */; }; @@ -867,7 +872,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 = ""; }; @@ -880,6 +885,7 @@ 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = ""; }; 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = ""; }; 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfigurationTests.swift; sourceTree = ""; }; + 1756F24C1913F809A0039FD0 /* MessageComposerTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerTextField.swift; sourceTree = ""; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; 1877038D1AD3D5A029F8AE2C /* TimelineReadReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReadReceiptsView.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; @@ -980,6 +986,7 @@ 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = ""; }; 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTaskService.swift; sourceTree = ""; }; 3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItem.swift; sourceTree = ""; }; + 3E6A9B9DFEE964962C179DE3 /* RoomAttachmentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAttachmentPicker.swift; sourceTree = ""; }; 3E93A1BE7D8A2EBCAD51EEB4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; 3EF1AC723C2609C7705569CA /* MediaLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderTests.swift; sourceTree = ""; }; 3F40F48279322E504153AB0D /* MockClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockClientProxy.swift; sourceTree = ""; }; @@ -1008,7 +1015,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 = ""; }; @@ -1191,7 +1198,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 = ""; }; @@ -1240,6 +1247,7 @@ A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelProtocol.swift; sourceTree = ""; }; A05707BF550D770168A406DB /* LoginViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = ""; }; A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = ""; }; + A0A01AECFF54281CF35909A6 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = ""; }; A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenCoordinator.swift; sourceTree = ""; }; A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenu.swift; sourceTree = ""; }; @@ -1263,7 +1271,6 @@ A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationProtocol.swift; sourceTree = ""; }; A9FAFE1C2149E6AC8156ED2B /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; AA19C32BD97F45847724E09A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Untranslated.strings; sourceTree = ""; }; - AA85B02533375D19744EAA46 /* RoomAttachmentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAttachmentPicker.swift; sourceTree = ""; }; AAC9344689121887B74877AF /* UnitTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AACE9B8E1A4AE79A7E2914F6 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = es; path = es.lproj/Localizable.stringsdict; sourceTree = ""; }; AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProfile+Mock.swift"; sourceTree = ""; }; @@ -1301,7 +1308,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 = ""; }; @@ -1316,6 +1323,7 @@ B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; B9227F7495DA43324050A863 /* TextBasedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineItem.swift; sourceTree = ""; }; B99E13633862847D8B7E2815 /* StartChatScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenModels.swift; sourceTree = ""; }; + BA188BB0A216D763E46E3279 /* ComposerToolbarModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarModels.swift; sourceTree = ""; }; BA241DEEF7C8A7181C0AEDC9 /* UserPreferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferenceTests.swift; sourceTree = ""; }; BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenModels.swift; sourceTree = ""; }; BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = ""; }; @@ -1324,7 +1332,6 @@ 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 = ""; }; @@ -1369,6 +1376,7 @@ C936FDD017808FE416742D64 /* PollRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollRoomTimelineView.swift; sourceTree = ""; }; C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinator.swift; sourceTree = ""; }; CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRootCoordinator.swift; sourceTree = ""; }; + CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModelTests.swift; sourceTree = ""; }; CA2A71915C1F075E403F559C /* InvitesScreenCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenCell.swift; sourceTree = ""; }; CA89A2DD51B6BBE1DA55E263 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenCoordinator.swift; sourceTree = ""; }; @@ -1382,15 +1390,17 @@ CD469F7513574341181F7EAA /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = ""; }; CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItemContent.swift; sourceTree = ""; }; CD6B0C4639E066915B5E6463 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; + CD95B3714F806AC9CF9A557B /* ComposerToolbarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModel.swift; 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 = ""; }; D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = ""; }; D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = ""; }; D0C2D52E36AD614B3C003EF6 /* RoomTimelineItemViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemViewState.swift; sourceTree = ""; }; + D121B4FCFC38DBCC17BCC6D6 /* ComposerToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbar.swift; sourceTree = ""; }; D1897720266C036471AD9D1B /* FormRowLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormRowLabelStyle.swift; sourceTree = ""; }; D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = ""; }; D316BB02636AF2174F2580E6 /* SoftLogoutScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1423,7 +1433,6 @@ DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineItem.swift; sourceTree = ""; }; E062C1750EFC8627DE4CAB8E /* MapTilerAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerAuthorization.swift; sourceTree = ""; }; E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = ""; }; - E18CF12478983A5EB390FB26 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreen.swift; sourceTree = ""; }; E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = ""; }; E26747B3154A5DBC3A7E24A5 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; @@ -1433,6 +1442,7 @@ E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = ""; }; E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModel.swift; sourceTree = ""; }; E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorModalView.swift; sourceTree = ""; }; + E44928D844E16EE48A311FCA /* ComposerToolbarViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModelProtocol.swift; sourceTree = ""; }; E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineView.swift; sourceTree = ""; }; E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyProtocol.swift; sourceTree = ""; }; @@ -1461,7 +1471,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 = ""; }; @@ -1475,7 +1485,7 @@ F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = ""; }; F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProvider.swift; sourceTree = ""; }; F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineItem.swift; sourceTree = ""; }; - F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = ""; }; + F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = ""; }; F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = ""; }; F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItem.swift; sourceTree = ""; }; F36C0A6D59717193F49EA986 /* UserSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionTests.swift; sourceTree = ""; }; @@ -1821,6 +1831,17 @@ path = Resources; sourceTree = ""; }; + 27F2500AC8736AAE774520C0 /* ComposerToolbar */ = { + isa = PBXGroup; + children = ( + BA188BB0A216D763E46E3279 /* ComposerToolbarModels.swift */, + CD95B3714F806AC9CF9A557B /* ComposerToolbarViewModel.swift */, + E44928D844E16EE48A311FCA /* ComposerToolbarViewModelProtocol.swift */, + 4BBA16517DB72736545D0F6E /* View */, + ); + path = ComposerToolbar; + sourceTree = ""; + }; 2C0F49BD446849654C0D24E0 /* RoomMember */ = { isa = PBXGroup; children = ( @@ -2174,6 +2195,17 @@ path = Tests; sourceTree = ""; }; + 4BBA16517DB72736545D0F6E /* View */ = { + isa = PBXGroup; + children = ( + D121B4FCFC38DBCC17BCC6D6 /* ComposerToolbar.swift */, + A0A01AECFF54281CF35909A6 /* MessageComposer.swift */, + 1756F24C1913F809A0039FD0 /* MessageComposerTextField.swift */, + 3E6A9B9DFEE964962C179DE3 /* RoomAttachmentPicker.swift */, + ); + path = View; + sourceTree = ""; + }; 4BF8D11D9ED15CFC373D0119 /* Analytics */ = { isa = PBXGroup; children = ( @@ -2485,6 +2517,7 @@ 6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */, EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */, 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */, + CA29952595B804DA221A0C1D /* ComposerToolbarViewModelTests.swift */, 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */, 3B5E97E9615A158C76B2AB77 /* DateTests.swift */, 6D0A27607AB09784C8501B5C /* DeveloperOptionsScreenViewModelTests.swift */, @@ -2616,9 +2649,6 @@ 79023E5904B155E8E2B8B502 /* View */ = { isa = PBXGroup; children = ( - E18CF12478983A5EB390FB26 /* MessageComposer.swift */, - BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */, - AA85B02533375D19744EAA46 /* RoomAttachmentPicker.swift */, 422724361B6555364C43281E /* RoomHeaderView.swift */, 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */, 4552D3466B1453F287223ADA /* SwipeRightAction.swift */, @@ -3495,6 +3525,7 @@ 669239C03835CD8B51E0FFDB /* AnalyticsPromptScreen */, E74CD7681375AD2EAA34D66B /* Authentication */, 53FB148CD26AFB6A5B9E20B3 /* BugReportScreen */, + 27F2500AC8736AAE774520C0 /* ComposerToolbar */, C18958141C8ED6D778F779A4 /* CreateRoom */, F5A65D1D3B83593598DC278D /* EmojiPickerScreen */, 448435400B561C40E514BE1C /* FilePreviewScreen */, @@ -4136,6 +4167,7 @@ 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */, C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */, B5321A1F5B26A0F3EC54909E /* CollapsibleFlowLayoutTests.swift in Sources */, + 0C932A5158C1D0604DFC5750 /* ComposerToolbarViewModelTests.swift in Sources */, D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */, CD0088B763CD970CF1CBF8CB /* DateTests.swift in Sources */, 864C69CF951BF36D25BE0C03 /* DeveloperOptionsScreenViewModelTests.swift in Sources */, @@ -4293,6 +4325,10 @@ 9FAF6DA7E8E85C9699757764 /* CollapsibleRoomTimelineView.swift in Sources */, 0DC815CA24E1BD7F408F37D3 /* CollapsibleTimelineItem.swift in Sources */, 663E198678778F7426A9B27D /* Collection.swift in Sources */, + 0B57C2399B9E1CE5CE0D8005 /* ComposerToolbar.swift in Sources */, + 56BAB81A0D03C2EF09B86294 /* ComposerToolbarModels.swift in Sources */, + 5995C63B1C61DE1373AA2BCE /* ComposerToolbarViewModel.swift in Sources */, + 9BEA56957B3AF954E7321658 /* ComposerToolbarViewModelProtocol.swift in Sources */, EA6613B29BA671F39CE1B1D2 /* ConfirmationDialog.swift in Sources */, AC7AA215D60FBC307F984028 /* Consumable.swift in Sources */, C3522917C0C367C403429EEC /* CoordinatorProtocol.swift in Sources */, @@ -4444,8 +4480,8 @@ A969147E0EEE0E27EE226570 /* MediaUploadPreviewScreenViewModel.swift in Sources */, 9B872FF37DBE6BE054903831 /* MediaUploadPreviewScreenViewModelProtocol.swift in Sources */, 8A0BD60CA4A6004DB06B5403 /* MediaUploadingPreprocessor.swift in Sources */, - 24906A1E82D0046655958536 /* MessageComposer.swift in Sources */, - 072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */, + 858B0A45257174AAFD448EA0 /* MessageComposer.swift in Sources */, + 20BB987875F99190A3E28632 /* MessageComposerTextField.swift in Sources */, C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */, 2BBA132149DEBED6624084A8 /* MessageForwardingScreenCoordinator.swift in Sources */, 695825D20A761C678809345D /* MessageForwardingScreenModels.swift in Sources */, @@ -4521,7 +4557,7 @@ 42A5A42ACF063EEE6B1980D2 /* ReportContentScreenViewModel.swift in Sources */, 8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */, A494741843F087881299ACF0 /* RestorationToken.swift in Sources */, - 755EE5B0998C6A4D764D86E5 /* RoomAttachmentPicker.swift in Sources */, + 1772AFA97DDA51CF1B293A78 /* RoomAttachmentPicker.swift in Sources */, 0BDA19079FD6E17C5AC62E22 /* RoomDetailsEditScreen.swift in Sources */, E78D429F18071545BF661A52 /* RoomDetailsEditScreenCoordinator.swift in Sources */, A6D4C5EEA85A6A0ABA1559D6 /* RoomDetailsEditScreenModels.swift in Sources */, diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 395a59fe4..241291ba3 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -208,7 +208,7 @@ { "identity" : "swiftui-introspect", "kind" : "remoteSourceControl", - "location" : "https://github.com/siteline/SwiftUI-Introspect", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", "state" : { "revision" : "b94da693e57eaf79d16464b8b7c90d09cba4e290", "version" : "0.9.2" diff --git a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift new file mode 100644 index 000000000..8a5a8a83a --- /dev/null +++ b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarModels.swift @@ -0,0 +1,63 @@ +// +// Copyright 2023 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 UIKit + +enum ComposerToolbarViewModelAction { + case sendMessage(message: String, mode: RoomScreenComposerMode) + + case displayCameraPicker + case displayMediaPicker + case displayDocumentPicker + case displayLocationPicker + + case handlePasteOrDrop(provider: NSItemProvider) + + case composerModeChanged(mode: RoomScreenComposerMode) + case focusedChanged(isFocused: Bool) +} + +enum ComposerToolbarViewAction { + case sendMessage(message: String, mode: RoomScreenComposerMode) + case cancelReply + case cancelEdit + case displayCameraPicker + case displayMediaPicker + case displayDocumentPicker + case displayLocationPicker + case handlePasteOrDrop(provider: NSItemProvider) +} + +struct ComposerToolbarViewState: BindableState { + var composerMode: RoomScreenComposerMode = .default + + var bindings: ComposerToolbarViewStateBindings + + var sendButtonDisabled: Bool { + bindings.composerText.count == 0 + } +} + +struct ComposerToolbarViewStateBindings { + var composerText: String + var composerFocused: Bool + + var showAttachmentPopover = false { + didSet { + composerFocused = false + } + } +} diff --git a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift new file mode 100644 index 000000000..40d2ca623 --- /dev/null +++ b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModel.swift @@ -0,0 +1,96 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +typealias ComposerToolbarViewModelType = StateStoreViewModel + +final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerToolbarViewModelProtocol { + private let actionsSubject: PassthroughSubject = .init() + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init() { + super.init(initialViewState: ComposerToolbarViewState(bindings: .init(composerText: "", composerFocused: false))) + + context.$viewState + .map(\.composerMode) + .removeDuplicates() + .sink { [weak self] in self?.actionsSubject.send(.composerModeChanged(mode: $0)) } + .store(in: &cancellables) + + context.$viewState + .map(\.bindings.composerFocused) + .removeDuplicates() + .sink { [weak self] in self?.actionsSubject.send(.focusedChanged(isFocused: $0)) } + .store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: ComposerToolbarViewAction) { + switch viewAction { + case .sendMessage(let message, let mode): + actionsSubject.send(.sendMessage(message: message, mode: mode)) + case .cancelReply: + set(mode: .default) + case .cancelEdit: + set(mode: .default) + set(text: "") + case .displayCameraPicker: + actionsSubject.send(.displayCameraPicker) + case .displayMediaPicker: + actionsSubject.send(.displayMediaPicker) + case .displayDocumentPicker: + actionsSubject.send(.displayDocumentPicker) + case .displayLocationPicker: + actionsSubject.send(.displayLocationPicker) + case .handlePasteOrDrop(let provider): + actionsSubject.send(.handlePasteOrDrop(provider: provider)) + } + } + + func process(roomAction: RoomScreenComposerAction) { + switch roomAction { + case .setMode(mode: let mode): + set(mode: mode) + case .setText(text: let text): + set(text: text) + case .removeFocus: + state.bindings.composerFocused = false + case .clear: + set(mode: .default) + set(text: "") + } + } + + // MARK: - Private + + private func set(mode: RoomScreenComposerMode) { + guard mode != state.composerMode else { return } + + state.composerMode = mode + if mode != .default { + // Focus composer when switching to reply/edit + state.bindings.composerFocused = true + } + } + + private func set(text: String) { + state.bindings.composerText = text + } +} diff --git a/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModelProtocol.swift b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModelProtocol.swift new file mode 100644 index 000000000..7906ca5c9 --- /dev/null +++ b/ElementX/Sources/Screens/ComposerToolbar/ComposerToolbarViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +protocol ComposerToolbarViewModelProtocol { + var actions: AnyPublisher { get } + var context: ComposerToolbarViewModelType.Context { get } + func process(roomAction: RoomScreenComposerAction) +} diff --git a/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift b/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift new file mode 100644 index 000000000..1560e4cfd --- /dev/null +++ b/ElementX/Sources/Screens/ComposerToolbar/View/ComposerToolbar.swift @@ -0,0 +1,50 @@ +// +// Copyright 2023 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 ComposerToolbar: View { + @ObservedObject var context: ComposerToolbarViewModel.Context + + var body: some View { + HStack(alignment: .bottom, spacing: 10) { + RoomAttachmentPicker(context: context) + .padding(.bottom, 5) // centre align with the send button + messageComposer + .environmentObject(context) + } + } + + private var messageComposer: some View { + MessageComposer(text: $context.composerText, + focused: $context.composerFocused, + sendingDisabled: context.viewState.sendButtonDisabled, + mode: context.viewState.composerMode) { + sendMessage() + } pasteAction: { provider in + context.send(viewAction: .handlePasteOrDrop(provider: provider)) + } replyCancellationAction: { + context.send(viewAction: .cancelReply) + } editCancellationAction: { + context.send(viewAction: .cancelEdit) + } + } + + private func sendMessage() { + guard !context.viewState.sendButtonDisabled else { return } + context.send(viewAction: .sendMessage(message: context.composerText, mode: context.viewState.composerMode)) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift b/ElementX/Sources/Screens/ComposerToolbar/View/MessageComposer.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift rename to ElementX/Sources/Screens/ComposerToolbar/View/MessageComposer.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/MessageComposerTextField.swift b/ElementX/Sources/Screens/ComposerToolbar/View/MessageComposerTextField.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/MessageComposerTextField.swift rename to ElementX/Sources/Screens/ComposerToolbar/View/MessageComposerTextField.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomAttachmentPicker.swift b/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift similarity index 85% rename from ElementX/Sources/Screens/RoomScreen/View/RoomAttachmentPicker.swift rename to ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift index b77125b25..521426c80 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomAttachmentPicker.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/View/RoomAttachmentPicker.swift @@ -17,7 +17,7 @@ import SwiftUI struct RoomAttachmentPicker: View { - @ObservedObject var context: RoomScreenViewModel.Context + @ObservedObject var context: ComposerToolbarViewModel.Context @Environment(\.isPresented) var isPresented @State private var sheetContentHeight = CGFloat(0) @@ -102,12 +102,8 @@ struct RoomAttachmentPicker: View { } struct RoomAttachmentPicker_Previews: PreviewProvider { - static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), - roomProxy: RoomProxyMock(with: .init(displayName: "")), - appSettings: ServiceLocator.shared.settings, - analytics: ServiceLocator.shared.analytics, - userIndicatorController: ServiceLocator.shared.userIndicatorController) + static let viewModel = ComposerToolbarViewModel() + static var previews: some View { RoomAttachmentPicker(context: viewModel.context) } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 748e79962..683aa5286 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -38,8 +38,10 @@ enum RoomScreenCoordinatorAction { final class RoomScreenCoordinator: CoordinatorProtocol { private var parameters: RoomScreenCoordinatorParameters - private var viewModel: RoomScreenViewModelProtocol + private var composerViewModel: ComposerToolbarViewModel + + private var cancellables = Set() private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { @@ -48,49 +50,63 @@ final class RoomScreenCoordinator: CoordinatorProtocol { init(parameters: RoomScreenCoordinatorParameters) { self.parameters = parameters - + viewModel = RoomScreenViewModel(timelineController: parameters.timelineController, mediaProvider: parameters.mediaProvider, roomProxy: parameters.roomProxy, appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController) + + composerViewModel = ComposerToolbarViewModel() } // MARK: - Public func start() { - viewModel.callback = { [weak self] action in - guard let self else { return } - - switch action { - case .displayRoomDetails: - actionsSubject.send(.presentRoomDetails) - case .displayEmojiPicker(let itemID, let selectedEmojis): - actionsSubject.send(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis)) - case .displayReportContent(let itemID, let senderID): - actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID)) - case .displayCameraPicker: - actionsSubject.send(.presentMediaUploadPicker(.camera)) - case .displayMediaPicker: - actionsSubject.send(.presentMediaUploadPicker(.photoLibrary)) - case .displayDocumentPicker: - actionsSubject.send(.presentMediaUploadPicker(.documents)) - case .displayLocationPicker: - actionsSubject.send(.presentLocationPicker) - case .displayMediaUploadPreviewScreen(let url): - actionsSubject.send(.presentMediaUploadPreviewScreen(url)) - case .displayRoomMemberDetails(let member): - actionsSubject.send(.presentRoomMemberDetails(member: member)) - case .displayMessageForwarding(let itemID): - actionsSubject.send(.presentMessageForwarding(itemID: itemID)) - case .displayLocation(let body, let geoURI, let description): - actionsSubject.send(.presentLocationViewer(body: body, geoURI: geoURI, description: description)) + viewModel.actions + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .displayRoomDetails: + actionsSubject.send(.presentRoomDetails) + case .displayEmojiPicker(let itemID, let selectedEmojis): + actionsSubject.send(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis)) + case .displayReportContent(let itemID, let senderID): + actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID)) + case .displayCameraPicker: + actionsSubject.send(.presentMediaUploadPicker(.camera)) + case .displayMediaPicker: + actionsSubject.send(.presentMediaUploadPicker(.photoLibrary)) + case .displayDocumentPicker: + actionsSubject.send(.presentMediaUploadPicker(.documents)) + case .displayLocationPicker: + actionsSubject.send(.presentLocationPicker) + case .displayMediaUploadPreviewScreen(let url): + actionsSubject.send(.presentMediaUploadPreviewScreen(url)) + case .displayRoomMemberDetails(let member): + actionsSubject.send(.presentRoomMemberDetails(member: member)) + case .displayMessageForwarding(let itemID): + actionsSubject.send(.presentMessageForwarding(itemID: itemID)) + case .displayLocation(let body, let geoURI, let description): + actionsSubject.send(.presentLocationViewer(body: body, geoURI: geoURI, description: description)) + case .composer(let action): + composerViewModel.process(roomAction: action) + } } - } + .store(in: &cancellables) + + composerViewModel.actions + .sink { [weak self] composerAction in + guard let self else { return } + + viewModel.process(composerAction: composerAction) + } + .store(in: &cancellables) } func toPresentable() -> AnyView { - AnyView(RoomScreen(context: viewModel.context)) + AnyView(RoomScreen(context: viewModel.context, composerToolbar: ComposerToolbar(context: composerViewModel.context))) } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 5fc646acd..479cf9fbb 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -32,6 +32,7 @@ enum RoomScreenViewModelAction { case displayRoomMemberDetails(member: RoomMemberProxyProtocol) case displayMessageForwarding(itemID: TimelineItemIdentifier) case displayLocation(body: String, geoURI: GeoURI, description: String?) + case composer(action: RoomScreenComposerAction) } enum RoomScreenComposerMode: Equatable { @@ -55,10 +56,7 @@ enum RoomScreenViewAction { case itemDisappeared(itemID: TimelineItemIdentifier) case itemTapped(itemID: TimelineItemIdentifier) case linkClicked(url: URL) - case sendMessage case toggleReaction(key: String, itemID: TimelineItemIdentifier) - case cancelReply - case cancelEdit case sendReadReceiptIfNeeded(TimelineItemIdentifier) case paginateBackwards @@ -67,11 +65,6 @@ enum RoomScreenViewAction { case displayEmojiPicker(itemID: TimelineItemIdentifier) - case displayCameraPicker - case displayMediaPicker - case displayDocumentPicker - case displayLocationPicker - case handlePasteOrDrop(provider: NSItemProvider) case tappedOnUser(userID: String) @@ -83,6 +76,13 @@ enum RoomScreenViewAction { case scrolledToBottom } +enum RoomScreenComposerAction { + case setMode(mode: RoomScreenComposerMode) + case setText(text: String) + case removeFocus + case clear +} + struct RoomScreenViewState: BindableState { var roomID: String var roomTitle = "" @@ -93,29 +93,16 @@ struct RoomScreenViewState: BindableState { var readReceiptsEnabled: Bool var isEncryptedOneToOneRoom = false var timelineViewState = TimelineViewState() // check the doc before changing this - var composerMode: RoomScreenComposerMode = .default var swiftUITimelineEnabled = false - + var bindings: RoomScreenViewStateBindings /// A closure providing the actions to show when long pressing on an item in the timeline. var timelineItemMenuActionProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> TimelineItemMenuActions?)? - - var sendButtonDisabled: Bool { - bindings.composerText.count == 0 - } } struct RoomScreenViewStateBindings { - var composerText: String - var composerFocused: Bool - var isScrolledToBottom = true - var showAttachmentPopover = false { - didSet { - composerFocused = false - } - } /// The state of wether reactions listed on the timeline are expanded/collapsed. /// Key is itemID, value is the collapsed state. diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 8659802b4..44c581b23 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -34,11 +34,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private let analytics: AnalyticsService private unowned let userIndicatorController: UserIndicatorControllerProtocol private let notificationCenterProtocol: NotificationCenterProtocol + private let composerFocusedSubject = PassthroughSubject() + + private let actionsSubject: PassthroughSubject = .init() private var canCurrentUserRedact = false private var paginateBackwardsTask: Task? - + init(timelineController: RoomTimelineControllerProtocol, mediaProvider: MediaProviderProtocol, roomProxy: RoomProxyProtocol, @@ -59,11 +62,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol timelineStyle: appSettings.timelineStyle, readReceiptsEnabled: appSettings.readReceiptsEnabled, isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom, - bindings: .init(composerText: "", composerFocused: false, reactionsCollapsed: [:])), + bindings: .init(reactionsCollapsed: [:])), imageProvider: mediaProvider) setupSubscriptions() - + setupDirectRoomSubscriptionsIfNeeded() + state.timelineItemMenuActionProvider = { [weak self] itemId -> TimelineItemMenuActions? in guard let self else { return nil @@ -73,18 +77,22 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } buildTimelineViews() - - trackComposerMode() + + // Note: beware if we get to e.g. restore a reply / edit, + // maybe we are tracking a non-needed first initial state + trackComposerMode(.default) } // MARK: - Public - var callback: ((RoomScreenViewModelAction) -> Void)? + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } override func process(viewAction: RoomScreenViewAction) { switch viewAction { case .displayRoomDetails: - callback?(.displayRoomDetails) + actionsSubject.send(.displayRoomDetails) case .itemAppeared(let id): Task { await timelineController.processItemAppearance(id) } case .itemDisappeared(let id): @@ -93,15 +101,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol Task { await itemTapped(with: id) } case .linkClicked(let url): MXLog.warning("Link clicked: \(url)") - case .sendMessage: - Task { await sendCurrentMessage() } case .toggleReaction(let emoji, let itemId): Task { await timelineController.toggleReaction(emoji, to: itemId) } - case .cancelReply: - setComposerMode(.default) - case .cancelEdit: - setComposerMode(.default) - state.bindings.composerText = "" case .sendReadReceiptIfNeeded(let lastVisibleItemID): Task { await sendReadReceiptIfNeeded(for: lastVisibleItemID) } case .timelineItemMenu(let itemID): @@ -115,14 +116,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } case .timelineItemMenuAction(let itemID, let action): processTimelineItemMenuAction(action, itemID: itemID) - case .displayCameraPicker: - callback?(.displayCameraPicker) - case .displayMediaPicker: - callback?(.displayMediaPicker) - case .displayDocumentPicker: - callback?(.displayDocumentPicker) - case .displayLocationPicker: - callback?(.displayLocationPicker) case .handlePasteOrDrop(let provider): handlePasteOrDrop(provider) case .tappedOnUser(userID: let userID): @@ -143,6 +136,27 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } } + + func process(composerAction: ComposerToolbarViewModelAction) { + switch composerAction { + case .sendMessage(let message, let mode): + Task { await sendCurrentMessage(message, mode: mode) } + case .displayCameraPicker: + actionsSubject.send(.displayCameraPicker) + case .displayMediaPicker: + actionsSubject.send(.displayMediaPicker) + case .displayDocumentPicker: + actionsSubject.send(.displayDocumentPicker) + case .displayLocationPicker: + actionsSubject.send(.displayLocationPicker) + case .handlePasteOrDrop(let provider): + handlePasteOrDrop(provider) + case .composerModeChanged(mode: let mode): + trackComposerMode(mode) + case .focusedChanged(isFocused: let isFocused): + composerFocusedSubject.send(isFocused) + } + } // MARK: - Private @@ -197,8 +211,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } .weakAssign(to: \.state.members, on: self) .store(in: &cancellables) - - setupDirectRoomSubscriptionsIfNeeded() } private func setupDirectRoomSubscriptionsIfNeeded() { @@ -206,8 +218,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol return } - let shouldShowInviteAlert = context.$viewState - .map(\.bindings.composerFocused) + let shouldShowInviteAlert = composerFocusedSubject .removeDuplicates() .map { [weak self] isFocused in guard let self else { return false } @@ -292,10 +303,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol switch action { case .displayMediaFile(let file, let title): - state.bindings.composerFocused = false // Hide the keyboard otherwise a big white space is sometimes shown when dismissing the preview. + actionsSubject.send(.composer(action: .removeFocus)) // Hide the keyboard otherwise a big white space is sometimes shown when dismissing the preview. state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: title) case .displayLocation(let body, let geoURI, let description): - callback?(.displayLocation(body: body, geoURI: geoURI, description: description)) + actionsSubject.send(.displayLocation(body: body, geoURI: geoURI, description: description)) case .none: break } @@ -409,18 +420,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol return eventTimelineItem.properties.reactions.isEmpty && eventTimelineItem.sender == otherEventTimelineItem.sender } - private func sendCurrentMessage() async { - guard !state.bindings.composerText.isEmpty else { + private func sendCurrentMessage(_ currentMessage: String, mode: RoomScreenComposerMode) async { + guard !currentMessage.isEmpty else { fatalError("This message should never be empty") } - - let currentMessage = state.bindings.composerText - let currentComposerState = state.composerMode - state.bindings.composerText = "" - setComposerMode(.default) + actionsSubject.send(.composer(action: .clear)) - switch currentComposerState { + switch mode { case .reply(let itemId, _): await timelineController.sendMessage(currentMessage, inReplyTo: itemId) case .edit(let originalItemId): @@ -430,16 +437,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } - private func setComposerMode(_ mode: RoomScreenComposerMode) { - guard mode != state.composerMode else { return } - state.composerMode = mode - trackComposerMode() - } - - private func trackComposerMode() { + private func trackComposerMode(_ mode: RoomScreenComposerMode) { var isEdit = false var isReply = false - switch state.composerMode { + switch mode { case .edit: isEdit = true case .reply: @@ -473,7 +474,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol // Don't show a menu for non-event based items. return } - + + actionsSubject.send(.composer(action: .removeFocus)) state.bindings.actionMenuInfo = .init(item: eventTimelineItem) } @@ -555,10 +557,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol guard let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol else { return } - - state.bindings.composerFocused = true - state.bindings.composerText = messageTimelineItem.body - setComposerMode(.edit(originalItemId: messageTimelineItem.id)) + + actionsSubject.send(.composer(action: .setText(text: messageTimelineItem.body))) + actionsSubject.send(.composer(action: .setMode(mode: .edit(originalItemId: messageTimelineItem.id)))) case .copyPermalink: do { guard let eventID = eventTimelineItem.id.eventID else { @@ -581,13 +582,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } case .reply: - state.bindings.composerFocused = true - let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, contentType: buildReplyContent(for: eventTimelineItem)) - - setComposerMode(.reply(itemID: eventTimelineItem.id, replyDetails: replyDetails)) + + actionsSubject.send(.composer(action: .setMode(mode: .reply(itemID: eventTimelineItem.id, replyDetails: replyDetails)))) case .forward(let itemID): - callback?(.displayMessageForwarding(itemID: itemID)) + actionsSubject.send(.displayMessageForwarding(itemID: itemID)) case .viewSource: let debugInfo = timelineController.debugInfo(for: eventTimelineItem.id) MXLog.info(debugInfo) @@ -597,13 +596,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol await timelineController.retryDecryption(for: sessionID) } case .report: - callback?(.displayReportContent(itemID: itemID, senderID: eventTimelineItem.sender.id)) + actionsSubject.send(.displayReportContent(itemID: itemID, senderID: eventTimelineItem.sender.id)) case .react: showEmojiPicker(for: itemID) } if action.switchToDefaultComposer { - setComposerMode(.default) + actionsSubject.send(.composer(action: .setMode(mode: .default))) } } @@ -652,7 +651,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } }.value - self.callback?(.displayMediaUploadPreviewScreen(url: url)) + self.actionsSubject.send(.displayMediaUploadPreviewScreen(url: url)) } catch { self.displayError(.toast(L10n.screenRoomErrorFailedProcessingMedia)) MXLog.error("Failed storing NSItemProvider data \(providerDescription) with error: \(error)") @@ -678,7 +677,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol switch result { case .success(let member): - callback?(.displayRoomMemberDetails(member: member)) + actionsSubject.send(.displayRoomMemberDetails(member: member)) case .failure(let error): displayError(.alert(L10n.screenRoomErrorFailedRetrievingUserDetails)) MXLog.error("Failed retrieving the user given the following id \(userID) with error: \(error)") @@ -770,7 +769,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol return } let selectedEmojis = Set(eventTimelineItem.properties.reactions.compactMap { $0.isHighlighted ? $0.key : nil }) - callback?(.displayEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis)) + actionsSubject.send(.displayEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis)) } private func showReactionSummary(for itemID: TimelineItemIdentifier, selectedKey: String) { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift index c90575394..14fd53271 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift @@ -14,10 +14,13 @@ // limitations under the License. // +import Combine import Foundation +import SwiftUI @MainActor protocol RoomScreenViewModelProtocol { - var callback: ((RoomScreenViewModelAction) -> Void)? { get set } + var actions: AnyPublisher { get } var context: RoomScreenViewModelType.Context { get } + func process(composerAction: ComposerToolbarViewModelAction) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 93cc2e5a7..48476b220 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -19,24 +19,20 @@ import SwiftUI struct RoomScreen: View { @ObservedObject var context: RoomScreenViewModel.Context @State private var dragOver = false - + let composerToolbar: ComposerToolbar + private let attachmentButtonPadding = 10.0 var body: some View { timeline .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .safeAreaInset(edge: .bottom, spacing: 0) { - HStack(alignment: .bottom, spacing: attachmentButtonPadding) { - RoomAttachmentPicker(context: context) - .padding(.bottom, 5) // centre align with the send button - messageComposer - .environmentObject(context) - } - .padding(.leading, attachmentButtonPadding) - .padding(.trailing, 12) - .padding(.top, 8) - .padding(.bottom) - .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) + composerToolbar + .padding(.leading, attachmentButtonPadding) + .padding(.trailing, 12) + .padding(.top, 8) + .padding(.bottom) + .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) } .navigationBarTitleDisplayMode(.inline) .toolbar { toolbar } @@ -124,24 +120,6 @@ struct RoomScreen: View { .animation(.elementDefault, value: context.isScrolledToBottom) } - private var messageComposer: some View { - MessageComposer(text: $context.composerText, - focused: $context.composerFocused, - sendingDisabled: context.viewState.sendButtonDisabled, - mode: context.viewState.composerMode) { - sendMessage() - } pasteAction: { provider in - context.send(viewAction: .handlePasteOrDrop(provider: provider)) - } replyCancellationAction: { - context.send(viewAction: .cancelReply) - } editCancellationAction: { - context.send(viewAction: .cancelEdit) - } - .onChange(of: context.actionMenuInfo) { _ in - context.composerFocused = false - } - } - @ViewBuilder private var loadingIndicator: some View { if context.viewState.showLoading { @@ -162,11 +140,6 @@ struct RoomScreen: View { RoomHeaderView(context: context) } } - - private func sendMessage() { - guard !context.viewState.sendButtonDisabled else { return } - context.send(viewAction: .sendMessage) - } } // MARK: - Previews @@ -178,10 +151,12 @@ struct RoomScreen_Previews: PreviewProvider { appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController) + + static let composerViewModel = ComposerToolbarViewModel() static var previews: some View { NavigationStack { - RoomScreen(context: viewModel.context) + RoomScreen(context: viewModel.context, composerToolbar: ComposerToolbar(context: composerViewModel.context)) } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift index 3828563a5..c5d41b15b 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift @@ -52,10 +52,6 @@ class TimelineTableViewController: UIViewController { sendReadReceiptIfNeeded() } } - - /// The mode of the message composer. This is used to render selected - /// items in the timeline when replying, editing etc. - var composerMode: RoomScreenComposerMode = .default /// Whether or not the timeline has more messages to back paginate. var canBackPaginate = true diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/UITimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/UITimelineView.swift index 58e695e1e..1986e92fb 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/UITimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/UITimelineView.swift @@ -65,9 +65,6 @@ struct UITimelineView: UIViewControllerRepresentable { if tableViewController.isBackPaginating != context.viewState.timelineViewState.isBackPaginating { tableViewController.isBackPaginating = context.viewState.timelineViewState.isBackPaginating } - if tableViewController.composerMode != context.viewState.composerMode { - tableViewController.composerMode = context.viewState.composerMode - } // Doesn't have an equatable conformance :( tableViewController.contextMenuActionProvider = context.viewState.timelineItemMenuActionProvider @@ -88,10 +85,12 @@ struct UITimelineView_Previews: PreviewProvider { appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController) + + static let composerViewModel = ComposerToolbarViewModel() static var previews: some View { NavigationStack { - RoomScreen(context: viewModel.context) + RoomScreen(context: viewModel.context, composerToolbar: ComposerToolbar(context: composerViewModel.context)) } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift index 3b582e769..68af3ec73 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineView.swift @@ -174,9 +174,11 @@ struct TimelineView_Previews: PreviewProvider { analytics: ServiceLocator.shared.analytics, userIndicatorController: ServiceLocator.shared.userIndicatorController) + static let composerViewModel = ComposerToolbarViewModel() + static var previews: some View { NavigationStack { - RoomScreen(context: viewModel.context) + RoomScreen(context: viewModel.context, composerToolbar: ComposerToolbar(context: composerViewModel.context)) } } } diff --git a/UnitTests/Sources/ComposerToolbarViewModelTests.swift b/UnitTests/Sources/ComposerToolbarViewModelTests.swift new file mode 100644 index 000000000..ef47c2703 --- /dev/null +++ b/UnitTests/Sources/ComposerToolbarViewModelTests.swift @@ -0,0 +1,59 @@ +// +// Copyright 2023 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. +// + +@testable import ElementX +import XCTest + +@MainActor +class ComposerToolbarViewModelTests: XCTestCase { + func testComposerFocus() { + let viewModel = ComposerToolbarViewModel() + viewModel.process(roomAction: .setMode(mode: .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock")))) + XCTAssertTrue(viewModel.state.bindings.composerFocused) + viewModel.process(roomAction: .removeFocus) + XCTAssertFalse(viewModel.state.bindings.composerFocused) + } + + func testComposerMode() { + let viewModel = ComposerToolbarViewModel() + let mode: RoomScreenComposerMode = .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock")) + viewModel.process(roomAction: .setMode(mode: mode)) + XCTAssertEqual(viewModel.state.composerMode, mode) + viewModel.process(roomAction: .clear) + XCTAssertEqual(viewModel.state.composerMode, .default) + } + + func testComposerModeIsPublished() { + let viewModel = ComposerToolbarViewModel() + let mode: RoomScreenComposerMode = .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock")) + let expectation = expectation(description: "Composer mode is published") + let cancellable = viewModel + .context + .$viewState + .map(\.composerMode) + .removeDuplicates() + .dropFirst() + .sink(receiveValue: { composerMode in + XCTAssertEqual(composerMode, mode) + expectation.fulfill() + }) + + viewModel.process(roomAction: .setMode(mode: mode)) + + wait(for: [expectation], timeout: 2.0) + cancellable.cancel() + } +} diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index 78573d781..78936a417 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -15,11 +15,14 @@ // @testable import ElementX + +import Combine import XCTest @MainActor class RoomScreenViewModelTests: XCTestCase { var userIndicatorControllerMock: UserIndicatorControllerMock! + var cancellables = Set() override func setUp() async throws { userIndicatorControllerMock = UserIndicatorControllerMock.default @@ -195,15 +198,17 @@ class RoomScreenViewModelTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, userIndicatorController: userIndicatorControllerMock) - viewModel.callback = { action in - switch action { - case .displayRoomMemberDetails(let member): - XCTAssert(member === roomMemberMock) - default: - XCTFail("Did not received the expected action") + viewModel.actions + .sink { action in + switch action { + case .displayRoomMemberDetails(let member): + XCTAssert(member === roomMemberMock) + default: + XCTFail("Did not received the expected action") + } + expectation.fulfill() } - expectation.fulfill() - } + .store(in: &cancellables) // Test viewModel.context.send(viewAction: .tappedOnUser(userID: "bob")) @@ -232,15 +237,17 @@ class RoomScreenViewModelTests: XCTestCase { analytics: ServiceLocator.shared.analytics, userIndicatorController: userIndicatorControllerMock) - viewModel.callback = { action in - switch action { - case .displayRoomMemberDetails(let member): - XCTAssert(member === roomMemberMock) - expectation.fulfill() - default: - XCTFail("Did not received the expected action") + viewModel.actions + .sink { action in + switch action { + case .displayRoomMemberDetails(let member): + XCTAssert(member === roomMemberMock) + expectation.fulfill() + default: + XCTFail("Did not received the expected action") + } } - } + .store(in: &cancellables) // Test viewModel.context.send(viewAction: .tappedOnUser(userID: "bob")) @@ -268,9 +275,11 @@ class RoomScreenViewModelTests: XCTestCase { appSettings: ServiceLocator.shared.settings, analytics: ServiceLocator.shared.analytics, userIndicatorController: userIndicatorControllerMock) - viewModel.callback = { _ in - XCTFail("Should not receive any action") - } + viewModel.actions + .sink { _ in + XCTFail("Should not receive any action") + } + .store(in: &cancellables) // Test let deferred = deferFulfillment(viewModel.context.$viewState.collect(2).first(),