diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 5903c6e4a..196284c62 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -62,6 +62,7 @@ 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 */; }; + 0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59B7CC77B82C6C67DE3AD869 /* HighlightedTimelineItemModifier.swift */; }; 0C346A4AD174F441EDB1414E /* IdentityConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB76A9AFC6CCAD4998D9B045 /* IdentityConfirmationScreenViewModel.swift */; }; 0C47AE2CA7929CB3B0E2D793 /* ServerSelectionScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0685156EB62D7E243F097CFC /* ServerSelectionScreenViewModelProtocol.swift */; }; 0C58A846F61949B1D545D661 /* NoticeRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421E716C521F96D24ECE69B3 /* NoticeRoomTimelineItem.swift */; }; @@ -72,7 +73,6 @@ 0CF81807BE5FBFC9E2BBCECF /* PollFormScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3368395F06AA180138E185B6 /* PollFormScreenUITests.swift */; }; 0D4EB2ABAA5FE8CB10FDBCB8 /* TimelineItemFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */; }; 0DC815CA24E1BD7F408F37D3 /* CollapsibleTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */; }; - 0DCDF49AB95F75BFC8B1879C /* SwipeToReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E45C3DC740D3AB9A47FD32 /* SwipeToReplyView.swift */; }; 0E08BB72B2258652CF501A8B /* Prefire in Frameworks */ = {isa = PBXBuildFile; productRef = 9B68DE8678BF67D4612BCC16 /* Prefire */; }; 0E3A2787C6AEC761A81A938A /* AuthenticationStartScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8609BE4CA71C30D1FCE3AF9B /* AuthenticationStartScreenModels.swift */; }; 0E8C480700870BB34A2A360F /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4003BC24B24C9E63D3304177 /* DeviceKit */; }; @@ -80,15 +80,18 @@ 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; 0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */; }; 0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */; }; + 109AEB7D33C4497727AFB87F /* TimelineInteractionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BA894BC09972DC45E497D37 /* TimelineInteractionHandler.swift */; }; 10D60D287025B71F4743A425 /* RoomDirectorySearchProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471BB7276C97AF60B3A5463B /* RoomDirectorySearchProxy.swift */; }; 1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0376C429FAB1687C3D905F3E /* MockCoder.swift */; }; 119AE9A3FC6E0606C1146528 /* NotificationSettingsEditScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */; }; 11A6B8E3CBDBF0A4107FF4CE /* OnboardingFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */; }; 126EE01D8BEAEF26105D83C5 /* RoomDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */; }; + 128FFD8A3D85845F9A927F47 /* PollRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF8548D48512127CCC17C520 /* PollRoomTimelineView.swift */; }; 12C867E85E6D12EEDFD0B127 /* CustomStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */; }; 12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3F82523D6F48B926D6AF68 /* AppSettings.swift */; }; + 12CD8B5CC30A05061228BF9E /* TimelineItemMenuActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E6065FC6BC4A1B4C629E08 /* TimelineItemMenuActionProvider.swift */; }; + 1307268DC41730E5BCF7D9A0 /* PollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638790D3F915F0909315C47A /* PollView.swift */; }; 1318721F4E5F307586D98112 /* VoiceMessageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8516302ACCA94A0E680AB3B /* VoiceMessageButton.swift */; }; - 13853973A5E24374FCEDE8A3 /* RedactedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */; }; 13C77FDF17C4C6627CFFC205 /* RoomTimelineItemFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */; }; 13CBC470FB619A6393A21908 /* RoomNotificationSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8296D6FB451E25CEC0767BBA /* RoomNotificationSettingsScreenCoordinator.swift */; }; 14343C2F9AD2BFEA92CA28FF /* MapTilerStyleBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7AE92E7BFF71797BDE1D261 /* MapTilerStyleBuilder.swift */; }; @@ -97,12 +100,11 @@ 14E99D27628B1A6F0CB46FEA /* SeparatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A9F49B3EE59147AF2F70BB /* SeparatorRoomTimelineItem.swift */; }; 151D2477F75782C8702F2873 /* PollInteractionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */; }; 152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */; }; - 153E22E8227F46545E5D681C /* PollRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C936FDD017808FE416742D64 /* PollRoomTimelineView.swift */; }; 155063E980E763D4910EA3CF /* Analytics+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */; }; 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */; }; - 157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */; }; 1583E2D766E4485FF91662FC /* PermalinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3EB5B1848CF4F64E63C6B7 /* PermalinkTests.swift */; }; 15913A5B07118C1268A840E4 /* RoomSummaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */; }; + 1653275750CE11F5CE94DDFD /* ReadReceiptsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8063E65441E771200108C558 /* ReadReceiptsSummaryView.swift */; }; 167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */; }; 16CBD087038DE3815CDA512C /* PollMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D38391154120264910D19528 /* PollMock.swift */; }; 16E4F1B8B9BFE1367F96DDA7 /* CompletionSuggestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989FC684408B31A677F5538B /* CompletionSuggestionView.swift */; }; @@ -114,6 +116,7 @@ 18E3786918486D4C9726BC84 /* FormButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89FBFC09F9DAFF1E4BA97849 /* FormButtonStyles.swift */; }; 192A3CDCD0174AD1E4A128E4 /* AudioRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */; }; 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; }; + 197441F1EF23A5DABACCA79F /* StickerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5338450E6783A576B5C16DD /* StickerRoomTimelineView.swift */; }; 19DED23340D0855B59693ED2 /* VoiceMessageRecorderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45C9EAA86423D7D3126DE4F /* VoiceMessageRecorderProtocol.swift */; }; 19DF5600A7F547B22DD7872A /* CompletionSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A12D3D8138F1B71AFA7C858 /* CompletionSuggestionService.swift */; }; 19FE025AE9BA2959B6589B0D /* RoomMemberDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */; }; @@ -137,8 +140,6 @@ 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */; }; 1DC227816777A2F3A19657E5 /* RoomDirectorySearchScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCF71646898A2F720C5BFDF5 /* RoomDirectorySearchScreenViewModel.swift */; }; 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; }; - 1EC6D1B58B24369734CD62BA /* PollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317F41A4B5C4F457AF710666 /* PollView.swift */; }; - 1F04C63D4FA95948E3F52147 /* FileRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */; }; 1F3232BD368DF430AB433907 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = 07FEEEDB11543A7DED420F04 /* Compound */; }; 1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; }; 1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */; }; @@ -181,19 +182,21 @@ 292827744227DF61C930BDDB /* CreateRoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB0D6CB491777E7FC6B5BA12 /* CreateRoomScreen.swift */; }; 29491EE7AE37E239E839C5A3 /* LocationSharingScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */; }; 2955F4C160CFD7794D819C64 /* EffectsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024F7398C5FC12586FB10E9D /* EffectsScene.swift */; }; + 298F9EC30E918F12AB7F1EE8 /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81F0325E252B057FAEEE1B2D /* TypingIndicatorView.swift */; }; 29EE1791E0AFA1ABB7F23D2F /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; 2A864BB12A8501B47805D828 /* AuthenticationFlowCoordinatorUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295E28C3B9EAADF519BF2F44 /* AuthenticationFlowCoordinatorUITests.swift */; }; - 2A90DD14DE5C891BFA433950 /* TimelineReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */; }; 2AAB2A77F1762A2648078A30 /* InteractiveQuickLook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */; }; 2ABF11717C64054CEF2819A3 /* RoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F85164F9475FF2867F71AAA /* RoomTimelineController.swift */; }; - 2B1E080B32167AE9EFC763A2 /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B8315A340B46F98B9C5AF0 /* TimelineTableViewController.swift */; }; + 2AED12987603157C32C2114D /* TimelineBubbleLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D8FEB1FED10E995CB002F7 /* TimelineBubbleLayout.swift */; }; 2B97BCE72D86645F1485C976 /* RoomDirectorySearchMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 894EE8F5B399A165BA2A6634 /* RoomDirectorySearchMock.swift */; }; 2BA59D0AEFB4B82A2EC2A326 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 50009897F60FAE7D63EF5E5B /* Kingfisher */; }; 2BAA5B222856068158D0B3C6 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */; }; 2BBA132149DEBED6624084A8 /* MessageForwardingScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE373A7F20780BA84B59C /* MessageForwardingScreenCoordinator.swift */; }; + 2BBC0EB1E07963810A5D7423 /* ReadMarkerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012A284622B32052015F1F89 /* ReadMarkerRoomTimelineView.swift */; }; 2BBE320EE426A347AAE5C7DA /* IdentityConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00AFC5F08734C2EA4EE79C59 /* IdentityConfirmationScreen.swift */; }; 2C4C750D0039AFABDF24236C /* TemplateScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342BEBC3C5FC3F9943C41C4C /* TemplateScreenViewModelProtocol.swift */; }; 2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */; }; + 2CA61BB208CD82EBDB58CD13 /* VideoRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0CBEAB5F796BEFBAF7BB6A /* VideoRoomTimelineView.swift */; }; 2CA6ABBC9A88EB89EA52FCCB /* ConfettiScene.scn in Resources */ = {isa = PBXBuildFile; fileRef = B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */; }; 2D2D8A53B35BE8D8A01449C6 /* PinnedEventsBannerStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */; }; 2DA27D78560D5F79B917E163 /* AudioConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */; }; @@ -201,6 +204,7 @@ 2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; }; 2E8C6672D0EE7D5B1BEDB8E2 /* ServerConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7478623CECC9438014244BA /* ServerConfirmationScreen.swift */; }; 2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086B997409328F091EBA43CE /* RoomScreenUITests.swift */; }; + 2F6207CB5C4715FE313B1E95 /* TimelineViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6509708F54FC883604DFDC95 /* TimelineViewModelTests.swift */; }; 2F623DA1122140A987B34D08 /* NotificationSettingsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */; }; 2F66701B15657A87B4AC3A0A /* WaitlistScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09CE2B7AD979BDEE09FEDB08 /* WaitlistScreenModels.swift */; }; 2F94054F50E312AF30BE07F3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; }; @@ -229,7 +233,6 @@ 3582056513A384F110EC8274 /* MediaPlayerProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */; }; 35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; }; 366D5BFE52CB79E804C7D095 /* CallScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */; }; - 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; }; 36926D795D6D19177C7812F8 /* EncryptionResetPasswordScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6935A55AB3B0C94BC566DD6 /* EncryptionResetPasswordScreenCoordinator.swift */; }; 369BF960E52BBEE61F8A5BD1 /* BlockedUsersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */; }; 36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; }; @@ -256,11 +259,14 @@ 3A7DD0D13B0FB8876D69D829 /* TextBasedRoomTimelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */; }; 3AA9E878FDCFF85664AC071F /* ComposerDraftService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D70253004A5AEC9C73D6A4F /* ComposerDraftService.swift */; }; 3B0F9B57D25B07E66F15762A /* MediaUploadPreviewScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2E7C987AE5DC9087BB19F7D /* MediaUploadPreviewScreenModels.swift */; }; + 3B277D9538090766DA6C4566 /* StateRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2AF1828A5B76B7C371240FE /* StateRoomTimelineView.swift */; }; 3B28408450BCAED911283AA2 /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FA991289149D31F4286747 /* UserPreference.swift */; }; + 3B98049F56025726FB646ABD /* SwipeToReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0E0B55E2EE75AF67029924 /* SwipeToReplyView.swift */; }; 3C31E1A65EEB61E72E1113B4 /* AudioRecorderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBEC57C204D77908E355EF42 /* AudioRecorderProtocol.swift */; }; 3C549A0BF39F8A854D45D9FD /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; }; 3C73442084BF8A6939F0F80B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */; }; 3CE4C5071B6D2576E2473989 /* OrderedSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62B07B296D7A9D2F09120853 /* OrderedSet.swift */; }; + 3D72F5F9109AAA257542456B /* CallInviteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664ABD745A746C45CB842158 /* CallInviteRoomTimelineView.swift */; }; 3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DC38E64A5ED3FDB201029A /* BugReportService.swift */; }; 3DAD62988F072607441CB7A5 /* PollFormScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */; }; 3DAF325D8AE461F7CDB282BD /* StartChatScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */; }; @@ -285,8 +291,6 @@ 42A5A42ACF063EEE6B1980D2 /* ReportContentScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81B17B1F29448D1B9049B11C /* ReportContentScreenViewModel.swift */; }; 42B084FDE621FBEE433AF444 /* LegalInformationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4176C3E20C772DE8D182863C /* LegalInformationScreen.swift */; }; 42F1C8731166633E35A6D7E6 /* RoomEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0A307A44F952CD73E63AE31 /* RoomEventStringBuilder.swift */; }; - 4362C770C7E05ADC750E5070 /* LongPressWithFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DC6A9917A7123E7E9A3F81 /* LongPressWithFeedback.swift */; }; - 43EF6D8E694F54C5471BF5F3 /* TimelineBubbleLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DCA495ED42D2463DDAA94D /* TimelineBubbleLayout.swift */; }; 43F06DF42EC00B3CE2B020A4 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3F82523D6F48B926D6AF68 /* AppSettings.swift */; }; 43F35A7E5703D64DB0519C59 /* ServerSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD469F7513574341181F7EAA /* ServerSelectionScreen.swift */; }; 440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */; }; @@ -298,7 +302,6 @@ 454F8DDC4442C0DE54094902 /* LABiometryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F219838588C62198E726E3 /* LABiometryType.swift */; }; 4557192F5B15A8D9BB920232 /* AdvancedSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E492690C8B27A892C194CC4 /* AdvancedSettingsScreenCoordinator.swift */; }; 4610C57A4785FFF5E67F0C6D /* DSWaveformImageViews in Frameworks */ = {isa = PBXBuildFile; productRef = 2A4106A0A96DC4C273128AA5 /* DSWaveformImageViews */; }; - 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; }; 4681820102DAC8BA586357D4 /* VoiceMessageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB8D7926A5684E18196B538 /* VoiceMessageCache.swift */; }; 46A183C6125A669AEB005699 /* UserProfileScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F134D2D91DFF732FB75B2CB7 /* UserProfileScreenViewModelProtocol.swift */; }; 46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCE3636E3D01477C8B2E9D0 /* ReportContentScreenModels.swift */; }; @@ -306,9 +309,9 @@ 46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565F1B2B300597C616B37888 /* FullscreenDialog.swift */; }; 46C9F8FE3810A04A005FE16B /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B19B2BCC779ED934E0BBC2A /* AudioPlayer.swift */; }; 46FCD999E92D9717D24AAB94 /* QRCodeLoginScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDEDD4D2DE0646DA724985D5 /* QRCodeLoginScreenModels.swift */; }; + 4716587A9BA69ED8FD1B986B /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B19D10B102956066AF117B /* PollOptionView.swift */; }; 47305C0911C9E1AA774A4000 /* TemplateScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */; }; 4799A852132F1744E2825994 /* CreateRoomViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */; }; - 47FF70C051A991FB65CDBCF3 /* RoomScreenInteractionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0135A608FFAD86E6674EE730 /* RoomScreenInteractionHandler.swift */; }; 4807E8F51DB54F56B25E1C7E /* AppLockSetupSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C38663020DF2EB2D13F5E /* AppLockSetupSettingsScreenViewModel.swift */; }; 48416BBEB8DDF3E4DED0EDB6 /* ElementCallServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FC8B21E86B137BE4E91F82A /* ElementCallServiceProtocol.swift */; }; 484202C5D50983442D24D061 /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; }; @@ -318,8 +321,6 @@ 4940B439681767BE9D78CFDB /* AppLockSetupBiometricsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F5EE5DE3B55D59299DB5BC /* AppLockSetupBiometricsScreenViewModelTests.swift */; }; 4949C8C12669D1B5E082366E /* QRCodeLoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA9EA59D5C0DA1BFC7B3621 /* QRCodeLoginScreen.swift */; }; 49500BBA1CD65A5AE252D970 /* RoomDirectorySearchScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41BB37D96C3EA18F3CE8675D /* RoomDirectorySearchScreenModels.swift */; }; - 49814A48470F347426513B07 /* TimelineReadReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1877038D1AD3D5A029F8AE2C /* TimelineReadReceiptsView.swift */; }; - 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */; }; 4A4110369DBB79E4A314F415 /* ComposerToolbarViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0618820D26F9871A4BBB40E /* ComposerToolbarViewModelProtocol.swift */; }; 4A618590DEB72C4F186BFED4 /* UserSessionFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */; }; 4A8287E5281B44A8754BE509 /* SessionVerificationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */; }; @@ -335,8 +336,10 @@ 4C5A638DAA8AF64565BA4866 /* EncryptedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5351EBD7A0B9610548E4B7B2 /* EncryptedRoomTimelineItem.swift */; }; 4C8C0C9FC10BA73AB7780534 /* RoomListFiltersStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */; }; 4CE638FD837ED72CD98AD9A9 /* AppHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */; }; + 4D0F4385B7DDB68C66C78857 /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C258C9C815272911A5B132C3 /* FormattedBodyText.swift */; }; 4D23D41B8109E010304050F8 /* QRCodeLoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA551A98778CEE7366838CE2 /* QRCodeLoginScreenCoordinator.swift */; }; 4D4D236F0BBCDC4D2CBCCBB5 /* RoomChangePermissionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C729D95CB4588D4D9AAC3DFA /* RoomChangePermissionsScreenModels.swift */; }; + 4DAEE2468669848B6C9F55B4 /* TimelineReadReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33035418BB35754232985871 /* TimelineReadReceiptsView.swift */; }; 4E0D9E09B52CEC4C0E6211A8 /* MediaPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */; }; 4E36A66E0EDA74BF3A036FD0 /* RoomChangeRolesScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAD8C633AA57948B34EDCF7 /* RoomChangeRolesScreenViewModelProtocol.swift */; }; 4E8A2A2CFEB212F14E49E1A1 /* AppLockSetupSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */; }; @@ -347,6 +350,7 @@ 4FC085B1E5D1EB804495E2F4 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */; }; 4FC1EFE4968A259CBBACFAFB /* RoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */; }; 4FDC8A9764CFDA90CE035725 /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB2253D36E81E045E1CB432 /* Duration.swift */; }; + 4FE688FE9375B2FBF424146A /* TextBasedRoomTimelineViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6EA0D8B0BBD8805F7D5A133 /* TextBasedRoomTimelineViewProtocol.swift */; }; 4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */; }; 4FFDC274824F7CC0BBDF581E /* BugReportScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C2BCE0BC1FC69C1B36E688 /* BugReportScreenModels.swift */; }; 50381244BA280451771BE3ED /* PINTextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF13BFD415CA84B1272E94F8 /* PINTextFieldTests.swift */; }; @@ -358,10 +362,10 @@ 51B3B19FA5F91B455C807BA7 /* RoomPollsHistoryScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */; }; 523C6800ED85D5810CF18C19 /* OIDCAccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */; }; 52473A4D7B1FBD4CD1E770C8 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; + 5341D48F833E3E30F16FA2A3 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2910422CB628D3B2BBE47449 /* SeparatorRoomTimelineView.swift */; }; 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; 53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */; }; 53A59720F4729D9BBFFB7CAB /* NotificationSettingsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9CB3B9DFA353AB2B7CD9F8 /* NotificationSettingsEditScreenCoordinator.swift */; }; - 53A795964991B06A672B4AAD /* CallNotificationRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584509A363ADF6244DFDB96A /* CallNotificationRoomTimelineView.swift */; }; 53C1E7F6A7D6409D89F36ED7 /* AggregatedReactionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */; }; 53DEF39F0C4DE02E3FC56D91 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 800631D7250B7F93195035F1 /* KeychainAccess */; }; 53F1196F9C69512306A2693F /* TextRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C19F54A0C4FC9AB7ABD583 /* TextRoomTimelineItemContent.swift */; }; @@ -380,8 +384,6 @@ 5710AAB27D5D866292C1FE06 /* SessionVerificationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF848B41DAF1066F3054D4A1 /* SessionVerificationScreenModels.swift */; }; 5732395A4F71F51F9C754C5A /* ElementCallService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33AE897D86784CCA5E4E9227 /* ElementCallService.swift */; }; 5780E444F405AA1304E1C23E /* DeveloperOptionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E521D6C2BF8DF0DFB35146 /* DeveloperOptionsScreen.swift */; }; - 57B9562E6FE788FC172D4AAF /* TimelineItemSendInfoLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15447A39D91D2EF536C74DD /* TimelineItemSendInfoLabel.swift */; }; - 57E115A8C33E599DE564F8C3 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDEB27575FEBCF414D4DEE31 /* TimelineView.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 */; }; @@ -389,7 +391,6 @@ 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; }; 5AE6404C4FD4848ACCFF9EDC /* SecureBackupLogoutConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1573D28C8A9FB6399D0EEFB /* SecureBackupLogoutConfirmationScreenCoordinator.swift */; }; - 5B2D1210B40570D87B11BD3B /* ThreadDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CA3F8E905DF50BF22ECC18F /* ThreadDecorator.swift */; }; 5B6E5AD224509E6C0B520D6E /* RoomMemberDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDF49CEBC0DFC59C308335F /* RoomMemberDetailsScreenViewModelProtocol.swift */; }; 5B7D24A318AFF75AD611A026 /* RoomDirectorySearchScreenScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */; }; 5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */; }; @@ -397,14 +398,12 @@ 5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */; }; 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */; }; 5D27B6537591471A42C89027 /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */; }; - 5D2AF8C0DF872E7985F8FE54 /* TimelineDeliveryStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */; }; 5D52925FEB1B780C65B0529F /* PinnedEventsTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4F6D7000EDCD187E0989E7 /* PinnedEventsTimelineScreen.swift */; }; 5D53AE9342A4C06B704247ED /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; }; 5D56CE09743C6B90C21B04C2 /* RoomMembersListScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E9E0929CEFA356090BE5FB8 /* RoomMembersListScreenViewModelTests.swift */; }; 5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; }; 5DD0EF30070DC0A82C5CCD33 /* RoomMembersListManageMemberSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */; }; 5DD85A0FE3D85AEC3C7EFE36 /* DeveloperOptionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */; }; - 5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */; }; 5EE1D4E316D66943E97FDCF2 /* BloomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BEB970F500BFB248443FA1 /* BloomView.swift */; }; 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; 5F0B5797D1BFF2A51084B4C3 /* PinnedEventsTimelineScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86D7CD5CA270BFC3EBB450CA /* PinnedEventsTimelineScreenViewModel.swift */; }; @@ -419,7 +418,6 @@ 61A36B9BB2ADE36CEFF5E98C /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E93A1BE7D8A2EBCAD51EEB4 /* Array.swift */; }; 62418EA4E3EB597AD184AEB6 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; }; 627139A3D79F032BA81E3A53 /* UserSessionFlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA29BAE9B0F2D90E57B261C /* UserSessionFlowCoordinatorTests.swift */; }; - 62833C090D599023D92A0424 /* TimelineItemMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8D3544DE3FABD958BA8F19 /* TimelineItemMenu.swift */; }; 62910B515BCB4B455E24D7C1 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */; }; 6298AB0906DDD3525CD78C6B /* LoremSwiftum in Frameworks */ = {isa = PBXBuildFile; productRef = 1A6B622CCFDEFB92D9CF1CA5 /* LoremSwiftum */; }; 62A7FC3A0191BC7181AA432B /* AudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 907FA4DE17DEA1A3738EFB83 /* AudioRecorder.swift */; }; @@ -432,29 +430,30 @@ 64AB99285DC4437C0DDE9585 /* MenuSheetLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49ABAB186CF00B15C5521D04 /* MenuSheetLabelStyle.swift */; }; 64D05250CEDE8B604119F6E6 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981663D961C94270FA035FD0 /* Alert.swift */; }; 64E541F88F35BD126C4AFCA1 /* AppLockScreenPINKeypad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38345442415E07A931197C55 /* AppLockScreenPINKeypad.swift */; }; - 64F43D7390DA2A0AFD6BA911 /* VideoRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */; }; + 64EE9D2CF7AD02EE53983CE1 /* FileRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F75EF13F49DD2204E760910 /* FileRoomTimelineView.swift */; }; 651341E67C3514F9811A1EC1 /* LoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F598B1B346DAF223651C91 /* LoginScreenCoordinator.swift */; }; 652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1423AB065857FA546444DB15 /* NotificationManager.swift */; }; 6530865EB9A8C0F0AF0216DA /* ServerSelectionScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */; }; 654E802C127B84554042903E /* AnalyticsSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD5FCBA169B6A82F501CA1B /* AnalyticsSettingsScreenViewModelProtocol.swift */; }; + 6583A95947E78767736CB51A /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8112846C9D9D3817689CBAF8 /* TimelineTableViewController.swift */; }; 6586E1F1D5F0651D0638FFAF /* UserSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4469F6AE311BDC439B3A5EC /* UserSessionMock.swift */; }; 659E5B766F76FDEC1BF393A4 /* RoomDetailsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */; }; + 661EF50C1F7D4B0BC8A7AAE3 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44ABA63DBE7F76C58260B43B /* EmoteRoomTimelineView.swift */; }; 663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; }; 67160204A8D362BB7D4AD259 /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693E16574C6F7F9FA1015A8C /* Search.swift */; }; 6786C4B0936AC84D993B20BF /* NotificationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */; }; 67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; 67D6E0700A9C1E676F6231F8 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = AD544C0FA48DFFB080920061 /* Collections */; }; 67E9926C4572C54F59FCA91A /* AuthenticationFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B069D7772DDF6513E0F1B8 /* AuthenticationFlowCoordinator.swift */; }; + 67EFF46180B939CBF389AECD /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C713D124FE915ABF47A6B7 /* TimelineView.swift */; }; 6817EAD73DC1FFD8B943B5B9 /* HomeScreenRoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73587C2E3CF5998361AE516 /* HomeScreenRoomTests.swift */; }; 68184EF36396424FE19A727D /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; }; 6832733838C57A7D3FE8FEB5 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; }; 6851B077B4C913CC12DB6E77 /* AppLockFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */; }; 6860721DB3091BE08164C132 /* MapAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B48B7AD4908C5C374517B892 /* MapAssets.xcassets */; }; - 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; 695825D20A761C678809345D /* MessageForwardingScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52135BD9E0E7A091688F627A /* MessageForwardingScreenModels.swift */; }; 69A9B430397C15075D86193F /* UserPropertiesExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66AFD800AF033D8B0D11191A /* UserPropertiesExt.swift */; }; 69B3C6010B42010F591FC3CB /* RoomRolesAndPermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AF829F12FDC99717082D9 /* RoomRolesAndPermissionsScreenViewModel.swift */; }; - 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; 69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */; }; 69DE29C3E3180BB17D840690 /* ProgressCursorModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97C8E13A1FBA717B0C277ECC /* ProgressCursorModifier.swift */; }; 6A0E7551E0D1793245F34CDD /* ClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09A267106B9585D3D0CFC0D /* ClientError.swift */; }; @@ -462,18 +461,18 @@ 6AD722DD92E465E56D2885AB /* BugReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */; }; 6AEB650311F694A5702255C9 /* UserProfileScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B4932E4EFBC8FAC10972CD /* UserProfileScreenCoordinator.swift */; }; 6AECC84BE14A13440120FED8 /* NSESettingsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB4F169D653296023ED65E6 /* NSESettingsProtocol.swift */; }; - 6B05AA5D9BBCD6D8D63B80EB /* TimelineItemAccessibilityModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74C6F3DAD167F972702C8893 /* TimelineItemAccessibilityModifier.swift */; }; 6B31508C6334C617360C2EAB /* RoomMemberDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */; }; 6BAD956B909A6E29F6CC6E7C /* ButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC23C63849452BC86EA2852 /* ButtonStyle.swift */; }; 6BB6944443C421C722ED1E7D /* portrait_test_video.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = F2D513D2477B57F90E98EEC0 /* portrait_test_video.mp4 */; }; 6C34237AFB808E38FC8776B9 /* RoomStateEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */; }; + 6C98153D60FF9B648C166C27 /* TimelineItemMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91FFE1F410969ECB23FE9BB2 /* TimelineItemMenu.swift */; }; 6CD61FAF03E8986523C2ABB8 /* StartChatScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3005886F00029F058DB62BE /* StartChatScreenCoordinator.swift */; }; - 6D6E651ACACE27E9C5690818 /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE47A97726F0675DEE387BF9 /* TypingIndicatorView.swift */; }; 6DC8E43BA04AC2AC4EB2EB97 /* AnalyticsPromptScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18486B87745B1811E7FBD3D2 /* AnalyticsPromptScreenModels.swift */; }; 6E391F7F628D984AF44385D9 /* RoomAttachmentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 128501375217576AF0FE3E92 /* RoomAttachmentPicker.swift */; }; 6E47D126DD7585E8F8237CE7 /* LoadableAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */; }; 6E4E401BE97AC241DA7C7716 /* AppLockSetupSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502F986D57158674172C58E3 /* AppLockSetupSettingsScreenModels.swift */; }; 6E63704717F17593A475D152 /* RoomNotificationSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA14564EE143F73F7E4D1F79 /* RoomNotificationSettingsScreenModels.swift */; }; + 6EB46C92ECFEAE71959D91D2 /* TimelineReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */; }; 6EC7A40A537CFB3D526A111C /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; }; 6F26CBC84AE87EB4068D398B /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 78B28D75FF7AF8E6146DEE2A /* LRUCache */; }; 6F2AB43A1EFAD8A97AF41A15 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 9C73F37731C9FDED1BB24C1C /* Collections */; }; @@ -481,17 +480,17 @@ 6F86349BDEAF4495EAE38931 /* PHGPostHogMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2AD8A56CD37E23071A2F4BF /* PHGPostHogMock.swift */; }; 6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */; }; 6FD8053301C5FEFA82D2F246 /* URLComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BFDCA5A09EE70BC17F2EFA7 /* URLComponents.swift */; }; - 6FF51EB400DBA0668FC38B97 /* TimelineStartRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */; }; 70394ECD2DCC70741538620D /* AccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */; }; 70558528EF68CAAEF09972D5 /* RoomTimelineItemFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96ED747FF90332EA1333C22 /* RoomTimelineItemFixtures.swift */; }; 706289B086B0A6B0C211763F /* UITestsSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */; }; 707E49BE07E8EB8A13C0EB1E /* SessionVerificationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FACD034DB52525A3CEF2BDF /* SessionVerificationScreen.swift */; }; + 709A9B52FC26B4CB86B8B020 /* EncryptedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */; }; 70B83D44043293B4B77440B9 /* PollFormScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB63761D9F9CE8B23CBD6179 /* PollFormScreenModels.swift */; }; + 71643093F87153F633A1B025 /* ThreadDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA4F09CB613C54FDC73AE6A /* ThreadDecorator.swift */; }; 719E7AAD1F8E68F68F30FECD /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40C19719687984FD9478FBE /* Task.swift */; }; 71AC1CAAC23403FFE847F2C9 /* ComposerToolbarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90514BE9B8ACCBCF0AD2489 /* ComposerToolbarViewModel.swift */; }; 71B62C48B8079D49F3FBC845 /* ExpiringTaskRunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */; }; 71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; }; - 71C532CDC9995236FC1B6EE6 /* TimelineItemMenuActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2751266F17A5BF25DA9227E /* TimelineItemMenuActionProvider.swift */; }; 733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */; }; 7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; }; 7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */; }; @@ -509,7 +508,6 @@ 762DB0973865293F0C3D3D7B /* SessionVerificationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */; }; 763D69741D58D2B650BC1FC9 /* CallScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */; }; 7640A4B412CACF15D143CCD4 /* Strings+SAS.swift in Sources */ = {isa = PBXBuildFile; fileRef = B172057567E049007A5C4D92 /* Strings+SAS.swift */; }; - 764AFCC225B044CF5F9B41E5 /* PaginationIndicatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */; }; 767D366C40F1311CFA333763 /* PillContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86376BEE425704AEE197CA54 /* PillContext.swift */; }; 7691233E3572A9173FD96CB3 /* SecureBackupKeyBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */; }; 7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; }; @@ -523,21 +521,26 @@ 77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */; }; 7807B1DEE32617896886A8E5 /* FormattingToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1E6FAA3719E9B7A2D5510B /* FormattingToolbar.swift */; }; 784592335560C2E91D32D177 /* DeveloperOptionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06B098A612DCB5A7358EECD5 /* DeveloperOptionsScreenModels.swift */; }; + 785613C0C092B532198EB3BB /* TimelineStartRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44ECC9D66400727DFFEE12E8 /* TimelineStartRoomTimelineView.swift */; }; 7856DE3EA4580AE0329986EB /* ComposerDraftServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */; }; + 78A3D84BA47DAC69B4D0A34C /* CollapsibleRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBF273BC2BFB9F3EEFA988B /* CollapsibleRoomTimelineView.swift */; }; 795A854F63301DC6B46217B9 /* AccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */; }; 79741C1953269FF1A211D246 /* RoomPollsHistoryScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E14FF533D25A0692F7CEB0 /* RoomPollsHistoryScreenViewModel.swift */; }; + 798BF3072137833FBD3F4C96 /* TimelineDeliveryStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */; }; + 79959F8E45C3749997482A7F /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */; }; 7A02EB29F3B993AB20E0A198 /* RoomPollsHistoryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */; }; + 7A0D335D38ECA095A575B4F7 /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB0E533508094156D8024C3 /* TimelineStyler.swift */; }; 7A170A5A4A352954BB2A1B96 /* AuthenticationStartScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24E8C8817F59BEC7E358EB78 /* AuthenticationStartScreen.swift */; }; 7A25D6926A2C01DB8D0D67A5 /* BadgeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */; }; 7A642EE5F1ADC5D520F21924 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */; }; 7A71AEF419904209BB8C2833 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */; }; 7A8B264506D3DDABC01B4EEB /* AppMediator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53AC78E49A297AC1D72A7CF /* AppMediator.swift */; }; - 7AE82514D96C725F8BDD0ED4 /* HighlightedTimelineItemModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D162B2280A15ACAF35360554 /* HighlightedTimelineItemModifier.swift */; }; 7B1605C6FFD4D195F264A684 /* RoomPollsHistoryScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40233F2989AD49906BB310D /* RoomPollsHistoryScreenViewModelTests.swift */; }; 7B3A59786DB2F741A1743ED0 /* PinnedEventsTimelineScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510E89B989477E5EE8E503C0 /* PinnedEventsTimelineScreenViewModelProtocol.swift */; }; 7B5DAB915357BE596529BF25 /* MapTilerStaticMapProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20872C3887F835958CE2F1D0 /* MapTilerStaticMapProtocol.swift */; }; 7B66DA4E7E5FE4D1A0FCEAA4 /* JoinRoomScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAB5662310AE73D93815134 /* JoinRoomScreenViewModelProtocol.swift */; }; 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */; }; + 7BD2123144A32F082CECC108 /* AudioRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EAFFD44F81F86012D6EC27 /* AudioRoomTimelineView.swift */; }; 7BF368A78E6D9AFD222F25AF /* SecureBackupScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE094FCB6387D268C436161 /* SecureBackupScreenViewModel.swift */; }; 7C0E29E0279866C62EC67A28 /* JoinRoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5127D6EA05B2E45D0A7D59 /* JoinRoomScreenViewModelTests.swift */; }; 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */; }; @@ -552,6 +555,7 @@ 7ECF12D5DCD69F67BD3E3842 /* RoomTimelineControllerFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */; }; 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */; }; 7F7EA51A9A43125A8CB6AC90 /* NotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D560DDA3B20C82766ACFAD /* NotificationSettingsScreenViewModel.swift */; }; + 7F825CBD857D65DC986087BA /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F54FA7C5CB7B342EF9B9B2F /* NoticeRoomTimelineView.swift */; }; 7F941B063C94E1718DFC2CF3 /* RoomChangeRolesScreenRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23E6EB7960BC9D0F7396B3BD /* RoomChangeRolesScreenRow.swift */; }; 7FED77802940EA7DF4D0D3A2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */; }; 7FF6E1FBE6E9517FD29A1D8E /* RoomChangeRolesScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48A5C34C4E4268EF65D171EF /* RoomChangeRolesScreenModels.swift */; }; @@ -561,12 +565,12 @@ 81A7C020CB5F6232242A8414 /* UserSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F36C0A6D59717193F49EA986 /* UserSessionTests.swift */; }; 8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.swift */; }; 828EA5009557C2B9DCD4CA0F /* UserDiscoverySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */; }; - 8317E1314C00DCCC99D30DA8 /* TextBasedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9227F7495DA43324050A863 /* TextBasedRoomTimelineItem.swift */; }; 832A4EA1094B8FE423A08700 /* RoomChangeRolesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B2A421198FD20AAAED20004 /* RoomChangeRolesScreen.swift */; }; 8358D145F9BF94F412BEDCA8 /* RoomRolesAndPermissionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */; }; 83A4DAB181C56987C3E804FF /* MapTilerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */; }; 83B17A44D3E7E6DF22D9A2A4 /* RoomModerationRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */; }; 84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; }; + 8446C2A7ECEFDA79F622725F /* TimelineReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AD70D6E03D2031AE1B5A52 /* TimelineReactionsView.swift */; }; 8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */; }; 847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1D4A6D451F43A03CACD01D /* PINTextField.swift */; }; 84C631E734FD2555B39B681C /* RoomRolesAndPermissionsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48FEFF746DB341CDB18D7AAA /* RoomRolesAndPermissionsScreenViewModelTests.swift */; }; @@ -594,6 +598,7 @@ 87CEDB8A0696F0D5AE2ABB28 /* test_audio.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = D5E26C54362206BBDD096D83 /* test_audio.mp3 */; }; 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */; }; 88356DE7F2AD243AB10C7B7A /* Signposter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */; }; + 887AC93C523AEFB640EA5EC8 /* TextBasedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E33FD32BBC44D703C7AE4F9 /* TextBasedRoomTimelineItem.swift */; }; 88CBF1595E39CE697928DE48 /* SFNumberedListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */; }; 88F348E2CB14FF71CBBB665D /* AudioRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7475C5AE20BA896930907EA8 /* AudioRoomTimelineItemContent.swift */; }; 890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C687844F60BFF532D49A994C /* AnalyticsTests.swift */; }; @@ -602,6 +607,7 @@ 899359A4D1147601F6C4E364 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; }; 899793EFC63DF93C3E0141E7 /* RoomMemberDetailsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FA60F848D1C14F873F9621A /* RoomMemberDetailsScreenCoordinator.swift */; }; 8A0BD60CA4A6004DB06B5403 /* MediaUploadingPreprocessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669F35C505ACE1110589F875 /* MediaUploadingPreprocessor.swift */; }; + 8A5064CAC8E5F3B18645621D /* CallNotificationRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6E082B0507FB28F966516A /* CallNotificationRoomTimelineView.swift */; }; 8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */; }; 8AA84EF202F2EFC8453A97BD /* SecureBackupRecoveryKeyScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */; }; 8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */; }; @@ -616,6 +622,7 @@ 8C706DA7EAC0974CA2F8F1CD /* MentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15748C254911E3654C93B0ED /* MentionBuilder.swift */; }; 8C91D242BEEC657FABCC0B95 /* BlockedUsersScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8642512079EEFD622E3AA66B /* BlockedUsersScreenModels.swift */; }; 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; }; + 8CFDA5F1562479CB3A34D277 /* RedactedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C8BD78F7B9247AC57FA1A3 /* RedactedRoomTimelineView.swift */; }; 8D3E1FADD78E72504DE0E402 /* UserAgentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */; }; 8D71E5E53F372202379BECCE /* BugReportScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303FCADE77DF1F3670C086ED /* BugReportScreenViewModel.swift */; }; 8DC176CC5ABA24138EB443DD /* RoomMemberDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55679AF67545EF8087E47BE /* RoomMemberDetails.swift */; }; @@ -624,11 +631,9 @@ 8E650379587C31D7912ED67B /* UNNotification+Creator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */; }; 8E7A902CA16E24928F83646C /* ElementCallServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */; }; 8ED8AF57A06F5EE9978ED23F /* AuthenticationStartScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB89DC7F9A4A91020037001 /* AuthenticationStartScreenViewModelTests.swift */; }; - 8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */; }; 8F2FAA98457750D9D664136F /* Mapbox in Frameworks */ = {isa = PBXBuildFile; productRef = C1BF15833233CD3BDB7E2B1D /* Mapbox */; }; 904F06C9C1AEF884C2077542 /* RoomDirectorySearchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2E4EF80DFB8FE7C4469B15D /* RoomDirectorySearchScreen.swift */; }; 90733645AE76FB33DAD28C2B /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE40D4A5DD857AC16EED945A /* URLSession.swift */; }; - 9095B9E40DB5CF8BA26CE0D8 /* ReactionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 153726EDCE1ACBB3D466A916 /* ReactionsSummaryView.swift */; }; 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; }; 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; 915B4CDAF220D9AEB4047D45 /* PollInteractionHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */; }; @@ -659,6 +664,7 @@ 9696ECAFB4F0C079C5C2A526 /* AppLockSetupPINScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAF8C2226A57B9AB7446B31 /* AppLockSetupPINScreenCoordinator.swift */; }; 96B3606E30F824095B1DD022 /* NetworkMonitorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */; }; 97189E495F0E47805D1868DB /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 527578916BD388A09F5A8036 /* DTCoreText */; }; + 973C48F9E4EFB808F61BE401 /* LocationRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */; }; 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; }; 97969EF0B9C412CD38E5CA93 /* AppLockScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4005D82E9D27BAF006A8FE1 /* AppLockScreenViewModel.swift */; }; 97BAEDD9054FB5F233EE928B /* EncryptionResetScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 306AB507E1027D6C5C147EB6 /* EncryptionResetScreenModels.swift */; }; @@ -666,6 +672,7 @@ 983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */; }; 9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */; }; 988BA75A182738150894A23F /* UserIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8AE4B3273BA189FDCD4055C /* UserIndicator.swift */; }; + 98EE4259A4A49BC757BA442C /* TimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B2ACA28A854E41AE3AC9AD /* TimelineViewModel.swift */; }; 9905C1B1C6EFE38F3A6533F3 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33B3F17996DFDF5F0181512 /* Data.swift */; }; 9912F9EB2D6589141A2957B4 /* AppLockScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C44BBC892499BE45B074F89 /* AppLockScreenCoordinator.swift */; }; 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; @@ -676,7 +683,6 @@ 9AFEE46B03B7E995B3E1A53D /* WaitlistScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80C4927D09099497233E9980 /* WaitlistScreen.swift */; }; 9B03943616A1147539DF7F08 /* RoomChangePermissionsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41D041A857614A9AE13C7795 /* RoomChangePermissionsScreenViewModelTests.swift */; }; 9B356742E035D90A8BB5CABE /* ProposedViewSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */; }; - 9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */; }; 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 */; }; @@ -693,13 +699,12 @@ 9F11B9F347F9E2D236799FB3 /* ElementCallServiceConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */; }; 9F11E743EA01482E78A438B0 /* GlobalSearchScreenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22DB19219E6CC4D002E15D48 /* GlobalSearchScreenCell.swift */; }; 9F19096BFA629C0AC282B1E4 /* CreateRoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CEB4634C0DD7779C4AB504 /* CreateRoomScreenUITests.swift */; }; - 9FAF6DA7E8E85C9699757764 /* CollapsibleRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2656184491C505700D2405 /* CollapsibleRoomTimelineView.swift */; }; 9FB41B0E8B2AA9B404E52C8B /* AppLockSetupBiometricsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */; }; + 9FBE1FB20171012260A32492 /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D53FCCE44F96E0BC411A6CF0 /* TimelineSenderAvatarView.swift */; }; A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */; }; A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; }; A0601810597769B81C2358AF /* EncryptionResetPasswordScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */; }; A0868BDE84D2140A885BE3C9 /* EncryptionResetScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8562F4D7DE073BC32902AB /* EncryptionResetScreenViewModelProtocol.swift */; }; - A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; }; A0D7E5BD0298A97DCBDCE40B /* Emojibase in Frameworks */ = {isa = PBXBuildFile; productRef = C05729B1684C331F5FFE9232 /* Emojibase */; }; A10D6CCDE2010C09EEA1A593 /* HomeScreenRoomList.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */; }; A14A9419105A1CD42F0511C4 /* UserIndicatorModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */; }; @@ -710,7 +715,6 @@ A2172B5A26976F9174228B8A /* AppHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E4AB573FAEBB7B853DD04C /* AppHooks.swift */; }; A23B8B27A1436A1049EEF68E /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; A2434D4DFB49A68E5CD0F53C /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; }; - A2A5AB2E8B3F5CA769E531FA /* TextBasedRoomTimelineViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E47F18A9A077E351CEA10D4 /* TextBasedRoomTimelineViewProtocol.swift */; }; A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; }; A36AD251013402EDBD666C75 /* AppMediatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAC027034248429A438886B /* AppMediatorMock.swift */; }; A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 287FC98AF2664EAD79C0D902 /* UIDevice.swift */; }; @@ -728,7 +732,6 @@ A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; }; A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; }; A64B52D9F73F9A6B95AF24FE /* UserDetailsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CD503F5E0938FE53C7C6E7 /* UserDetailsEditScreenCoordinator.swift */; }; - A680F54935A6ADEA4ED6C38F /* TimelineItemStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4C9547BBFEEF30AA11329B /* TimelineItemStatusView.swift */; }; A6B83EB78F025D21B6EBA90C /* CompoundIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044E501B8331B339874D1B96 /* CompoundIcon.swift */; }; A6D4C5EEA85A6A0ABA1559D6 /* RoomDetailsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */; }; A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; }; @@ -736,6 +739,7 @@ A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */; }; A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; + A808DC3F72D15C6C5A52317E /* TimelineItemDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDA016D05107DED3B9495CB /* TimelineItemDebugView.swift */; }; A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; }; A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; }; A896998A6784DB6F16E912F4 /* MockMediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */; }; @@ -751,13 +755,14 @@ AA93B3F9B5DD097DEF79F981 /* NotificationSettingsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */; }; AADE7C2497A7B55D8BED7BD6 /* IdentityConfirmedScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8319173DD66C07F45DC48848 /* IdentityConfirmedScreenViewModelProtocol.swift */; }; AAF0BBED840DF4A53EE85E77 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C2C69B8BA5A9702E7A8BC08F /* MatrixRustSDK */; }; - ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; }; + ABD29E06DD1224812E750AF8 /* ReadReceiptCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D75941CBD7D336F831924EC /* ReadReceiptCell.swift */; }; AC1DB27A4134470846BE49F6 /* UserProfileScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BD116096CAA9139B95EEA9C /* UserProfileScreenViewModel.swift */; }; AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */; }; AC7AA215D60FBC307F984028 /* Consumable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127A57D053CE8C87B5EFB089 /* Consumable.swift */; }; AC90434798E7894370E80E66 /* SecureBackupScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79BB714D28C9F588DD69353 /* SecureBackupScreenViewModelProtocol.swift */; }; AD2A81B65A9F6163012086F1 /* MXLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111B698739E3410E2CDB7144 /* MXLog.swift */; }; AD55E245FE686D7DB4C86406 /* RoomTimelineItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90F2F8998E5632668B0AD848 /* RoomTimelineItemView.swift */; }; + AE07F215EBC2B9CBF17AA54B /* TimelineItemMenuAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F1C3CBBC62C566DDF5E84C1 /* TimelineItemMenuAction.swift */; }; AE1160076F663BF14E0E893A /* EffectsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4548A9BDE5CB3AB864BCA9F /* EffectsView.swift */; }; AE1A73B24D63DA3D63DC4EE3 /* SessionVerificationControllerProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */; }; AE5AAD9E32511544FDFA5560 /* WindowManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06F27F588F9059128E17C669 /* WindowManagerProtocol.swift */; }; @@ -767,9 +772,9 @@ AF8BFA37791E1756EE243E08 /* SettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */; }; AFE2AB612A1460E49578D746 /* JoinRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDCCD2F6B405C14B9BCE94E /* JoinRoomScreenCoordinator.swift */; }; B04E9EB589CE99C3929E817A /* HomeScreenRecoveryKeyConfirmationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */; }; - B0BA59A46ACCF0A3ECBBB7E0 /* TimelineItemMacContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31352FFC2EF2C353CB7EA376 /* TimelineItemMacContextMenu.swift */; }; B0CB16349B96262AA65A04AF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; B1069F361E604D5436AE9FFD /* StaticLocationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B06663F7858E45882E63471 /* StaticLocationScreen.swift */; }; + B10F7D5C237417DA160F4603 /* LongPressWithFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9F148717D74F73BE724434 /* LongPressWithFeedback.swift */; }; B13774779EA19FDD7A35A4A8 /* RoomRolesAndPermissionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C28B70BEFD3676F11D5D51F /* RoomRolesAndPermissionsScreenCoordinator.swift */; }; B1387648C6F71F1B98244803 /* SecureBackupRecoveryKeyScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 596AA8843AC1A234F3387767 /* SecureBackupRecoveryKeyScreenCoordinator.swift */; }; B14BC354E56616B6B7D9A3D7 /* NotificationServiceExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */; }; @@ -790,7 +795,6 @@ B5479997ECC516C121E6625E /* LocationMarkerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFECCE59967018204876D0A5 /* LocationMarkerView.swift */; }; B5618E3C948584E5C1F67033 /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; }; B5899F18AD6C56CE08FE532B /* RoomSummaryProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */; }; - B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */; }; B5E455C9689EA600EDB3E9E0 /* NavigationRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */; }; B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; B6064D82FCDCB829601C1F59 /* SecureBackupLogoutConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEE10AB666891E6A675E5E /* SecureBackupLogoutConfirmationScreen.swift */; }; @@ -805,6 +809,7 @@ B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */; }; B79E8AB83EBBDCD476D0362F /* PollFormScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D622EC7898469BB1D0881CDD /* PollFormScreen.swift */; }; B7C9E07F4F9CCC8DD7156A20 /* CallScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28146817C61423CACCF942F5 /* CallScreenModels.swift */; }; + B818580464CFB5400A3EF6AE /* TimelineModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029D5701F80A9AF7167BB4D0 /* TimelineModels.swift */; }; B828C600A54B2EE20871A451 /* PerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD700E035C85738EE4B97129 /* PerformanceTests.swift */; }; B879446FD8E65A711EF8F9F7 /* AdvancedSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */; }; B89990DD875B0B603D4D4332 /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */; }; @@ -822,6 +827,7 @@ BB6BF528BC7F5B87E08C4F18 /* CameraPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */; }; BB784A02BADB03C820617A46 /* TextRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */; }; BB9B800C6094E34860E89DC5 /* AppLockSetupBiometricsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CCF9A924521DECA44778C4 /* AppLockSetupBiometricsScreen.swift */; }; + BC7CA1379D7C24F47B1B8B7E /* PaginationIndicatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7F7A975514E850A834B29F /* PaginationIndicatorRoomTimelineView.swift */; }; BCC864190651B3A3CF51E4DF /* MediaFileHandleProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */; }; BD0BE20DBCE31253AE4490A1 /* RoomListFiltersEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC1DDB2293A51EA4C2739351 /* RoomListFiltersEmptyStateView.swift */; }; BD11E639CF566A9DA8FCA717 /* RoundedLabelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7C80EF77AD102053D3646E /* RoundedLabelItem.swift */; }; @@ -836,6 +842,7 @@ C0090506A52A1991BAF4BA68 /* NotificationSettingsChatType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */; }; C051475DFF4C8EBDDF4DC8E4 /* StartChatScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99E13633862847D8B7E2815 /* StartChatScreenModels.swift */; }; C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */; }; + C0B97FFEC0083F3A36609E61 /* TimelineItemMacContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = A243A6E6207297123E60DE48 /* TimelineItemMacContextMenu.swift */; }; C11939FDC40716C4387275A4 /* NotificationSettingsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8544F7058D31DBEB8DBFF0F5 /* NotificationSettingsEditScreenViewModelTests.swift */; }; C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */; }; C1429699A6A5BB09A25775C1 /* AudioPlayerStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */; }; @@ -873,6 +880,7 @@ C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */; }; C915347779B3C7FDD073A87A /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */; }; C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E0B4A34E69BD2132BEC521 /* MessageText.swift */; }; + C9A631FD968249B4BA0B7B3C /* ReactionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EE0FABA8ED6D6C1D6CE71D /* ReactionsSummaryView.swift */; }; C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81B6170DB690013CEB646F4 /* MapLibreModels.swift */; }; C9F5B48D15B9BCAE1F8D564E /* RoomNotificationModeProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */; }; CA12AE0DCD57D49CD96C699A /* WaveformCursorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB9EABCA9348DFA27439A809 /* WaveformCursorView.swift */; }; @@ -911,7 +919,6 @@ D12F440F7973F1489F61389D /* NotificationSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F64447FF544298A6A3BEF85 /* NotificationSettingsScreenModels.swift */; }; D181AC8FF236B7F91C0A8C28 /* MapTiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AA3F4B285570805CB0CCDD /* MapTiler.swift */; }; D19A748E95E2FAB2940570F0 /* CallScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4103AB4340F2974D690A12A /* CallScreen.swift */; }; - D1E29F345F1220E1AF1BE9DF /* ReadReceiptsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0A77874B29D79DDFC051AC /* ReadReceiptsSummaryView.swift */; }; D1EEF0CB0F5D9C15E224E670 /* landscape_test_video.mov in Resources */ = {isa = PBXBuildFile; fileRef = 9A2AC7BE17C05CF7D2A22338 /* landscape_test_video.mov */; }; D2048FD56760BDABA3DB5FC2 /* AppLockServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */; }; D22345698F6548C1EE960940 /* IdentityConfirmedScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBE70FFB7936F35811772C1 /* IdentityConfirmedScreenModels.swift */; }; @@ -934,15 +941,15 @@ D5B1531A72387D432939D4E0 /* RoomDirectorySearchProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */; }; D5C805F49B2C75DC3793E780 /* EmojiItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */; }; D5E771132BB36240DE38102F /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */; }; - D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; }; D5FE90A6AF5FD5AE91BD37C7 /* NotificationSettingsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */; }; D6152E21036B88C44ECB22E7 /* EncryptionResetPasswordScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303D9438EFB481F57A366E82 /* EncryptionResetPasswordScreenViewModel.swift */; }; D63974A88CF2BC721F109C77 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = DCA3C4A997AD28E6918D4CE5 /* Compound */; }; D6661A94DBD97658B2ADBD6A /* MapTilerStaticMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A4D29F2683F5772AC72406F /* MapTilerStaticMap.swift */; }; D6DE764B17FB4A9A12C33BF4 /* MessageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1DF3FFFE5ED2B8133F43A7 /* MessageComposer.swift */; }; D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; }; + D8459AAD6969B1431ECBE990 /* UnsupportedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9E535B3388755B65C34CD10 /* UnsupportedRoomTimelineView.swift */; }; D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; - D9473FC9B077A6EDB7A12001 /* LocationRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */; }; + D8CFA0EE46376F9FF04EEE45 /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4853C923A1AF43711D025EAF /* TextRoomTimelineView.swift */; }; D98B5EE8C4F5A2CE84687AE8 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352359663A0E52BA20761EE /* LoadableImage.swift */; }; DA7E867F5EAFF8E20B2EE3B6 /* SecureBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3D16709ADD4F4BCC710B1E /* SecureBackupScreenModels.swift */; }; @@ -971,10 +978,10 @@ E14E469CD97550D0FC58F3CA /* CancellableTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */; }; E184FFAD32342D3D6E2F89AA /* PinnedEventsTimelineScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D53754227CEBD06358956D7 /* PinnedEventsTimelineScreenCoordinator.swift */; }; E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; - E1F446C6B78A3A0FEA15079C /* UnsupportedRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */; }; E21FE4C5B614F311C0955859 /* UserProfileProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C454AE59914B551A6D02C0 /* UserProfileProxy.swift */; }; E27C4D1A1F8BB77CA790B403 /* InviteUsersScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */; }; E290C78E7F09F47FD2662986 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40C19719687984FD9478FBE /* Task.swift */; }; + E2D57361B835E4D2230960E6 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5599255A6C98EBDA77B76E6 /* ImageRoomTimelineView.swift */; }; E2DB696117BAEABAD5718023 /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */; }; E2DDA49BD62F03F180A42E30 /* MapLibreStaticMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 592A35163B0749C66BFD6186 /* MapLibreStaticMapView.swift */; }; E313BDD2B8813144139B2E00 /* UserDiscoveryServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0287793F11C480E242B03DF5 /* UserDiscoveryServiceTest.swift */; }; @@ -994,8 +1001,8 @@ E58F1F3276E98A93F7D39219 /* RoomPollsHistoryScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D8479BB704B7EF696F8ABE /* RoomPollsHistoryScreenCoordinator.swift */; }; E5AB28123E2488F97E953AC0 /* CallNotificationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED17433ADC77287F8904F9 /* CallNotificationRoomTimelineItem.swift */; }; E5F4C992845388B50BABACAA /* ServerSelectionScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */; }; - E62EC30B39354A391E32A126 /* AudioRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC2D505742FDA21FCDC4C18A /* AudioRoomTimelineView.swift */; }; E67418DACEDBC29E988E6ACD /* message.caf in Resources */ = {isa = PBXBuildFile; fileRef = ED482057AE39D5C6D9C5F3D8 /* message.caf */; }; + E6FA87F773424B27614B23E9 /* TimelineItemAccessibilityModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA7E93C2E148B96EF6A8500 /* TimelineItemAccessibilityModifier.swift */; }; E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */; }; E77469C5CD7F7F58C0AC9752 /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 3FFDA99C98BE05F43A92343B /* test_pdf.pdf */; }; E77FE06B165A38BF1735509F /* SecureBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF73F49E6B6683F7E2D26F0 /* SecureBackupScreenCoordinator.swift */; }; @@ -1030,7 +1037,6 @@ EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */; }; EE56238683BC3ECA9BA00684 /* GlobalSearchScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4D639E27D5882A6A71AECF /* GlobalSearchScreenViewModelTests.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; - EE8A37E2A1A77DE5CF941632 /* StateRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */; }; EEAE954289DE813A61656AE0 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; }; EEB9C1555C63B93CA9C372C2 /* EmojiPickerScreenHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5E29E9A22F45534FBD5B58 /* EmojiPickerScreenHeaderView.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; @@ -1038,6 +1044,7 @@ EF47D802A404A53F15D5D4B6 /* JoinRoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CD7C0A2750998C2D77AD00F /* JoinRoomScreenViewModel.swift */; }; EF5009AC03212227131C8AF2 /* RoomNotificationSettingsProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */; }; EF890DEF0479E66548F2BA23 /* AppLockTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490BEADEFB2D6B7C9F618AE8 /* AppLockTimer.swift */; }; + EFBBD44C0A16F017C32D2099 /* TimelineItemStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72614BFF35B8394C6E13F55A /* TimelineItemStatusView.swift */; }; F0570F1ECD70C4C851FB2052 /* SecureBackupRecoveryKeyScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E7304F5ECB4CB11CB10E60 /* SecureBackupRecoveryKeyScreenViewModelProtocol.swift */; }; F06CE9132855E81EBB6DDC32 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 940C605265DD82DA0C655E23 /* Kingfisher */; }; F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */; }; @@ -1049,19 +1056,17 @@ F12F6BED7B6D7EE4BEE55039 /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; }; F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57916A1578D8043BB0795441 /* GeneratedMocks.swift */; }; F253AAB4C8F06208173C9C4A /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; - F28BE69B7D35C8C97BAE1F2D /* ReadReceiptCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15F30E7AE8A303E8FEC2499E /* ReadReceiptCell.swift */; }; - F32B271F60531BE92C6E62A1 /* StickerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612EF972F2A1800682D32C5E /* StickerRoomTimelineView.swift */; }; + F2D5C0E1351DA7BD16867629 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */; }; F37629BAA5E8F50AAF2A131D /* SoftLogoutScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7BAD55A4E2B8E5828CD64C /* SoftLogoutScreenViewModel.swift */; }; F3E2D3F7ACDED65A4E5CD8DE /* RoomMembersListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */; }; + F3ECA377FF77E81A4F1FA062 /* TimelineItemSendInfoLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 753B4C6C0EDDCBF0708DC384 /* TimelineItemSendInfoLabel.swift */; }; F3F38062C6CA21CF403C5C90 /* AudioConverterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2757B1BE23DF8AA239937243 /* AudioConverterProtocol.swift */; }; F3F9D61C53C348043D3D6F51 /* EncryptionResetScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 811E8BF34E931D51552C9C13 /* EncryptionResetScreen.swift */; }; F40B097470D3110DFDB1FAAA /* LegalInformationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */; }; F4971845B5C4F270F6BC5745 /* ScaledFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D82F234B3576BD6268C7950 /* ScaledFrameModifier.swift */; }; F4996C82A4B3A5FF0C8EDD03 /* RoomListFilterModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E06AAD6D9D3F5833E7A5A2F9 /* RoomListFilterModels.swift */; }; F4C005F006FC3657B9F0A31D /* BugReportHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25586C0ADB814FEE9897DCAA /* BugReportHook.swift */; }; - F50A6FCE26714E27FE5495DD /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F23B21CF15F9F4BAA0788B /* PollOptionView.swift */; }; F519DE17A3A0F760307B2E6D /* InviteUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D155E09BF961BBA8F85263 /* InviteUsersScreenViewModel.swift */; }; - F541922A5B28C995E0BDB4E7 /* TimelineItemMenuAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF64B3A815D04325F1980E02 /* TimelineItemMenuAction.swift */; }; F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */; }; F5D2270B5021D521C0D22E11 /* FlowCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */; }; F656F92A63D3DC1978D79427 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 290FDEDA4D764B9F7EBE55A9 /* Algorithms */; }; @@ -1070,7 +1075,6 @@ F697284B9B5F2C00CFEA3B12 /* EmojiDetectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */; }; F6DFA23885980118AD7359C5 /* NotificationSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2389732B0E115A999A069083 /* NotificationSettingsScreenCoordinator.swift */; }; F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; }; - F7048AD79361405AA95F2B3B /* CallInviteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA8F098AE48D958B4257EB24 /* CallInviteRoomTimelineView.swift */; }; F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; }; F777C6FEE7D106136E2ED2B2 /* MessageForwardingScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */; }; F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */; }; @@ -1079,6 +1083,7 @@ F7D709D7ECABE46641BB8B6B /* PHGPostHogProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEEAE1BFAACD6C96B6DB731 /* PHGPostHogProtocol.swift */; }; F833D5B5BE6707F961FA88DB /* SecureBackupController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1119E9C63AE530252640D2 /* SecureBackupController.swift */; }; F86102DC2C68BBBB0521BAAE /* SoftLogoutScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB385E148DE55C85C0A02D6 /* SoftLogoutScreenModels.swift */; }; + F8B2F5CBCF2A0E0798E8D646 /* TimelineViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB5B00A014307CE37B2812CD /* TimelineViewModelProtocol.swift */; }; F8C87130FD999F7F1076208C /* RoomChangePermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89AAEA70CFF3284920811941 /* RoomChangePermissionsScreen.swift */; }; F8E725D42023ECA091349245 /* AudioRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57EAAF82432B0B53881CF826 /* AudioRoomTimelineItem.swift */; }; F8F47CE757EE656905F01F2C /* RoomRolesAndPermissionsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90DFF217B3D9D0941283278C /* RoomRolesAndPermissionsScreenViewModelProtocol.swift */; }; @@ -1093,7 +1098,6 @@ FB53CD9B74A15B3B94F9F788 /* CreateRoomModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B849D2FF2CC12BA411A1651 /* CreateRoomModels.swift */; }; FB595EC9C00AB32F39034055 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A37E2FACFD041CE466223CD /* SceneDelegate.swift */; }; FB9A1DD83EF641A75ABBCE69 /* WaitlistScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C796FC1DFDBCDD5573D0360F /* WaitlistScreenViewModelTests.swift */; }; - FBCCF1EA25A071324FCD8544 /* TimelineItemDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7023EB4F3B7C7D1FBA68638B /* TimelineItemDebugView.swift */; }; FBD402E3170EB1ED0D1AA672 /* EncryptionKeyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2355398E4A55DA5A89128AD1 /* EncryptionKeyProvider.swift */; }; FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */; }; FC10228E73323BDC09526F97 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; }; @@ -1110,7 +1114,6 @@ FF34BF2AF731340AF9414A18 /* SwipeRightAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4552D3466B1453F287223ADA /* SwipeRightAction.swift */; }; FF7E8ECC8E7E1D1851517536 /* PollFormScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */; }; FF9C06BBF6AC6F1CFFBEBFFC /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 90791B9C739C716A40E1B230 /* target.yml */; }; - FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892E29C98C4E8182C9037F84 /* TimelineStyler.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1169,13 +1172,15 @@ 00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreen.swift; sourceTree = ""; }; 00AFC5F08734C2EA4EE79C59 /* IdentityConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreen.swift; sourceTree = ""; }; 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelTests.swift; sourceTree = ""; }; - 0135A608FFAD86E6674EE730 /* RoomScreenInteractionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenInteractionHandler.swift; sourceTree = ""; }; + 012A284622B32052015F1F89 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = ""; }; 01B795AAAB7B8747FE2FF311 /* LogViewerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenModels.swift; sourceTree = ""; }; 01C4C7DB37597D7D8379511A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 022E6BD64CB4610B9C95FC02 /* UserDetailsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenViewModel.swift; sourceTree = ""; }; 024F7398C5FC12586FB10E9D /* EffectsScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectsScene.swift; sourceTree = ""; }; 0287793F11C480E242B03DF5 /* UserDiscoveryServiceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceTest.swift; sourceTree = ""; }; + 029D5701F80A9AF7167BB4D0 /* TimelineModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineModels.swift; sourceTree = ""; }; 02D155E09BF961BBA8F85263 /* InviteUsersScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenViewModel.swift; sourceTree = ""; }; + 02EE0FABA8ED6D6C1D6CE71D /* ReactionsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsSummaryView.swift; sourceTree = ""; }; 03277E40D0E0DE0712021A71 /* ServerConfirmationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenCoordinator.swift; sourceTree = ""; }; 033DB41C51865A2E83174E87 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 035177BCD8E8308B098AC3C2 /* WindowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowManager.swift; sourceTree = ""; }; @@ -1210,10 +1215,11 @@ 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = ""; }; 09CE2B7AD979BDEE09FEDB08 /* WaitlistScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenModels.swift; sourceTree = ""; }; 0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = ""; }; + 0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = ""; }; + 0B0E0B55E2EE75AF67029924 /* SwipeToReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToReplyView.swift; sourceTree = ""; }; 0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomModerationRole.swift; sourceTree = ""; }; 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineItem.swift; sourceTree = ""; }; 0BB05221D7D941CC82DC8480 /* LogViewerScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenViewModel.swift; sourceTree = ""; }; - 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSenderAvatarView.swift; sourceTree = ""; }; 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModel.swift; sourceTree = ""; }; 0BD116096CAA9139B95EEA9C /* UserProfileScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenViewModel.swift; sourceTree = ""; }; 0C34667458773B02AB5FB0B2 /* LegalInformationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModel.swift; sourceTree = ""; }; @@ -1252,23 +1258,19 @@ 13BE9781699FB510E9263192 /* AppSettingsHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsHook.swift; sourceTree = ""; }; 1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 1454CF3AABD242F55C8A2615 /* InviteUsersScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenModels.swift; sourceTree = ""; }; - 153726EDCE1ACBB3D466A916 /* ReactionsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsSummaryView.swift; sourceTree = ""; }; 1562EAF6231151A675BED7A9 /* RoomDirectorySearchScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenCoordinator.swift; sourceTree = ""; }; 15748C254911E3654C93B0ED /* MentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionBuilder.swift; sourceTree = ""; }; 1575947B7A6FE08C57FE5EE4 /* NetworkMonitorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorProtocol.swift; sourceTree = ""; }; 15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModel.swift; sourceTree = ""; }; - 15F30E7AE8A303E8FEC2499E /* ReadReceiptCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptCell.swift; sourceTree = ""; }; 161CD412E75F4086F422AE39 /* SessionVerificationScreenStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenStateMachine.swift; sourceTree = ""; }; 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenModels.swift; 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 = ""; }; 18486B87745B1811E7FBD3D2 /* AnalyticsPromptScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenModels.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 = ""; }; 18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactoryProtocol.swift; sourceTree = ""; }; 190EC7285D3CFEF0D3011BCF /* GeoURI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoURI.swift; sourceTree = ""; }; - 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = ""; }; 196004E7695FBA292A7944AF /* ScreenTrackerViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTrackerViewModifier.swift; sourceTree = ""; }; 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderProtocol.swift; sourceTree = ""; }; 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Untranslated.swift"; sourceTree = ""; }; @@ -1284,6 +1286,7 @@ 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxyProtocol.swift; sourceTree = ""; }; 1BA5A62DA4B543827FF82354 /* LAContextMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LAContextMock.swift; sourceTree = ""; }; 1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerProtocol.swift; sourceTree = ""; }; + 1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; 1C7F63EB1525E697CAEB002B /* BlankFormCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlankFormCoordinator.swift; sourceTree = ""; }; 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreen.swift; sourceTree = ""; }; 1CD7C0A2750998C2D77AD00F /* JoinRoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModel.swift; sourceTree = ""; }; @@ -1292,6 +1295,8 @@ 1D67E616BCA82D8A1258D488 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = ""; }; 1D70253004A5AEC9C73D6A4F /* ComposerDraftService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerDraftService.swift; sourceTree = ""; }; 1D8C38663020DF2EB2D13F5E /* AppLockSetupSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenViewModel.swift; sourceTree = ""; }; + 1D9F148717D74F73BE724434 /* LongPressWithFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPressWithFeedback.swift; sourceTree = ""; }; + 1DA7E93C2E148B96EF6A8500 /* TimelineItemAccessibilityModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemAccessibilityModifier.swift; sourceTree = ""; }; 1DB2FC2AA9A07EE792DF65CF /* NotificationPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenModels.swift; sourceTree = ""; }; 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = ""; }; 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenModels.swift; sourceTree = ""; }; @@ -1309,7 +1314,6 @@ 216F0DDC98F2A2C162D09C28 /* FileRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItemContent.swift; sourceTree = ""; }; 218AB05B4E3889731959C5F1 /* EventBasedTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedTimelineItemProtocol.swift; sourceTree = ""; }; 21BA866267F84BF4350B0CB7 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-BR"; path = "pt-BR.lproj/Localizable.stringsdict"; sourceTree = ""; }; - 21DC6A9917A7123E7E9A3F81 /* LongPressWithFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPressWithFeedback.swift; sourceTree = ""; }; 21DD8599815136EFF5B73F38 /* UserFlowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFlowTests.swift; sourceTree = ""; }; 225EFCA26877E75CDFE7F48D /* MapTilerStyleBuilderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStyleBuilderProtocol.swift; sourceTree = ""; }; 22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1344,7 +1348,6 @@ 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfigurationScreenViewStateTests.swift; sourceTree = ""; }; 27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; 27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = ""; }; - 27B8315A340B46F98B9C5AF0 /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = ""; }; 27D0EA07BD545CC9F234DB8D /* UserDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenModels.swift; sourceTree = ""; }; 28146817C61423CACCF942F5 /* CallScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenModels.swift; sourceTree = ""; }; 283974987DA7EC61D2AB57D9 /* VoiceMessageCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheTests.swift; sourceTree = ""; }; @@ -1352,12 +1355,14 @@ 287FC98AF2664EAD79C0D902 /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = ""; }; 28C19F54A0C4FC9AB7ABD583 /* TextRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItemContent.swift; sourceTree = ""; }; 28EA8BE9EEDBD17555141C7E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = ""; }; + 2910422CB628D3B2BBE47449 /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = ""; }; 295E28C3B9EAADF519BF2F44 /* AuthenticationFlowCoordinatorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationFlowCoordinatorUITests.swift; sourceTree = ""; }; 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = ""; }; 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = ""; }; 2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineTests.swift; sourceTree = ""; }; 2AE807361805463F5AEDD1CA /* VoiceMessagePreviewComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessagePreviewComposer.swift; sourceTree = ""; }; 2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenViewModelProtocol.swift; sourceTree = ""; }; + 2BA894BC09972DC45E497D37 /* TimelineInteractionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineInteractionHandler.swift; sourceTree = ""; }; 2BB385E148DE55C85C0A02D6 /* SoftLogoutScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenModels.swift; sourceTree = ""; }; 2BDB3E65A79779EDA5D33D8A /* AudioPlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerState.swift; sourceTree = ""; }; 2BFDCA5A09EE70BC17F2EFA7 /* URLComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponents.swift; sourceTree = ""; }; @@ -1368,9 +1373,12 @@ 2D0946F77B696176E062D037 /* RoomMembersListScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenModels.swift; sourceTree = ""; }; 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemProxy.swift; sourceTree = ""; }; 2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProviderTests.swift; sourceTree = ""; }; + 2DA4F09CB613C54FDC73AE6A /* ThreadDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDecorator.swift; sourceTree = ""; }; + 2DB0E533508094156D8024C3 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = ""; }; 2E11E7C396ED06A154CF6DF3 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/SAS.strings; sourceTree = ""; }; 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModelTests.swift; sourceTree = ""; }; 2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsViewModelTests.swift; sourceTree = ""; }; + 2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = ""; }; 2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxy.swift; sourceTree = ""; }; 303D9438EFB481F57A366E82 /* EncryptionResetPasswordScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenViewModel.swift; sourceTree = ""; }; 303FCADE77DF1F3670C086ED /* BugReportScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModel.swift; sourceTree = ""; }; @@ -1378,14 +1386,13 @@ 307702DD66E7DDCDD9214784 /* IdentityConfirmedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreen.swift; sourceTree = ""; }; 309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriver.swift; sourceTree = ""; }; 30ED584467DB380E3CEFB1DB /* NotificationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerTests.swift; sourceTree = ""; }; - 31352FFC2EF2C353CB7EA376 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = ""; }; 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceipt.swift; sourceTree = ""; }; - 317F41A4B5C4F457AF710666 /* PollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollView.swift; sourceTree = ""; }; 31A6314FDC51DA25712D9A81 /* PillContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillContextTests.swift; sourceTree = ""; }; 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModel.swift; sourceTree = ""; }; 3203C6566DC17B7AECC1B7FD /* RoomNotificationSettingsUserDefinedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsUserDefinedScreen.swift; sourceTree = ""; }; 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyProtocol.swift; sourceTree = ""; }; 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutViewModelTests.swift; sourceTree = ""; }; + 33035418BB35754232985871 /* TimelineReadReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReadReceiptsView.swift; sourceTree = ""; }; 330AF4D121C3396F7A14B21D /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/SAS.strings; sourceTree = ""; }; 3339B1DDB1341E833D2555BC /* AVMetadataMachineReadableCodeObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVMetadataMachineReadableCodeObject.swift; sourceTree = ""; }; 3368395F06AA180138E185B6 /* PollFormScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenUITests.swift; sourceTree = ""; }; @@ -1397,7 +1404,6 @@ 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenViewModelTests.swift; sourceTree = ""; }; 34E0FA38BD473FFA6F1AB7A5 /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = be; path = be.lproj/Localizable.stringsdict; sourceTree = ""; }; 34ED3AB7E0287552A5648AB3 /* be */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = be; path = be.lproj/InfoPlist.strings; sourceTree = ""; }; - 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReactionsView.swift; sourceTree = ""; }; 3558A15CFB934F9229301527 /* RestorationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationToken.swift; sourceTree = ""; }; 35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; 35FA991289149D31F4286747 /* UserPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreference.swift; sourceTree = ""; }; @@ -1432,6 +1438,7 @@ 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = ""; }; 3D4DD336905C72F95EAF34B7 /* ElementX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ElementX-Bridging-Header.h"; sourceTree = ""; }; 3D65BCC659FD9087E49B3C25 /* AppAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAppearance.swift; sourceTree = ""; }; + 3D75941CBD7D336F831924EC /* ReadReceiptCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptCell.swift; sourceTree = ""; }; 3D9B45D584D232CB9E5C7734 /* RoomChangeRolesScreenSelectedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenSelectedItem.swift; sourceTree = ""; }; 3D9FCE4D1E3A81AC1CC5CB91 /* AppLockSetupSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenCoordinator.swift; sourceTree = ""; }; 3DBE70FFB7936F35811772C1 /* IdentityConfirmedScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreenModels.swift; sourceTree = ""; }; @@ -1440,6 +1447,7 @@ 3E93A1BE7D8A2EBCAD51EEB4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; 3E9E0929CEFA356090BE5FB8 /* RoomMembersListScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModelTests.swift; sourceTree = ""; }; 3EF1AC723C2609C7705569CA /* MediaLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderTests.swift; sourceTree = ""; }; + 3F54FA7C5CB7B342EF9B9B2F /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; 3FFDA99C98BE05F43A92343B /* test_pdf.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = test_pdf.pdf; sourceTree = ""; }; 40076C770A5FB83325252973 /* VoiceMessageMediaManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManager.swift; sourceTree = ""; }; 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelTests.swift; sourceTree = ""; }; @@ -1455,11 +1463,12 @@ 422724361B6555364C43281E /* RoomHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomHeaderView.swift; sourceTree = ""; }; 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenModels.swift; sourceTree = ""; }; 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreen.swift; sourceTree = ""; }; - 42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineView.swift; sourceTree = ""; }; 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenModels.swift; sourceTree = ""; }; 43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheProtocol.swift; sourceTree = ""; }; 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareToMapsAppActivity.swift; sourceTree = ""; }; + 44ABA63DBE7F76C58260B43B /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; 44C314C00533E2C297796B60 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; + 44ECC9D66400727DFFEE12E8 /* TimelineStartRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineView.swift; sourceTree = ""; }; 450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = ""; }; 4525E8C0FBDD27D1ACE90952 /* SecureBackupKeyBackupScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModelProtocol.swift; sourceTree = ""; }; 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreen.swift; sourceTree = ""; }; @@ -1475,11 +1484,11 @@ 46D560DDA3B20C82766ACFAD /* NotificationSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModel.swift; sourceTree = ""; }; 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = ""; }; 471BB7276C97AF60B3A5463B /* RoomDirectorySearchProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxy.swift; sourceTree = ""; }; - 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; 475D47D0BFE961B02BAC5D49 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = ""; }; 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.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 = ""; }; + 4853C923A1AF43711D025EAF /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = ""; }; 48A5C34C4E4268EF65D171EF /* RoomChangeRolesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenModels.swift; sourceTree = ""; }; 48FEFF746DB341CDB18D7AAA /* RoomRolesAndPermissionsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenViewModelTests.swift; sourceTree = ""; }; 490BEADEFB2D6B7C9F618AE8 /* AppLockTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimer.swift; sourceTree = ""; }; @@ -1488,7 +1497,6 @@ 4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenModels.swift; sourceTree = ""; }; 49ABAB186CF00B15C5521D04 /* MenuSheetLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSheetLabelStyle.swift; sourceTree = ""; }; 49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - 49E45C3DC740D3AB9A47FD32 /* SwipeToReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToReplyView.swift; sourceTree = ""; }; 49E6066092ED45E36BB306F7 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.stringsdict"; sourceTree = ""; }; 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = ""; }; 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1502,10 +1510,11 @@ 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; }; 4D3A7375AB22721C436EB056 /* ComposerToolbarModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarModels.swift; sourceTree = ""; }; 4E2245243369B99216C7D84E /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; - 4E47F18A9A077E351CEA10D4 /* TextBasedRoomTimelineViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewProtocol.swift; sourceTree = ""; }; 4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerDraftServiceMock.swift; sourceTree = ""; }; 4E625B0EB2F86B37C14EF7E6 /* SettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenViewModel.swift; sourceTree = ""; }; + 4E7F7A975514E850A834B29F /* PaginationIndicatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineView.swift; sourceTree = ""; }; 4F5F0662483ED69791D63B16 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = et; path = et.lproj/Localizable.stringsdict; sourceTree = ""; }; + 4F75EF13F49DD2204E760910 /* FileRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineView.swift; sourceTree = ""; }; 4FA29BAE9B0F2D90E57B261C /* UserSessionFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinatorTests.swift; sourceTree = ""; }; 4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinatorTests.swift; sourceTree = ""; }; 4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = ""; }; @@ -1515,7 +1524,6 @@ 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItem.swift; sourceTree = ""; }; 50D685B4DB38BB5BD87C956A /* AuthenticationStartScreenBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenBackgroundImage.swift; sourceTree = ""; }; 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixUserShareLink.swift; sourceTree = ""; }; - 50F23B21CF15F9F4BAA0788B /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = ""; }; 510E89B989477E5EE8E503C0 /* PinnedEventsTimelineScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsTimelineScreenViewModelProtocol.swift; sourceTree = ""; }; 514363244AE7D68080D44C6F /* NotificationSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModelTests.swift; sourceTree = ""; }; 514923AA9640C34F39E0500A /* GenericCallLinkCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericCallLinkCoordinator.swift; sourceTree = ""; }; @@ -1535,17 +1543,16 @@ 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; 5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreen.swift; sourceTree = ""; }; 54A5E6F398C269AD52C9AE21 /* EncryptionResetPasswordScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenModels.swift; sourceTree = ""; }; + 54AD70D6E03D2031AE1B5A52 /* TimelineReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReactionsView.swift; sourceTree = ""; }; 54C4E7B46099462F12000C91 /* DeveloperOptionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModelProtocol.swift; sourceTree = ""; }; 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemSender.swift; sourceTree = ""; }; 5644919DB2022397D9D5825A /* MockSoftLogoutScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSoftLogoutScreenState.swift; sourceTree = ""; }; 565F1B2B300597C616B37888 /* FullscreenDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenDialog.swift; sourceTree = ""; }; - 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = ""; }; 56D6F88FE35A0979D2821E06 /* AppLockScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreen.swift; sourceTree = ""; }; 57916A1578D8043BB0795441 /* GeneratedMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedMocks.swift; sourceTree = ""; }; 57B6B383F1FD04CC0E7B60C6 /* AnalyticsConsentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsConsentState.swift; sourceTree = ""; }; 57EAAF82432B0B53881CF826 /* AudioRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineItem.swift; sourceTree = ""; }; 57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLocator.swift; sourceTree = ""; }; - 584509A363ADF6244DFDB96A /* CallNotificationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallNotificationRoomTimelineView.swift; sourceTree = ""; }; 584A61D9C459FAFEF038A7C0 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = ""; }; 5875F7C0A2398E9F134B1284 /* EncryptionResetScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreenViewModel.swift; sourceTree = ""; }; 58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelTests.swift; sourceTree = ""; }; @@ -1554,6 +1561,7 @@ 595EC503DA5517BBE6D39406 /* IdentityConfirmedScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreenCoordinator.swift; sourceTree = ""; }; 596AA8843AC1A234F3387767 /* SecureBackupRecoveryKeyScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenCoordinator.swift; sourceTree = ""; }; 598F01EBD0C4CC550C644418 /* AuthenticationStartLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartLogo.swift; sourceTree = ""; }; + 59B7CC77B82C6C67DE3AD869 /* HighlightedTimelineItemModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightedTimelineItemModifier.swift; sourceTree = ""; }; 5A07692536D66E3DA32C4964 /* LogViewerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreen.swift; sourceTree = ""; }; 5A1119E9C63AE530252640D2 /* SecureBackupController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupController.swift; sourceTree = ""; }; 5A2FCA3D0F239B9E911B966B /* SecureBackupRecoveryKeyScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreen.swift; sourceTree = ""; }; @@ -1567,6 +1575,7 @@ 5D82F234B3576BD6268C7950 /* ScaledFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledFrameModifier.swift; sourceTree = ""; }; 5D99730313BEBF08CDE81EE3 /* EmojiDetection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetection.swift; sourceTree = ""; }; 5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProperties.swift; sourceTree = ""; }; + 5E33FD32BBC44D703C7AE4F9 /* TextBasedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineItem.swift; sourceTree = ""; }; 5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProtocol.swift; sourceTree = ""; }; 5E9CBF577B9711CFBB4FA40D /* VoiceMessageRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingView.swift; sourceTree = ""; }; 5EB2CAA266B921D128C35710 /* LegalInformationScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenCoordinator.swift; sourceTree = ""; }; @@ -1577,36 +1586,37 @@ 604A69C081B935D6A38DE6D8 /* UserProfileScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenModels.swift; sourceTree = ""; }; 60C9BAE9F9436B14E4E22E8F /* PinnedItemsBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedItemsBannerView.swift; sourceTree = ""; }; 60F18AECC9D38C2B6D85F99C /* Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publisher.swift; sourceTree = ""; }; - 612EF972F2A1800682D32C5E /* StickerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerRoomTimelineView.swift; sourceTree = ""; }; 61B33F23681660E940BA57F4 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/SAS.strings; sourceTree = ""; }; 622D09D4ECE759189009AEAF /* MapLibreMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreMapView.swift; sourceTree = ""; }; 624244C398804ADC885239AA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; 62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderTests.swift; sourceTree = ""; }; 62B07B296D7A9D2F09120853 /* OrderedSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedSet.swift; sourceTree = ""; }; + 638790D3F915F0909315C47A /* PollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollView.swift; sourceTree = ""; }; 638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveQuickLook.swift; sourceTree = ""; }; - 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = ""; }; 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenViewModelProtocol.swift; sourceTree = ""; }; 645E027C112740573D27765C /* SecureBackupRecoveryKeyScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenModels.swift; sourceTree = ""; }; 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridableAvatarImage.swift; sourceTree = ""; }; 6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenCoordinator.swift; sourceTree = ""; }; 64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerScreenCoordinator.swift; sourceTree = ""; }; + 6509708F54FC883604DFDC95 /* TimelineViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModelTests.swift; sourceTree = ""; }; 653610CB5F9776EAAAB98155 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = ""; }; 6569593FA36B22259E806A67 /* AudioRecorderState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderState.swift; sourceTree = ""; }; 65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryService.swift; sourceTree = ""; }; 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationServiceProxy.swift; sourceTree = ""; }; + 664ABD745A746C45CB842158 /* CallInviteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallInviteRoomTimelineView.swift; sourceTree = ""; }; 6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = ""; }; 6663BFB9FDB8752562CD12CA /* AuthenticationStartScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenCoordinator.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 = ""; }; 66AFD800AF033D8B0D11191A /* UserPropertiesExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPropertiesExt.swift; sourceTree = ""; }; 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProvider.swift; sourceTree = ""; }; + 66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = ""; }; 6722709BD6178E10B70C9641 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/SAS.strings; sourceTree = ""; }; 68010886142843705E342645 /* ProgressMaskModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressMaskModifier.swift; sourceTree = ""; }; 6861FE915C7B5466E6962BBA /* StartChatScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreen.swift; sourceTree = ""; }; 693E16574C6F7F9FA1015A8C /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = ""; }; 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregatedReactionMock.swift; sourceTree = ""; }; 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModelTests.swift; sourceTree = ""; }; - 6A4C9547BBFEEF30AA11329B /* TimelineItemStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemStatusView.swift; sourceTree = ""; }; 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlistReader.swift; sourceTree = ""; }; 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitTestsAppCoordinator.swift; sourceTree = ""; }; 6AB54B4F94686CCF0289B72F /* UserIndicatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorPresenter.swift; sourceTree = ""; }; @@ -1615,21 +1625,21 @@ 6B5E29E9A22F45534FBD5B58 /* EmojiPickerScreenHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenHeaderView.swift; sourceTree = ""; }; 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProvider.swift; sourceTree = ""; }; 6C8EC6EA7EDFCE46710DA306 /* AnalyticsPromptScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenViewModel.swift; sourceTree = ""; }; + 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsScreenIdentifier.swift; sourceTree = ""; }; 6D0A27607AB09784C8501B5C /* DeveloperOptionsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModelTests.swift; sourceTree = ""; }; 6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationUITests.swift; sourceTree = ""; }; 6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatViewModelTests.swift; sourceTree = ""; }; - 6E2656184491C505700D2405 /* CollapsibleRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleRoomTimelineView.swift; sourceTree = ""; }; 6E5725BC6C63604CB769145B /* LegalInformationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModelTests.swift; sourceTree = ""; }; 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = ""; }; 6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenModels.swift; sourceTree = ""; }; 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = ""; }; + 6F1C3CBBC62C566DDF5E84C1 /* TimelineItemMenuAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenuAction.swift; sourceTree = ""; }; 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModel.swift; sourceTree = ""; }; 6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelTests.swift; sourceTree = ""; }; 6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsBannerStateTests.swift; sourceTree = ""; }; 6FC5015B9634698BDB8701AF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = ""; }; 6FC8B21E86B137BE4E91F82A /* ElementCallServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceProtocol.swift; sourceTree = ""; }; - 7023EB4F3B7C7D1FBA68638B /* TimelineItemDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemDebugView.swift; sourceTree = ""; }; 7061BE2C0BF427C38AEDEF5E /* SecureBackupRecoveryKeyScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModel.swift; sourceTree = ""; }; 70C86696AC9521F8ED88FBEB /* MediaUploadPreviewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreen.swift; sourceTree = ""; }; 713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1639,6 +1649,7 @@ 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenModels.swift; sourceTree = ""; }; 71D52BAA5BADB06E5E8C295D /* Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; 71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenViewModelTests.swift; sourceTree = ""; }; + 72614BFF35B8394C6E13F55A /* TimelineItemStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemStatusView.swift; sourceTree = ""; }; 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = ""; }; 7310D8DFE01AF45F0689C3AA /* Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Publisher.swift; sourceTree = ""; }; 7367B3B9A8CAF902220F31D1 /* BugReportFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportFlowCoordinator.swift; sourceTree = ""; }; @@ -1649,13 +1660,12 @@ 74653BE903970C0E36867D46 /* GlobalSearchScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenCoordinator.swift; sourceTree = ""; }; 7475C5AE20BA896930907EA8 /* AudioRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineItemContent.swift; sourceTree = ""; }; 748AE77AC3B0A01223033B87 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; - 74C6F3DAD167F972702C8893 /* TimelineItemAccessibilityModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemAccessibilityModifier.swift; sourceTree = ""; }; 74DD0855F2F76D47E5555082 /* MediaUploadPreviewScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenCoordinator.swift; sourceTree = ""; }; 74E08B8A66948E9690F38B94 /* SecureBackupLogoutConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenViewModelProtocol.swift; sourceTree = ""; }; 7509AB72755DCC4B4E721B36 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/SAS.strings; sourceTree = ""; }; 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signposter.swift; sourceTree = ""; }; + 753B4C6C0EDDCBF0708DC384 /* TimelineItemSendInfoLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemSendInfoLabel.swift; sourceTree = ""; }; 76310030C831D4610A705603 /* URLComponentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponentsTests.swift; sourceTree = ""; }; - 772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = ""; }; 7773CBFDBD458E0B7E270507 /* PillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillView.swift; sourceTree = ""; }; 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModel.swift; sourceTree = ""; }; 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1679,7 +1689,6 @@ 7C1AF829F12FDC99717082D9 /* RoomRolesAndPermissionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenViewModel.swift; sourceTree = ""; }; 7C28B70BEFD3676F11D5D51F /* RoomRolesAndPermissionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenCoordinator.swift; sourceTree = ""; }; 7C71B9802433F1B4252291BB /* IdentityConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenViewModelProtocol.swift; sourceTree = ""; }; - 7CA3F8E905DF50BF22ECC18F /* ThreadDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadDecorator.swift; sourceTree = ""; }; 7D0CBC76C80E04345E11F2DB /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 7D25A35764C7B3DB78954AB5 /* RoomTimelineItemFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemFactoryProtocol.swift; sourceTree = ""; }; 7D39AF1F659923D77778511E /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1694,8 +1703,10 @@ 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceProtocol.swift; sourceTree = ""; }; 7FB2253D36E81E045E1CB432 /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = ""; }; 7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenModels.swift; sourceTree = ""; }; + 8063E65441E771200108C558 /* ReadReceiptsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptsSummaryView.swift; sourceTree = ""; }; 80C4927D09099497233E9980 /* WaitlistScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreen.swift; sourceTree = ""; }; 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageEventStringBuilder.swift; sourceTree = ""; }; + 8112846C9D9D3817689CBAF8 /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = ""; }; 811E8BF34E931D51552C9C13 /* EncryptionResetScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreen.swift; sourceTree = ""; }; 8140010A796DB2C7977B6643 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; 8166F121C79C7B62BF01D508 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = pt; path = pt.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1703,6 +1714,7 @@ 81A9B5225D0881CEFA2CF7C9 /* RoomNotificationSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModel.swift; sourceTree = ""; }; 81B17B1F29448D1B9049B11C /* ReportContentScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenViewModel.swift; sourceTree = ""; }; 81B17DB1BC3B0C62AF84D230 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 81F0325E252B057FAEEE1B2D /* TypingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = ""; }; 8296D6FB451E25CEC0767BBA /* RoomNotificationSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenCoordinator.swift; sourceTree = ""; }; 82B612853BFB68373249777B /* SecureBackupKeyBackupScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModel.swift; sourceTree = ""; }; 82DFA1B7B088D033E0794B82 /* RoomChangeRolesScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenCoordinator.swift; sourceTree = ""; }; @@ -1735,7 +1747,6 @@ 8896CDD20CA2D87EA3B848A1 /* RoomNotificationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreen.swift; sourceTree = ""; }; 889DEDD63C68ABDA8AD29812 /* VoiceMessageMediaManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManagerProtocol.swift; sourceTree = ""; }; 89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerStateTests.swift; sourceTree = ""; }; - 892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = ""; }; 893777A4997BBDB68079D4F5 /* ArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayTests.swift; sourceTree = ""; }; 894EE8F5B399A165BA2A6634 /* RoomDirectorySearchMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchMock.swift; sourceTree = ""; }; 8977176AB534AA41630395BC /* LegalInformationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1754,7 +1765,6 @@ 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 = ""; }; 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorMock.swift; sourceTree = ""; }; - 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; 8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = ""; }; 8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = ""; }; @@ -1769,7 +1779,9 @@ 90F2F8998E5632668B0AD848 /* RoomTimelineItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemView.swift; sourceTree = ""; }; 913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachmentData.swift; sourceTree = ""; }; 91868EB98818044E6FEBE532 /* NotificationPermissionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenCoordinator.swift; sourceTree = ""; }; + 91C8BD78F7B9247AC57FA1A3 /* RedactedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineView.swift; sourceTree = ""; }; 91CF6F7D08228D16BA69B63B /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = ""; }; + 91FFE1F410969ECB23FE9BB2 /* TimelineItemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenu.swift; sourceTree = ""; }; 92390F9FA98255440A6BF5F8 /* OIDCAuthenticationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCAuthenticationPresenter.swift; sourceTree = ""; }; 92DB574F954CC2B40F7BE892 /* QRCodeScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScannerView.swift; sourceTree = ""; }; 9332DFE9642F0A46ECA0497B /* BlurHashEncode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashEncode.swift; sourceTree = ""; }; @@ -1777,7 +1789,7 @@ 9342F5D6729627B6393AF853 /* ServerConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenModels.swift; sourceTree = ""; }; 9349F590E35CE514A71E6764 /* LoginHomeserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginHomeserver.swift; sourceTree = ""; }; 935C2FB18EFB8EEE96B26330 /* CreateRoomFlowParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomFlowParameters.swift; sourceTree = ""; }; - 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelTests.swift; sourceTree = ""; }; + 93C713D124FE915ABF47A6B7 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; }; 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVMetadataMachineReadableCodeObjectExtensionsTest.swift; sourceTree = ""; }; 93E7304F5ECB4CB11CB10E60 /* SecureBackupRecoveryKeyScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModelProtocol.swift; sourceTree = ""; }; 94028A227645FA880B966211 /* WaveformSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformSource.swift; sourceTree = ""; }; @@ -1789,13 +1801,13 @@ 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomStringConvertible.swift; sourceTree = ""; }; 974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPermissions.swift; sourceTree = ""; }; 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelProtocol.swift; sourceTree = ""; }; + 97B2ACA28A854E41AE3AC9AD /* TimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModel.swift; sourceTree = ""; }; 97C8E13A1FBA717B0C277ECC /* ProgressCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressCursorModifier.swift; sourceTree = ""; }; 97CE98208321C4D66E363612 /* ShimmerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShimmerModifier.swift; sourceTree = ""; }; 981663D961C94270FA035FD0 /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = ""; }; 9873076F224E4CE09D8BD47D /* TemplateScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenUITests.swift; sourceTree = ""; }; 989D7380D9C86B3A10D30B13 /* AppLockSetupPINScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModelTests.swift; sourceTree = ""; }; 989FC684408B31A677F5538B /* CompletionSuggestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionView.swift; sourceTree = ""; }; - 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = ""; }; 997BF045585AF6DB2EBC5755 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinator.swift; sourceTree = ""; }; 9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeLabel.swift; sourceTree = ""; }; @@ -1835,8 +1847,7 @@ A16D0F226B1819D017531647 /* BlockedUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenCoordinator.swift; sourceTree = ""; }; A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachineTests.swift; sourceTree = ""; }; A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryPicker.swift; sourceTree = ""; }; - A2751266F17A5BF25DA9227E /* TimelineItemMenuActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenuActionProvider.swift; sourceTree = ""; }; - A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineView.swift; sourceTree = ""; }; + A243A6E6207297123E60DE48 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = ""; }; A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenContent.swift; sourceTree = ""; }; A3FBD9C2B9A5479526920399 /* BugReportScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenCoordinator.swift; sourceTree = ""; }; A40C19719687984FD9478FBE /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; @@ -1848,8 +1859,10 @@ A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiDetectionTests.swift; sourceTree = ""; }; A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = ""; }; A69869844D2B6F5BD9AABF85 /* OIDCConfigurationProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCConfigurationProxy.swift; sourceTree = ""; }; + A6B19D10B102956066AF117B /* PollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionView.swift; sourceTree = ""; }; A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = ""; }; A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriverProtocol.swift; sourceTree = ""; }; + A6EA0D8B0BBD8805F7D5A133 /* TextBasedRoomTimelineViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewProtocol.swift; sourceTree = ""; }; A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = ""; }; A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = ""; }; A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1858,6 +1871,7 @@ A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = ""; }; A8DF55467ED4CE76B7AE9A33 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/InfoPlist.strings; sourceTree = ""; }; A9B069D7772DDF6513E0F1B8 /* AuthenticationFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationFlowCoordinator.swift; sourceTree = ""; }; + A9E6065FC6BC4A1B4C629E08 /* TimelineItemMenuActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenuActionProvider.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 = ""; }; AAC9344689121887B74877AF /* UnitTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1878,6 +1892,7 @@ AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; AD558A898847C179E4B7A237 /* SecureBackupKeyBackupScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreen.swift; sourceTree = ""; }; AD6B522BD637845AB9570B10 /* RoomNotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxy.swift; sourceTree = ""; }; + AD6E082B0507FB28F966516A /* CallNotificationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallNotificationRoomTimelineView.swift; sourceTree = ""; }; AD72A9B720D75DBE60AC299F /* SecureBackupKeyBackupScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenModels.swift; sourceTree = ""; }; AD9AD6AE5FC868962F090740 /* CallScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModelProtocol.swift; sourceTree = ""; }; AD9CB3B9DFA353AB2B7CD9F8 /* NotificationSettingsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenCoordinator.swift; sourceTree = ""; }; @@ -1890,8 +1905,8 @@ AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilderTests.swift; sourceTree = ""; }; AF042B0FB2EE88977C91E330 /* portrait_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = portrait_test_image.jpg; sourceTree = ""; }; AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderTests.swift; sourceTree = ""; }; - AF64B3A815D04325F1980E02 /* TimelineItemMenuAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenuAction.swift; sourceTree = ""; }; AF848B41DAF1066F3054D4A1 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = ""; }; + AF8548D48512127CCC17C520 /* PollRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollRoomTimelineView.swift; sourceTree = ""; }; AFEF489B8E2450E2BA1A314E /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/SAS.strings; sourceTree = ""; }; B050A6B233D95807A09289E7 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = wrapper.cfbundle; path = Settings.bundle; sourceTree = ""; }; B0618820D26F9871A4BBB40E /* ComposerToolbarViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModelProtocol.swift; sourceTree = ""; }; @@ -1903,9 +1918,11 @@ B1E227F34BE43B08E098796E /* TestablePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestablePreview.swift; sourceTree = ""; }; B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorProtocol.swift; sourceTree = ""; }; B2AD8A56CD37E23071A2F4BF /* PHGPostHogMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogMock.swift; sourceTree = ""; }; + B2AF1828A5B76B7C371240FE /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = ""; }; B2B1DC3B3FB40A7F4AE9B7BF /* RoomRolesAndPermissionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreen.swift; sourceTree = ""; }; B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItemContent.swift; sourceTree = ""; }; B2E7C987AE5DC9087BB19F7D /* MediaUploadPreviewScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenModels.swift; sourceTree = ""; }; + B2EAFFD44F81F86012D6EC27 /* AudioRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineView.swift; sourceTree = ""; }; B3005886F00029F058DB62BE /* StartChatScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenCoordinator.swift; sourceTree = ""; }; B383DCD3DCB19E00FD478A5F /* ConfirmationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationDialog.swift; sourceTree = ""; }; B3A1398EFF65090FDA1CB639 /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = ""; }; @@ -1919,9 +1936,7 @@ B50F03079F6B5EF9CA005F14 /* TimelineProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProxyProtocol.swift; sourceTree = ""; }; B53AC78E49A297AC1D72A7CF /* AppMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediator.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 = ""; }; - B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = ""; }; B63B69F9A2BC74DD40DC75C8 /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = ""; }; B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetails.swift; sourceTree = ""; }; B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1941,17 +1956,16 @@ B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = ""; }; B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; - BB0A77874B29D79DDFC051AC /* ReadReceiptsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptsSummaryView.swift; sourceTree = ""; }; BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionProtocol.swift; sourceTree = ""; }; + BB5B00A014307CE37B2812CD /* TimelineViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModelProtocol.swift; sourceTree = ""; }; BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = ""; }; BBEC57C204D77908E355EF42 /* AudioRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderProtocol.swift; sourceTree = ""; }; BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = ""; }; - BDEB27575FEBCF414D4DEE31 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; }; + BCDA016D05107DED3B9495CB /* TimelineItemDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemDebugView.swift; sourceTree = ""; }; BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationContent.swift; sourceTree = ""; }; BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemReplyDetails.swift; sourceTree = ""; }; BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenViewModel.swift; sourceTree = ""; }; @@ -1961,6 +1975,7 @@ BEF5FE93A06F563B477F024A /* RoomAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAvatarImage.swift; sourceTree = ""; }; BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderScreenCoordinator.swift; sourceTree = ""; }; BFA9EA59D5C0DA1BFC7B3621 /* QRCodeLoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreen.swift; sourceTree = ""; }; + BFBF273BC2BFB9F3EEFA988B /* CollapsibleRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleRoomTimelineView.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 = ""; }; BFEE91FB8ABB5F5884B6D940 /* WaveformInteractionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformInteractionModifier.swift; sourceTree = ""; }; @@ -1976,6 +1991,7 @@ C18CC37B97E77838609CFFE7 /* AdvancedSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreen.swift; sourceTree = ""; }; C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OIDCAccountSettingsPresenter.swift; sourceTree = ""; }; C1FA515B3B0D61EF1E907D2D /* BadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeView.swift; sourceTree = ""; }; + C258C9C815272911A5B132C3 /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = ""; }; C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = ""; }; C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreen.swift; sourceTree = ""; }; C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKGeneratedMocks.swift; sourceTree = ""; }; @@ -1986,6 +2002,7 @@ C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelProtocol.swift; sourceTree = ""; }; C4CD503F5E0938FE53C7C6E7 /* UserDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenCoordinator.swift; sourceTree = ""; }; C55679AF67545EF8087E47BE /* RoomMemberDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetails.swift; sourceTree = ""; }; + C5599255A6C98EBDA77B76E6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = ""; }; C55CC239AE12339C565F6C9A /* AudioRecorderStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderStateTests.swift; sourceTree = ""; }; C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = ""; }; C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenUITests.swift; sourceTree = ""; }; @@ -2003,18 +2020,16 @@ C796FC1DFDBCDD5573D0360F /* WaitlistScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelTests.swift; sourceTree = ""; }; C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = ""; }; C833673B334A0651AB46F30B /* StaticLocationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModelTests.swift; sourceTree = ""; }; - C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineView.swift; sourceTree = ""; }; C90514BE9B8ACCBCF0AD2489 /* ComposerToolbarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModel.swift; sourceTree = ""; }; - C936FDD017808FE416742D64 /* PollRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollRoomTimelineView.swift; sourceTree = ""; }; C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = ""; }; C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenRoomCell.swift; sourceTree = ""; }; C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinator.swift; sourceTree = ""; }; + C9E535B3388755B65C34CD10 /* UnsupportedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineView.swift; sourceTree = ""; }; C9F893F4A111CB7BA5C96949 /* AppLockSetupBiometricsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenViewModel.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 = ""; }; CA4F6D7000EDCD187E0989E7 /* PinnedEventsTimelineScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsTimelineScreen.swift; sourceTree = ""; }; CA89A2DD51B6BBE1DA55E263 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; - CA8F098AE48D958B4257EB24 /* CallInviteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallInviteRoomTimelineView.swift; sourceTree = ""; }; CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenCoordinator.swift; sourceTree = ""; }; CACA846B3E3E9A521D98B178 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModelTests.swift; sourceTree = ""; }; @@ -2031,17 +2046,12 @@ CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItemContent.swift; sourceTree = ""; }; CD700E035C85738EE4B97129 /* PerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceTests.swift; sourceTree = ""; }; CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; - CE47A97726F0675DEE387BF9 /* TypingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorView.swift; sourceTree = ""; }; - CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; CEE20623EB4A9B88FB29F2BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SAS.strings; sourceTree = ""; }; CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModelProtocol.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 = ""; }; - D15447A39D91D2EF536C74DD /* TimelineItemSendInfoLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemSendInfoLabel.swift; sourceTree = ""; }; - D162B2280A15ACAF35360554 /* HighlightedTimelineItemModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightedTimelineItemModifier.swift; sourceTree = ""; }; D1896F6288D80E1F3EFB3DF8 /* ka */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ka; path = ka.lproj/Localizable.stringsdict; sourceTree = ""; }; D196116D2DD3F2757D45FCB7 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/SAS.strings; sourceTree = ""; }; D1BC84BA0AF11C2128D58ABD /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; @@ -2061,8 +2071,9 @@ D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = ""; }; D4DA544B2520BFA65D6DB4BB /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; D529B976F8B2AA654D923422 /* VoiceMessageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomTimelineItem.swift; sourceTree = ""; }; + D5338450E6783A576B5C16DD /* StickerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerRoomTimelineView.swift; sourceTree = ""; }; + D53FCCE44F96E0BC411A6CF0 /* TimelineSenderAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSenderAvatarView.swift; sourceTree = ""; }; D54E12B98252F6C527E31FEE /* MediaUploadPreviewScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelProtocol.swift; sourceTree = ""; }; - D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = ""; }; D5B4932E4EFBC8FAC10972CD /* UserProfileScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenCoordinator.swift; sourceTree = ""; }; D5E26C54362206BBDD096D83 /* test_audio.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = test_audio.mp3; sourceTree = ""; }; D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionServiceTests.swift; sourceTree = ""; }; @@ -2109,7 +2120,6 @@ E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = ""; }; E26C69EC1157D71CC61ADAE4 /* ScaledPaddingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledPaddingModifier.swift; sourceTree = ""; }; E2B1CC9AA154F4D5435BF60A /* Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comparable.swift; sourceTree = ""; }; - E2DCA495ED42D2463DDAA94D /* TimelineBubbleLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBubbleLayout.swift; sourceTree = ""; }; E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyMock.swift; sourceTree = ""; }; E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModel.swift; sourceTree = ""; }; E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceMock.swift; sourceTree = ""; }; @@ -2120,7 +2130,6 @@ E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorModalView.swift; sourceTree = ""; }; E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverter.swift; sourceTree = ""; }; E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteURLParserTests.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 = ""; }; E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = ""; }; E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyProtocol.swift; sourceTree = ""; }; @@ -2155,16 +2164,16 @@ EBEB8D9F4940E161B18FE4BC /* UITestsNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsNotificationCenter.swift; sourceTree = ""; }; EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModelTests.swift; sourceTree = ""; }; EC5D7DA665E1F5F509C994C7 /* ScaledOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledOffsetModifier.swift; sourceTree = ""; }; - EC8D3544DE3FABD958BA8F19 /* TimelineItemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenu.swift; sourceTree = ""; }; ECD5FCBA169B6A82F501CA1B /* AnalyticsSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = ""; }; ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfiguration.swift; sourceTree = ""; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; + ED0CBEAB5F796BEFBAF7BB6A /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModel.swift; sourceTree = ""; }; ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; + ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = ""; }; ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; 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 = ""; }; EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenScreenViewModelTests.swift; sourceTree = ""; }; @@ -2200,12 +2209,12 @@ F5311C989EC15B4C2D699025 /* StaticLocationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModel.swift; sourceTree = ""; }; F57C8022B8A871A1DCD1750A /* UserIndicatorToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorToastView.swift; sourceTree = ""; }; F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyMock.swift; sourceTree = ""; }; + F5D8FEB1FED10E995CB002F7 /* TimelineBubbleLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBubbleLayout.swift; sourceTree = ""; }; F5E23D8EE6CBACF32F1EC874 /* MediaPlayerProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProviderProtocol.swift; sourceTree = ""; }; F64A8582F65567AC38C2976A /* PollFormScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenViewModel.swift; sourceTree = ""; }; F6D698BFD68B061350553930 /* WaitingDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitingDialog.swift; sourceTree = ""; }; F72EFC8C634469F9262659C7 /* NSItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSItemProvider.swift; sourceTree = ""; }; F733F135E6D67BBBEB76CC30 /* AppLockUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockUITests.swift; sourceTree = ""; }; - F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = ""; }; F74532E01B317C56C1BE8FA8 /* RoomTimelineProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProviderMock.swift; sourceTree = ""; }; F7478623CECC9438014244BA /* ServerConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreen.swift; sourceTree = ""; }; F7E8A8047B50E3607ACD354E /* ImageProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProviderProtocol.swift; sourceTree = ""; }; @@ -2214,8 +2223,6 @@ F8CCF9A924521DECA44778C4 /* AppLockSetupBiometricsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreen.swift; sourceTree = ""; }; F8CEB4634C0DD7779C4AB504 /* CreateRoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreenUITests.swift; sourceTree = ""; }; F9E543072DE58E751F028998 /* TimelineProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProxy.swift; sourceTree = ""; }; - F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = ""; }; - F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineView.swift; sourceTree = ""; }; FA3EB5B1848CF4F64E63C6B7 /* PermalinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermalinkTests.swift; sourceTree = ""; }; FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelProtocol.swift; sourceTree = ""; }; FABAC5C4373B0EC24D399663 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/SAS.strings"; sourceTree = ""; }; @@ -2224,7 +2231,6 @@ FB9EABCA9348DFA27439A809 /* WaveformCursorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformCursorView.swift; sourceTree = ""; }; FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreen.swift; sourceTree = ""; }; FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = ""; }; - FC2D505742FDA21FCDC4C18A /* AudioRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineView.swift; sourceTree = ""; }; FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderMock.swift; sourceTree = ""; }; FC853F9B4FBE039D2C16EC6B /* RoomMembersListManageMemberSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListManageMemberSheet.swift; sourceTree = ""; }; FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockFlowCoordinator.swift; sourceTree = ""; }; @@ -2542,6 +2548,31 @@ path = View; sourceTree = ""; }; + 13CC0A77B6E5EEDF881C5E8B /* Supplementary */ = { + isa = PBXGroup; + children = ( + 02EE0FABA8ED6D6C1D6CE71D /* ReactionsSummaryView.swift */, + 66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */, + 72614BFF35B8394C6E13F55A /* TimelineItemStatusView.swift */, + 54AD70D6E03D2031AE1B5A52 /* TimelineReactionsView.swift */, + 33035418BB35754232985871 /* TimelineReadReceiptsView.swift */, + ); + path = Supplementary; + sourceTree = ""; + }; + 15D44FCA9475E660B7F56DB9 /* Timeline */ = { + isa = PBXGroup; + children = ( + 2BA894BC09972DC45E497D37 /* TimelineInteractionHandler.swift */, + 029D5701F80A9AF7167BB4D0 /* TimelineModels.swift */, + 8112846C9D9D3817689CBAF8 /* TimelineTableViewController.swift */, + 97B2ACA28A854E41AE3AC9AD /* TimelineViewModel.swift */, + BB5B00A014307CE37B2812CD /* TimelineViewModelProtocol.swift */, + FDF04D0E125CB4B5C5DB5191 /* View */, + ); + path = Timeline; + sourceTree = ""; + }; 17765802D2723FB2D9A6FF89 /* View */ = { isa = PBXGroup; children = ( @@ -2578,14 +2609,6 @@ path = DeveloperOptionsScreen; sourceTree = ""; }; - 1D8572B713A11CFDBF009B2F /* Replies */ = { - isa = PBXGroup; - children = ( - CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */, - ); - path = Replies; - sourceTree = ""; - }; 1E53A2E18B59B82EE3D8C23C /* View */ = { isa = PBXGroup; children = ( @@ -2769,6 +2792,22 @@ path = View; sourceTree = ""; }; + 308FE2283B9803DBBB05602C /* Style */ = { + isa = PBXGroup; + children = ( + 1D9F148717D74F73BE724434 /* LongPressWithFeedback.swift */, + 0B0E0B55E2EE75AF67029924 /* SwipeToReplyView.swift */, + 2DA4F09CB613C54FDC73AE6A /* ThreadDecorator.swift */, + F5D8FEB1FED10E995CB002F7 /* TimelineBubbleLayout.swift */, + 1DA7E93C2E148B96EF6A8500 /* TimelineItemAccessibilityModifier.swift */, + 0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */, + 753B4C6C0EDDCBF0708DC384 /* TimelineItemSendInfoLabel.swift */, + 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */, + 2DB0E533508094156D8024C3 /* TimelineStyler.swift */, + ); + path = Style; + sourceTree = ""; + }; 31CE4DA53232AA534057F912 /* Mocks */ = { isa = PBXGroup; children = ( @@ -3018,17 +3057,6 @@ path = PinnedEventsTimelineScreen; sourceTree = ""; }; - 3E69187DDC5E781EB96A7BED /* ItemMenu */ = { - isa = PBXGroup; - children = ( - 31352FFC2EF2C353CB7EA376 /* TimelineItemMacContextMenu.swift */, - EC8D3544DE3FABD958BA8F19 /* TimelineItemMenu.swift */, - AF64B3A815D04325F1980E02 /* TimelineItemMenuAction.swift */, - A2751266F17A5BF25DA9227E /* TimelineItemMenuActionProvider.swift */, - ); - path = ItemMenu; - sourceTree = ""; - }; 3EA31CC7012EA2A5653DAFC9 /* Fixtures */ = { isa = PBXGroup; children = ( @@ -3173,15 +3201,6 @@ path = AppLockSetupBiometricsScreen; sourceTree = ""; }; - 45778D52AECD4EB99A289214 /* Polls */ = { - isa = PBXGroup; - children = ( - 50F23B21CF15F9F4BAA0788B /* PollOptionView.swift */, - 317F41A4B5C4F457AF710666 /* PollView.swift */, - ); - path = Polls; - sourceTree = ""; - }; 459B661EA3598F9E709E81A7 /* View */ = { isa = PBXGroup; children = ( @@ -3211,15 +3230,6 @@ path = UserDetailsEditScreen; sourceTree = ""; }; - 4820FFB9F4FDDFD95763D498 /* ReadReceipts */ = { - isa = PBXGroup; - children = ( - 15F30E7AE8A303E8FEC2499E /* ReadReceiptCell.swift */, - BB0A77874B29D79DDFC051AC /* ReadReceiptsSummaryView.swift */, - ); - path = ReadReceipts; - sourceTree = ""; - }; 490F49F5627FBEF3BB8665A3 /* SimpleScreenExample */ = { isa = PBXGroup; children = ( @@ -3327,6 +3337,17 @@ path = RoomDirectorySearch; sourceTree = ""; }; + 505AE6F89590187813390D12 /* ItemMenu */ = { + isa = PBXGroup; + children = ( + A243A6E6207297123E60DE48 /* TimelineItemMacContextMenu.swift */, + 91FFE1F410969ECB23FE9BB2 /* TimelineItemMenu.swift */, + 6F1C3CBBC62C566DDF5E84C1 /* TimelineItemMenuAction.swift */, + A9E6065FC6BC4A1B4C629E08 /* TimelineItemMenuActionProvider.swift */, + ); + path = ItemMenu; + sourceTree = ""; + }; 52AA75722911233E40A3B366 /* Scripts */ = { isa = PBXGroup; children = ( @@ -3526,7 +3547,6 @@ isa = PBXGroup; children = ( B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */, - 0135A608FFAD86E6674EE730 /* RoomScreenInteractionHandler.swift */, C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */, 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */, A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */, @@ -3735,7 +3755,6 @@ F0096BC5DA86AF6B6E5742AC /* RoomPermissionsTests.swift */, B40233F2989AD49906BB310D /* RoomPollsHistoryScreenViewModelTests.swift */, 48FEFF746DB341CDB18D7AAA /* RoomRolesAndPermissionsScreenViewModelTests.swift */, - 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */, 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */, 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */, @@ -3754,6 +3773,7 @@ 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */, 2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */, 9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */, + 6509708F54FC883604DFDC95 /* TimelineViewModelTests.swift */, 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */, 76310030C831D4610A705603 /* URLComponentsTests.swift */, EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */, @@ -3895,16 +3915,7 @@ 422724361B6555364C43281E /* RoomHeaderView.swift */, 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */, 4552D3466B1453F287223ADA /* SwipeRightAction.swift */, - 7023EB4F3B7C7D1FBA68638B /* TimelineItemDebugView.swift */, - 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */, - 3E69187DDC5E781EB96A7BED /* ItemMenu */, 464C6BFAA853DC755B9C1F60 /* PinnedItemsBanner */, - 45778D52AECD4EB99A289214 /* Polls */, - 4820FFB9F4FDDFD95763D498 /* ReadReceipts */, - 1D8572B713A11CFDBF009B2F /* Replies */, - A312471EA62EFB0FD94E60DC /* Style */, - CCD48459CA34A1928EC7A26A /* Supplementary */, - B7D3886505ECC85A06DA8258 /* Timeline */, ); path = View; sourceTree = ""; @@ -3973,6 +3984,14 @@ path = NotificationManager; sourceTree = ""; }; + 7F6468993CB943953DC355A6 /* Replies */ = { + isa = PBXGroup; + children = ( + 1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */, + ); + path = Replies; + sourceTree = ""; + }; 8039515BAA53B7C3275AC64A /* Client */ = { isa = PBXGroup; children = ( @@ -4306,6 +4325,7 @@ ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */, 90F2F8998E5632668B0AD848 /* RoomTimelineItemView.swift */, D0C2D52E36AD614B3C003EF6 /* RoomTimelineItemViewState.swift */, + 5E33FD32BBC44D703C7AE4F9 /* TextBasedRoomTimelineItem.swift */, 75D1D02F7F3AC1122FCFB4F3 /* Items */, ); path = TimelineItems; @@ -4431,22 +4451,6 @@ path = View; sourceTree = ""; }; - A312471EA62EFB0FD94E60DC /* Style */ = { - isa = PBXGroup; - children = ( - 21DC6A9917A7123E7E9A3F81 /* LongPressWithFeedback.swift */, - 49E45C3DC740D3AB9A47FD32 /* SwipeToReplyView.swift */, - 7CA3F8E905DF50BF22ECC18F /* ThreadDecorator.swift */, - E2DCA495ED42D2463DDAA94D /* TimelineBubbleLayout.swift */, - 74C6F3DAD167F972702C8893 /* TimelineItemAccessibilityModifier.swift */, - 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */, - D15447A39D91D2EF536C74DD /* TimelineItemSendInfoLabel.swift */, - 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */, - 892E29C98C4E8182C9037F84 /* TimelineStyler.swift */, - ); - path = Style; - sourceTree = ""; - }; A33CE1B72A29E3931CBEC2A5 /* VoiceMessage */ = { isa = PBXGroup; children = ( @@ -4657,6 +4661,15 @@ path = SettingsScreen; sourceTree = ""; }; + B470504BE2DC95FAC94FDD79 /* ReadReceipts */ = { + isa = PBXGroup; + children = ( + 3D75941CBD7D336F831924EC /* ReadReceiptCell.swift */, + 8063E65441E771200108C558 /* ReadReceiptsSummaryView.swift */, + ); + path = ReadReceipts; + sourceTree = ""; + }; B53CA9BECD3F97805E1432D0 /* HomeScreen */ = { isa = PBXGroup; children = ( @@ -4682,41 +4695,6 @@ path = UserIndicator; sourceTree = ""; }; - B7D3886505ECC85A06DA8258 /* Timeline */ = { - isa = PBXGroup; - children = ( - FC2D505742FDA21FCDC4C18A /* AudioRoomTimelineView.swift */, - CA8F098AE48D958B4257EB24 /* CallInviteRoomTimelineView.swift */, - 584509A363ADF6244DFDB96A /* CallNotificationRoomTimelineView.swift */, - 6E2656184491C505700D2405 /* CollapsibleRoomTimelineView.swift */, - 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */, - 56C1BCB9E83B09A45387FCA2 /* EncryptedRoomTimelineView.swift */, - E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */, - F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */, - D162B2280A15ACAF35360554 /* HighlightedTimelineItemModifier.swift */, - D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */, - 772334731A8BF8E6D90B194D /* LocationRoomTimelineView.swift */, - B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */, - 42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */, - C936FDD017808FE416742D64 /* PollRoomTimelineView.swift */, - B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */, - C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */, - 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */, - ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */, - 612EF972F2A1800682D32C5E /* StickerRoomTimelineView.swift */, - B9227F7495DA43324050A863 /* TextBasedRoomTimelineItem.swift */, - 4E47F18A9A077E351CEA10D4 /* TextBasedRoomTimelineViewProtocol.swift */, - F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */, - F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */, - 27B8315A340B46F98B9C5AF0 /* TimelineTableViewController.swift */, - BDEB27575FEBCF414D4DEE31 /* TimelineView.swift */, - CE47A97726F0675DEE387BF9 /* TypingIndicatorView.swift */, - A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */, - 1941C8817E6B6971BA4415F5 /* VideoRoomTimelineView.swift */, - ); - path = Timeline; - sourceTree = ""; - }; B86CF59E083C82C2A842E4AD /* RoomMemberDetailsScreen */ = { isa = PBXGroup; children = ( @@ -4813,6 +4791,15 @@ path = UITests; sourceTree = ""; }; + C13DBC8C4A879A5A9C781BBD /* Polls */ = { + isa = PBXGroup; + children = ( + A6B19D10B102956066AF117B /* PollOptionView.swift */, + 638790D3F915F0909315C47A /* PollView.swift */, + ); + path = Polls; + sourceTree = ""; + }; C17C3586C93F3A314C1CC318 /* MapLibre */ = { isa = PBXGroup; children = ( @@ -4914,16 +4901,35 @@ path = UserSession; sourceTree = ""; }; - CCD48459CA34A1928EC7A26A /* Supplementary */ = { + CC2B160BAFD5FEAC106F89F3 /* TimelineItemViews */ = { isa = PBXGroup; children = ( - 153726EDCE1ACBB3D466A916 /* ReactionsSummaryView.swift */, - D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */, - 6A4C9547BBFEEF30AA11329B /* TimelineItemStatusView.swift */, - 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */, - 1877038D1AD3D5A029F8AE2C /* TimelineReadReceiptsView.swift */, + B2EAFFD44F81F86012D6EC27 /* AudioRoomTimelineView.swift */, + 664ABD745A746C45CB842158 /* CallInviteRoomTimelineView.swift */, + AD6E082B0507FB28F966516A /* CallNotificationRoomTimelineView.swift */, + BFBF273BC2BFB9F3EEFA988B /* CollapsibleRoomTimelineView.swift */, + 44ABA63DBE7F76C58260B43B /* EmoteRoomTimelineView.swift */, + 2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */, + 4F75EF13F49DD2204E760910 /* FileRoomTimelineView.swift */, + C258C9C815272911A5B132C3 /* FormattedBodyText.swift */, + 59B7CC77B82C6C67DE3AD869 /* HighlightedTimelineItemModifier.swift */, + C5599255A6C98EBDA77B76E6 /* ImageRoomTimelineView.swift */, + ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */, + 3F54FA7C5CB7B342EF9B9B2F /* NoticeRoomTimelineView.swift */, + 4E7F7A975514E850A834B29F /* PaginationIndicatorRoomTimelineView.swift */, + AF8548D48512127CCC17C520 /* PollRoomTimelineView.swift */, + 012A284622B32052015F1F89 /* ReadMarkerRoomTimelineView.swift */, + 91C8BD78F7B9247AC57FA1A3 /* RedactedRoomTimelineView.swift */, + 2910422CB628D3B2BBE47449 /* SeparatorRoomTimelineView.swift */, + B2AF1828A5B76B7C371240FE /* StateRoomTimelineView.swift */, + D5338450E6783A576B5C16DD /* StickerRoomTimelineView.swift */, + A6EA0D8B0BBD8805F7D5A133 /* TextBasedRoomTimelineViewProtocol.swift */, + 4853C923A1AF43711D025EAF /* TextRoomTimelineView.swift */, + 44ECC9D66400727DFFEE12E8 /* TimelineStartRoomTimelineView.swift */, + C9E535B3388755B65C34CD10 /* UnsupportedRoomTimelineView.swift */, + ED0CBEAB5F796BEFBAF7BB6A /* VideoRoomTimelineView.swift */, ); - path = Supplementary; + path = TimelineItemViews; sourceTree = ""; }; CE2FBFD64A89F5DBE4EB30DB /* Layout */ = { @@ -5121,6 +5127,7 @@ 2565414373E6F68005966B8E /* SecureBackup */, 70B74A432C241E56A7ACE610 /* Settings */, EC4545C7E37E8294D3FE6800 /* StartChatScreen */, + 15D44FCA9475E660B7F56DB9 /* Timeline */, 93C7520ED23C9598BB144DBB /* UserProfileScreen */, ); path = Screens; @@ -5360,6 +5367,24 @@ path = Audio; sourceTree = ""; }; + FDF04D0E125CB4B5C5DB5191 /* View */ = { + isa = PBXGroup; + children = ( + BCDA016D05107DED3B9495CB /* TimelineItemDebugView.swift */, + D53FCCE44F96E0BC411A6CF0 /* TimelineSenderAvatarView.swift */, + 93C713D124FE915ABF47A6B7 /* TimelineView.swift */, + 81F0325E252B057FAEEE1B2D /* TypingIndicatorView.swift */, + 505AE6F89590187813390D12 /* ItemMenu */, + C13DBC8C4A879A5A9C781BBD /* Polls */, + B470504BE2DC95FAC94FDD79 /* ReadReceipts */, + 7F6468993CB943953DC355A6 /* Replies */, + 308FE2283B9803DBBB05602C /* Style */, + 13CC0A77B6E5EEDF881C5E8B /* Supplementary */, + CC2B160BAFD5FEAC106F89F3 /* TimelineItemViews */, + ); + path = View; + sourceTree = ""; + }; FFD7C58CA6A7D6BBC2F584B5 /* JoinRoomScreen */ = { isa = PBXGroup; children = ( @@ -5984,7 +6009,6 @@ 2335D1AB954C151FD8779F45 /* RoomPermissionsTests.swift in Sources */, 7B1605C6FFD4D195F264A684 /* RoomPollsHistoryScreenViewModelTests.swift in Sources */, 84C631E734FD2555B39B681C /* RoomRolesAndPermissionsScreenViewModelTests.swift in Sources */, - 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, CC0D088F505F33A20DC5590F /* RoomStateEventStringBuilderTests.swift in Sources */, 15913A5B07118C1268A840E4 /* RoomSummaryTests.swift in Sources */, 7691233E3572A9173FD96CB3 /* SecureBackupKeyBackupScreenViewModelTests.swift in Sources */, @@ -6004,6 +6028,7 @@ E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */, 3A7DD0D13B0FB8876D69D829 /* TextBasedRoomTimelineTests.swift in Sources */, 0D4EB2ABAA5FE8CB10FDBCB8 /* TimelineItemFactoryTests.swift in Sources */, + 2F6207CB5C4715FE313B1E95 /* TimelineViewModelTests.swift in Sources */, 282A5F3375DDC774AE09B0C3 /* TracingConfigurationTests.swift in Sources */, 8E650379587C31D7912ED67B /* UNNotification+Creator.swift in Sources */, AF33B9044498211C3D82F1E1 /* UNTextInputNotificationResponse+Creator.swift in Sources */, @@ -6116,7 +6141,7 @@ 5992EF10AA157EBD97D88910 /* AudioRecorderState.swift in Sources */, F8E725D42023ECA091349245 /* AudioRoomTimelineItem.swift in Sources */, 88F348E2CB14FF71CBBB665D /* AudioRoomTimelineItemContent.swift in Sources */, - E62EC30B39354A391E32A126 /* AudioRoomTimelineView.swift in Sources */, + 7BD2123144A32F082CECC108 /* AudioRoomTimelineView.swift in Sources */, 9278EC51D24E57445B290521 /* AudioSessionProtocol.swift in Sources */, 67E9926C4572C54F59FCA91A /* AuthenticationFlowCoordinator.swift in Sources */, 9847B056C1A216C314D21E68 /* AuthenticationService.swift in Sources */, @@ -6155,9 +6180,9 @@ E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */, 6BAD956B909A6E29F6CC6E7C /* ButtonStyle.swift in Sources */, 01B63F1A04A276B39AC17014 /* CallInviteRoomTimelineItem.swift in Sources */, - F7048AD79361405AA95F2B3B /* CallInviteRoomTimelineView.swift in Sources */, + 3D72F5F9109AAA257542456B /* CallInviteRoomTimelineView.swift in Sources */, E5AB28123E2488F97E953AC0 /* CallNotificationRoomTimelineItem.swift in Sources */, - 53A795964991B06A672B4AAD /* CallNotificationRoomTimelineView.swift in Sources */, + 8A5064CAC8E5F3B18645621D /* CallNotificationRoomTimelineView.swift in Sources */, D19A748E95E2FAB2940570F0 /* CallScreen.swift in Sources */, 763D69741D58D2B650BC1FC9 /* CallScreenCoordinator.swift in Sources */, B7C9E07F4F9CCC8DD7156A20 /* CallScreenModels.swift in Sources */, @@ -6173,7 +6198,7 @@ DDFBDEE1DC32BDD5488F898C /* ClientProxyMock.swift in Sources */, 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */, 0C797CD650DFD2876BEC5173 /* CollapsibleReactionLayout.swift in Sources */, - 9FAF6DA7E8E85C9699757764 /* CollapsibleRoomTimelineView.swift in Sources */, + 78A3D84BA47DAC69B4D0A34C /* CollapsibleRoomTimelineView.swift in Sources */, 0DC815CA24E1BD7F408F37D3 /* CollapsibleTimelineItem.swift in Sources */, 663E198678778F7426A9B27D /* Collection.swift in Sources */, 24B7CD41342C143117ADA768 /* Comparable.swift in Sources */, @@ -6236,10 +6261,10 @@ FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */, 5D27B6537591471A42C89027 /* EmoteRoomTimelineItem.swift in Sources */, 8B41D0357B91CD3B6F6A3BCA /* EmoteRoomTimelineItemContent.swift in Sources */, - 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */, + 661EF50C1F7D4B0BC8A7AAE3 /* EmoteRoomTimelineView.swift in Sources */, 8B1D5CE017EEC734CF5FE130 /* Encodable.swift in Sources */, 4C5A638DAA8AF64565BA4866 /* EncryptedRoomTimelineItem.swift in Sources */, - B5903E48CF43259836BF2DBF /* EncryptedRoomTimelineView.swift in Sources */, + 709A9B52FC26B4CB86B8B020 /* EncryptedRoomTimelineView.swift in Sources */, 03CDCA6243F89B194E3FAD17 /* EncryptionAuthenticity.swift in Sources */, FBD402E3170EB1ED0D1AA672 /* EncryptionKeyProvider.swift in Sources */, 46A6DB0F78FB399BD59E2D41 /* EncryptionKeyProviderProtocol.swift in Sources */, @@ -6260,10 +6285,10 @@ 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */, D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */, 37D789F24199B32E3FD1AA7B /* FileRoomTimelineItemContent.swift in Sources */, - 1F04C63D4FA95948E3F52147 /* FileRoomTimelineView.swift in Sources */, + 64EE9D2CF7AD02EE53983CE1 /* FileRoomTimelineView.swift in Sources */, F5D2270B5021D521C0D22E11 /* FlowCoordinatorProtocol.swift in Sources */, 18E3786918486D4C9726BC84 /* FormButtonStyles.swift in Sources */, - A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */, + 4D0F4385B7DDB68C66C78857 /* FormattedBodyText.swift in Sources */, 7807B1DEE32617896886A8E5 /* FormattingToolbar.swift in Sources */, 46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */, F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */, @@ -6276,7 +6301,7 @@ 55D18AA4F4A2257642EBDB94 /* GlobalSearchScreenViewModel.swift in Sources */, E32A18802EB37EEE3EF7B965 /* GlobalSearchScreenViewModelProtocol.swift in Sources */, D4D5595C4A2A702CFF4E94FF /* HeroImage.swift in Sources */, - 7AE82514D96C725F8BDD0ED4 /* HighlightedTimelineItemModifier.swift in Sources */, + 0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */, 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */, 62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */, 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */, @@ -6302,7 +6327,7 @@ 85813D87DDD7F67A46BD9AF7 /* ImageProviderProtocol.swift in Sources */, 7CD16990BA843BE9ED639129 /* ImageRoomTimelineItem.swift in Sources */, B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */, - D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */, + E2D57361B835E4D2230960E6 /* ImageRoomTimelineView.swift in Sources */, B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */, BFEB24336DFD5F196E6F3456 /* IntentionalMentions.swift in Sources */, 2AAB2A77F1762A2648078A30 /* InteractiveQuickLook.swift in Sources */, @@ -6336,7 +6361,7 @@ B5479997ECC516C121E6625E /* LocationMarkerView.swift in Sources */, D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */, 854E82E064BA53CD0BC45600 /* LocationRoomTimelineItemContent.swift in Sources */, - D9473FC9B077A6EDB7A12001 /* LocationRoomTimelineView.swift in Sources */, + 973C48F9E4EFB808F61BE401 /* LocationRoomTimelineView.swift in Sources */, 29491EE7AE37E239E839C5A3 /* LocationSharingScreenModels.swift in Sources */, 36DE961B784087D5E18EF9BA /* LogViewerScreen.swift in Sources */, CF38B70D8C6DD42C00A56A27 /* LogViewerScreenCoordinator.swift in Sources */, @@ -6350,7 +6375,7 @@ C5A07E2D88BE7D51DCECD166 /* LoginScreenModels.swift in Sources */, BDA68E8D95B2B24B28825B8B /* LoginScreenViewModel.swift in Sources */, A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */, - 4362C770C7E05ADC750E5070 /* LongPressWithFeedback.swift in Sources */, + B10F7D5C237417DA160F4603 /* LongPressWithFeedback.swift in Sources */, B94368839BDB69172E28E245 /* MXLog.swift in Sources */, C1D0AB8222D7BAFC9AF9C8C0 /* MapLibreMapView.swift in Sources */, C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */, @@ -6406,7 +6431,7 @@ 865DD5CA474C6AE6C2BC008E /* NetworkMonitorProtocol.swift in Sources */, 0C58A846F61949B1D545D661 /* NoticeRoomTimelineItem.swift in Sources */, 9408CE8B8865C0C8DD4C9869 /* NoticeRoomTimelineItemContent.swift in Sources */, - 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */, + 7F825CBD857D65DC986087BA /* NoticeRoomTimelineView.swift in Sources */, 3F70E237CE4C3FAB02FC227F /* NotificationConstants.swift in Sources */, CE9530A4CA661E090635C2F2 /* NotificationItemProxy.swift in Sources */, FD4C21F8DA1E273DE94FCD1A /* NotificationItemProxyProtocol.swift in Sources */, @@ -6444,7 +6469,7 @@ F7D709D7ECABE46641BB8B6B /* PHGPostHogProtocol.swift in Sources */, 847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */, 7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */, - 764AFCC225B044CF5F9B41E5 /* PaginationIndicatorRoomTimelineView.swift in Sources */, + BC7CA1379D7C24F47B1B8B7E /* PaginationIndicatorRoomTimelineView.swift in Sources */, 962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */, EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */, 899359A4D1147601F6C4E364 /* PillConstants.swift in Sources */, @@ -6471,10 +6496,10 @@ 151D2477F75782C8702F2873 /* PollInteractionHandler.swift in Sources */, 915B4CDAF220D9AEB4047D45 /* PollInteractionHandlerProtocol.swift in Sources */, 16CBD087038DE3815CDA512C /* PollMock.swift in Sources */, - F50A6FCE26714E27FE5495DD /* PollOptionView.swift in Sources */, + 4716587A9BA69ED8FD1B986B /* PollOptionView.swift in Sources */, 864C0D3A4077BF433DBC691F /* PollRoomTimelineItem.swift in Sources */, - 153E22E8227F46545E5D681C /* PollRoomTimelineView.swift in Sources */, - 1EC6D1B58B24369734CD62BA /* PollView.swift in Sources */, + 128FFD8A3D85845F9A927F47 /* PollRoomTimelineView.swift in Sources */, + 1307268DC41730E5BCF7D9A0 /* PollView.swift in Sources */, DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */, FD4DEC88210F35C35B2FB386 /* ProcessInfo.swift in Sources */, 69DE29C3E3180BB17D840690 /* ProgressCursorModifier.swift in Sources */, @@ -6489,15 +6514,15 @@ C8C7AF33AADF88B306CD2695 /* QRCodeLoginService.swift in Sources */, BB04B1D8E7401C90506D401E /* QRCodeLoginServiceProtocol.swift in Sources */, FDD5B4B616D9FF4DE3E9A418 /* QRCodeScannerView.swift in Sources */, - 9095B9E40DB5CF8BA26CE0D8 /* ReactionsSummaryView.swift in Sources */, + C9A631FD968249B4BA0B7B3C /* ReactionsSummaryView.swift in Sources */, 743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */, - 8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */, + 2BBC0EB1E07963810A5D7423 /* ReadMarkerRoomTimelineView.swift in Sources */, 91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */, - F28BE69B7D35C8C97BAE1F2D /* ReadReceiptCell.swift in Sources */, - D1E29F345F1220E1AF1BE9DF /* ReadReceiptsSummaryView.swift in Sources */, + ABD29E06DD1224812E750AF8 /* ReadReceiptCell.swift in Sources */, + 1653275750CE11F5CE94DDFD /* ReadReceiptsSummaryView.swift in Sources */, C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */, AF19D65A9C60C6B2646F3210 /* RedactedRoomTimelineItem.swift in Sources */, - 13853973A5E24374FCEDE8A3 /* RedactedRoomTimelineView.swift in Sources */, + 8CFDA5F1562479CB3A34D277 /* RedactedRoomTimelineView.swift in Sources */, C413D36D44F89DE63D3ADFA4 /* ReportContentScreen.swift in Sources */, C1A5C386319835FB0C77736B /* ReportContentScreenCoordinator.swift in Sources */, 46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */, @@ -6594,7 +6619,6 @@ F8F47CE757EE656905F01F2C /* RoomRolesAndPermissionsScreenViewModelProtocol.swift in Sources */, C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */, A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */, - 47FF70C051A991FB65CDBCF3 /* RoomScreenInteractionHandler.swift in Sources */, 352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */, 7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */, 617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */, @@ -6654,7 +6678,7 @@ 7BF368A78E6D9AFD222F25AF /* SecureBackupScreenViewModel.swift in Sources */, AC90434798E7894370E80E66 /* SecureBackupScreenViewModelProtocol.swift in Sources */, 14E99D27628B1A6F0CB46FEA /* SeparatorRoomTimelineItem.swift in Sources */, - 49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */, + 5341D48F833E3E30F16FA2A3 /* SeparatorRoomTimelineView.swift in Sources */, 2E8C6672D0EE7D5B1BEDB8E2 /* ServerConfirmationScreen.swift in Sources */, 5894C2514400A4FBC9327632 /* ServerConfirmationScreenCoordinator.swift in Sources */, 401BB28CD6B7DD6B4E7863E7 /* ServerConfirmationScreenModels.swift in Sources */, @@ -6696,20 +6720,20 @@ 4A85928E27D4C1A548A06EE9 /* StartChatScreenViewModel.swift in Sources */, B402708F8728DD0DB7C324E2 /* StartChatScreenViewModelProtocol.swift in Sources */, CB6BCBF28E4B76EA08C2926D /* StateRoomTimelineItem.swift in Sources */, - EE8A37E2A1A77DE5CF941632 /* StateRoomTimelineView.swift in Sources */, + 3B277D9538090766DA6C4566 /* StateRoomTimelineView.swift in Sources */, B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */, B1069F361E604D5436AE9FFD /* StaticLocationScreen.swift in Sources */, 1AB3D8563AB12635250A6A6E /* StaticLocationScreenCoordinator.swift in Sources */, DFD5AA8688A34C72D48AF3B1 /* StaticLocationScreenViewModel.swift in Sources */, 5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */, C58E305C380D3ADDF7912180 /* StickerRoomTimelineItem.swift in Sources */, - F32B271F60531BE92C6E62A1 /* StickerRoomTimelineView.swift in Sources */, + 197441F1EF23A5DABACCA79F /* StickerRoomTimelineView.swift in Sources */, 2F94054F50E312AF30BE07F3 /* String.swift in Sources */, 7640A4B412CACF15D143CCD4 /* Strings+SAS.swift in Sources */, A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */, 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */, FF34BF2AF731340AF9414A18 /* SwipeRightAction.swift in Sources */, - 0DCDF49AB95F75BFC8B1879C /* SwipeToReplyView.swift in Sources */, + 3B98049F56025726FB646ABD /* SwipeToReplyView.swift in Sources */, E290C78E7F09F47FD2662986 /* Task.swift in Sources */, 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */, 47305C0911C9E1AA774A4000 /* TemplateScreenCoordinator.swift in Sources */, @@ -6717,42 +6741,46 @@ 275EDE8849A2AC1D9309ED7C /* TemplateScreenViewModel.swift in Sources */, 2C4C750D0039AFABDF24236C /* TemplateScreenViewModelProtocol.swift in Sources */, 642DF13C49ED4121C148230E /* TestablePreview.swift in Sources */, - 8317E1314C00DCCC99D30DA8 /* TextBasedRoomTimelineItem.swift in Sources */, - A2A5AB2E8B3F5CA769E531FA /* TextBasedRoomTimelineViewProtocol.swift in Sources */, + 887AC93C523AEFB640EA5EC8 /* TextBasedRoomTimelineItem.swift in Sources */, + 4FE688FE9375B2FBF424146A /* TextBasedRoomTimelineViewProtocol.swift in Sources */, BB784A02BADB03C820617A46 /* TextRoomTimelineItem.swift in Sources */, 53F1196F9C69512306A2693F /* TextRoomTimelineItemContent.swift in Sources */, - 5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */, - 5B2D1210B40570D87B11BD3B /* ThreadDecorator.swift in Sources */, - 43EF6D8E694F54C5471BF5F3 /* TimelineBubbleLayout.swift in Sources */, - 5D2AF8C0DF872E7985F8FE54 /* TimelineDeliveryStatusView.swift in Sources */, - 6B05AA5D9BBCD6D8D63B80EB /* TimelineItemAccessibilityModifier.swift in Sources */, - 157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */, - FBCCF1EA25A071324FCD8544 /* TimelineItemDebugView.swift in Sources */, - B0BA59A46ACCF0A3ECBBB7E0 /* TimelineItemMacContextMenu.swift in Sources */, - 62833C090D599023D92A0424 /* TimelineItemMenu.swift in Sources */, - F541922A5B28C995E0BDB4E7 /* TimelineItemMenuAction.swift in Sources */, - 71C532CDC9995236FC1B6EE6 /* TimelineItemMenuActionProvider.swift in Sources */, + D8CFA0EE46376F9FF04EEE45 /* TextRoomTimelineView.swift in Sources */, + 71643093F87153F633A1B025 /* ThreadDecorator.swift in Sources */, + 2AED12987603157C32C2114D /* TimelineBubbleLayout.swift in Sources */, + 798BF3072137833FBD3F4C96 /* TimelineDeliveryStatusView.swift in Sources */, + 109AEB7D33C4497727AFB87F /* TimelineInteractionHandler.swift in Sources */, + E6FA87F773424B27614B23E9 /* TimelineItemAccessibilityModifier.swift in Sources */, + 79959F8E45C3749997482A7F /* TimelineItemBubbledStylerView.swift in Sources */, + A808DC3F72D15C6C5A52317E /* TimelineItemDebugView.swift in Sources */, + C0B97FFEC0083F3A36609E61 /* TimelineItemMacContextMenu.swift in Sources */, + 6C98153D60FF9B648C166C27 /* TimelineItemMenu.swift in Sources */, + AE07F215EBC2B9CBF17AA54B /* TimelineItemMenuAction.swift in Sources */, + 12CD8B5CC30A05061228BF9E /* TimelineItemMenuActionProvider.swift in Sources */, 1C815DD79B401DEBA2914773 /* TimelineItemMock.swift in Sources */, 440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */, 9586E90A447C4896C0CA3A8E /* TimelineItemReplyDetails.swift in Sources */, - 57B9562E6FE788FC172D4AAF /* TimelineItemSendInfoLabel.swift in Sources */, + F3ECA377FF77E81A4F1FA062 /* TimelineItemSendInfoLabel.swift in Sources */, 1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */, - A680F54935A6ADEA4ED6C38F /* TimelineItemStatusView.swift in Sources */, + EFBBD44C0A16F017C32D2099 /* TimelineItemStatusView.swift in Sources */, 562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */, + B818580464CFB5400A3EF6AE /* TimelineModels.swift in Sources */, E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */, 2FEC6652055984389CE1BBEC /* TimelineProxyProtocol.swift in Sources */, - 9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */, - 49814A48470F347426513B07 /* TimelineReadReceiptsView.swift in Sources */, - 2A90DD14DE5C891BFA433950 /* TimelineReplyView.swift in Sources */, - ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */, + 8446C2A7ECEFDA79F622725F /* TimelineReactionsView.swift in Sources */, + 4DAEE2468669848B6C9F55B4 /* TimelineReadReceiptsView.swift in Sources */, + 6EB46C92ECFEAE71959D91D2 /* TimelineReplyView.swift in Sources */, + 9FBE1FB20171012260A32492 /* TimelineSenderAvatarView.swift in Sources */, C4FE0E11A907C8999F92D5A8 /* TimelineStartRoomTimelineItem.swift in Sources */, - 6FF51EB400DBA0668FC38B97 /* TimelineStartRoomTimelineView.swift in Sources */, - 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */, - FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */, - 2B1E080B32167AE9EFC763A2 /* TimelineTableViewController.swift in Sources */, - 57E115A8C33E599DE564F8C3 /* TimelineView.swift in Sources */, + 785613C0C092B532198EB3BB /* TimelineStartRoomTimelineView.swift in Sources */, + F2D5C0E1351DA7BD16867629 /* TimelineStyle.swift in Sources */, + 7A0D335D38ECA095A575B4F7 /* TimelineStyler.swift in Sources */, + 6583A95947E78767736CB51A /* TimelineTableViewController.swift in Sources */, + 67EFF46180B939CBF389AECD /* TimelineView.swift in Sources */, + 98EE4259A4A49BC757BA442C /* TimelineViewModel.swift in Sources */, + F8B2F5CBCF2A0E0798E8D646 /* TimelineViewModelProtocol.swift in Sources */, E37044401D9951D6C02C0855 /* TracingConfiguration.swift in Sources */, - 6D6E651ACACE27E9C5690818 /* TypingIndicatorView.swift in Sources */, + 298F9EC30E918F12AB7F1EE8 /* TypingIndicatorView.swift in Sources */, 36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */, A37EED79941AD3B7140B3822 /* UIDevice.swift in Sources */, 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */, @@ -6767,7 +6795,7 @@ 18867F4F1C8991EEC56EA932 /* UTType.swift in Sources */, 84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */, 34C752A73717C691582DC6C7 /* UnsupportedRoomTimelineItem.swift in Sources */, - E1F446C6B78A3A0FEA15079C /* UnsupportedRoomTimelineView.swift in Sources */, + D8459AAD6969B1431ECBE990 /* UnsupportedRoomTimelineView.swift in Sources */, 7A71AEF419904209BB8C2833 /* UserAgentBuilder.swift in Sources */, 7D58B4F46CAA9A7C3E4C6A30 /* UserDetailsEditScreen.swift in Sources */, A64B52D9F73F9A6B95AF24FE /* UserDetailsEditScreenCoordinator.swift in Sources */, @@ -6804,7 +6832,7 @@ AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */, F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */, 1A83DD22F3E6F76B13B6E2F9 /* VideoRoomTimelineItemContent.swift in Sources */, - 64F43D7390DA2A0AFD6BA911 /* VideoRoomTimelineView.swift in Sources */, + 2CA61BB208CD82EBDB58CD13 /* VideoRoomTimelineView.swift in Sources */, 6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */, 1318721F4E5F307586D98112 /* VoiceMessageButton.swift in Sources */, 4681820102DAC8BA586357D4 /* VoiceMessageCache.swift in Sources */, diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index bbd3d67eb..66e57b04b 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -471,7 +471,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { break case (.room, .presentPinnedEventsTimeline, .pinnedEventsTimeline): - presentPinnedEventsTimeline() + Task { await self.presentPinnedEventsTimeline() } case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .room): break @@ -481,7 +481,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { break case (.roomDetails, .presentPinnedEventsTimeline, .pinnedEventsTimeline): - presentPinnedEventsTimeline() + Task { await self.presentPinnedEventsTimeline() } case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .roomDetails): break @@ -965,9 +965,24 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } - private func presentPinnedEventsTimeline() { + private func presentPinnedEventsTimeline() async { + let userID = userSession.clientProxy.userID + let timelineItemFactory = RoomTimelineItemFactory(userID: userID, + encryptionAuthenticityEnabled: appSettings.timelineItemAuthenticityEnabled, + attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), + stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) + + guard let timelineController = await roomTimelineControllerFactory.buildRoomPinnedTimelineController(roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) else { + fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil") + } + let stackCoordinator = NavigationStackCoordinator() - let coordinator = PinnedEventsTimelineScreenCoordinator(parameters: .init()) + let coordinator = PinnedEventsTimelineScreenCoordinator(parameters: .init(roomProxy: roomProxy, + timelineController: timelineController, + mediaProvider: userSession.mediaProvider, + mediaPlayerProvider: MediaPlayerProvider(), + voiceMessageMediaManager: userSession.voiceMessageMediaManager, + appMediator: appMediator)) coordinator.actions .sink { [weak self] action in diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 6fb962d28..aa7eb4581 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -11508,6 +11508,76 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue } } + //MARK: - buildRoomPinnedTimelineController + + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = 0 + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryCallsCount: Int { + get { + if Thread.isMainThread { + return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = newValue + } + } + } + } + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryCalled: Bool { + return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryCallsCount > 0 + } + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReceivedArguments: (roomProxy: RoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol)? + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReceivedInvocations: [(roomProxy: RoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol)] = [] + + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue: RoomTimelineControllerProtocol? + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReturnValue: RoomTimelineControllerProtocol? { + get { + if Thread.isMainThread { + return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue + } else { + var returnValue: RoomTimelineControllerProtocol?? = nil + DispatchQueue.main.sync { + returnValue = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue = newValue + } + } + } + } + var buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryClosure: ((RoomProxyProtocol, RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol?)? + + func buildRoomPinnedTimelineController(roomProxy: RoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? { + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryCallsCount += 1 + buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReceivedArguments = (roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) + DispatchQueue.main.async { + self.buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReceivedInvocations.append((roomProxy: roomProxy, timelineItemFactory: timelineItemFactory)) + } + if let buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryClosure = buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryClosure { + return await buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryClosure(roomProxy, timelineItemFactory) + } else { + return buildRoomPinnedTimelineControllerRoomProxyTimelineItemFactoryReturnValue + } + } } class RoomTimelineProviderMock: RoomTimelineProviderProtocol { var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> { diff --git a/ElementX/Sources/Other/Pills/MessageText.swift b/ElementX/Sources/Other/Pills/MessageText.swift index 35a9d1c77..aab28bace 100644 --- a/ElementX/Sources/Other/Pills/MessageText.swift +++ b/ElementX/Sources/Other/Pills/MessageText.swift @@ -18,7 +18,7 @@ import MatrixRustSDK import SwiftUI final class MessageTextView: UITextView, PillAttachmentViewProviderDelegate { - var roomContext: RoomScreenViewModel.Context? + var timelineContext: TimelineViewModel.Context? var updateClosure: (() -> Void)? private var pillViews = NSHashTable.weakObjects() @@ -54,7 +54,7 @@ final class MessageTextView: UITextView, PillAttachmentViewProviderDelegate { struct MessageText: UIViewRepresentable { @Environment(\.openURL) private var openURLAction - @Environment(\.roomContext) private var viewModel + @Environment(\.timelineContext) private var viewModel @State private var computedSizes = [Double: CGSize]() @State var attributedString: AttributedString { @@ -66,7 +66,7 @@ struct MessageText: UIViewRepresentable { func makeUIView(context: Context) -> MessageTextView { // Need to use TextKit 1 for mentions let textView = MessageTextView(usingTextLayoutManager: false) - textView.roomContext = viewModel + textView.timelineContext = viewModel textView.updateClosure = { [weak textView] in guard let textView else { return } do { @@ -197,7 +197,7 @@ struct MessageText_Previews: PreviewProvider, TestablePreview { static var attachmentPreview: some View { MessageText(attributedString: attributedStringWithAttachment) .border(Color.purple) - .environmentObject(RoomScreenViewModel.mock.context) + .environmentObject(TimelineViewModel.mock.context) } static var previews: some View { diff --git a/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift b/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift index 0959e9607..35cd4bfdb 100644 --- a/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift +++ b/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift @@ -21,7 +21,7 @@ import UIKit import WysiwygComposer protocol PillAttachmentViewProviderDelegate: AnyObject { - var roomContext: RoomScreenViewModel.Context? { get } + var timelineContext: TimelineViewModel.Context? { get } func registerPillView(_ pillView: UIView) func invalidateTextAttachmentsDisplay() @@ -56,9 +56,9 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider, NSSecureCo // The mock viewModel simulates the loading logic for testing purposes context = PillContext.mock(type: .loadUser(isOwn: false)) imageProvider = MockMediaProvider() - } else if let roomContext = delegate?.roomContext { - context = PillContext(roomContext: roomContext, data: pillData) - imageProvider = roomContext.imageProvider + } else if let timelineContext = delegate?.timelineContext { + context = PillContext(timelineContext: timelineContext, data: pillData) + imageProvider = timelineContext.imageProvider } else { MXLog.failure("[PillAttachmentViewProvider]: missing room context") return @@ -93,21 +93,21 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider, NSSecureCo } final class ComposerMentionDisplayHelper: MentionDisplayHelper { - weak var roomContext: RoomScreenViewModel.Context? + weak var timelineContext: TimelineViewModel.Context? - init(roomContext: RoomScreenViewModel.Context) { - self.roomContext = roomContext + init(timelineContext: TimelineViewModel.Context) { + self.timelineContext = timelineContext } @MainActor static var mock: Self { - Self(roomContext: RoomScreenViewModel.mock.context) + Self(timelineContext: TimelineViewModel.mock.context) } } extension WysiwygTextView: PillAttachmentViewProviderDelegate { - var roomContext: RoomScreenViewModel.Context? { - (mentionDisplayHelper as? ComposerMentionDisplayHelper)?.roomContext + var timelineContext: TimelineViewModel.Context? { + (mentionDisplayHelper as? ComposerMentionDisplayHelper)?.timelineContext } func invalidateTextAttachmentsDisplay() { } diff --git a/ElementX/Sources/Other/Pills/PillContext.swift b/ElementX/Sources/Other/Pills/PillContext.swift index 49a1a618f..455062dfa 100644 --- a/ElementX/Sources/Other/Pills/PillContext.swift +++ b/ElementX/Sources/Other/Pills/PillContext.swift @@ -28,11 +28,11 @@ final class PillContext: ObservableObject { private var cancellable: AnyCancellable? - init(roomContext: RoomScreenViewModel.Context, data: PillTextAttachmentData) { + init(timelineContext: TimelineViewModel.Context, data: PillTextAttachmentData) { switch data.type { case let .user(id): - let isOwnMention = id == roomContext.viewState.ownUserID - if let profile = roomContext.viewState.members[id] { + let isOwnMention = id == timelineContext.viewState.ownUserID + if let profile = timelineContext.viewState.members[id] { var name = id if let displayName = profile.displayName { name = "@\(displayName)" @@ -40,7 +40,7 @@ final class PillContext: ObservableObject { viewState = PillViewState(isOwnMention: isOwnMention, displayText: name) } else { viewState = PillViewState(isOwnMention: isOwnMention, displayText: id) - cancellable = roomContext.$viewState.sink { [weak self] viewState in + cancellable = timelineContext.$viewState.sink { [weak self] viewState in guard let self else { return } @@ -73,7 +73,7 @@ extension PillContext { switch type { case .loadUser(let isOwn): pillType = .user(userID: testID) - let viewModel = PillContext(roomContext: RoomScreenViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body))) + let viewModel = PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body))) viewModel.viewState = PillViewState(isOwnMention: isOwn, displayText: testID) Task { try? await Task.sleep(for: .seconds(2)) @@ -82,12 +82,12 @@ extension PillContext { return viewModel case .loadedUser(let isOwn): pillType = .user(userID: "@test:test.com") - let viewModel = PillContext(roomContext: RoomScreenViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body))) + let viewModel = PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body))) viewModel.viewState = PillViewState(isOwnMention: isOwn, displayText: "@Very Very Long Test Display Text") return viewModel case .allUsers: pillType = .allUsers - return PillContext(roomContext: RoomScreenViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body))) + return PillContext(timelineContext: TimelineViewModel.mock.context, data: PillTextAttachmentData(type: pillType, font: .preferredFont(forTextStyle: .body))) } } } diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift index fd886d0e9..dc89320a4 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift @@ -17,7 +17,14 @@ import Combine import SwiftUI -struct PinnedEventsTimelineScreenCoordinatorParameters { } +struct PinnedEventsTimelineScreenCoordinatorParameters { + let roomProxy: RoomProxyProtocol + let timelineController: RoomTimelineControllerProtocol + let mediaProvider: MediaProviderProtocol + let mediaPlayerProvider: MediaPlayerProviderProtocol + let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol + let appMediator: AppMediatorProtocol +} enum PinnedEventsTimelineScreenCoordinatorAction { case dismiss @@ -28,6 +35,7 @@ enum PinnedEventsTimelineScreenCoordinatorAction { final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol { private let parameters: PinnedEventsTimelineScreenCoordinatorParameters private let viewModel: PinnedEventsTimelineScreenViewModelProtocol + private let timelineViewModel: TimelineViewModelProtocol private var cancellables = Set() @@ -40,6 +48,15 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol { self.parameters = parameters viewModel = PinnedEventsTimelineScreenViewModel() + timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy, + timelineController: parameters.timelineController, + mediaProvider: parameters.mediaProvider, + mediaPlayerProvider: parameters.mediaPlayerProvider, + voiceMessageMediaManager: parameters.voiceMessageMediaManager, + userIndicatorController: ServiceLocator.shared.userIndicatorController, + appMediator: parameters.appMediator, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) } func start() { @@ -56,6 +73,6 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol { } func toPresentable() -> AnyView { - AnyView(PinnedEventsTimelineScreen(context: viewModel.context)) + AnyView(PinnedEventsTimelineScreen(context: viewModel.context, timelineContext: timelineViewModel.context)) } } diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift index 912efee14..269b96a2d 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift @@ -19,6 +19,7 @@ import SwiftUI struct PinnedEventsTimelineScreen: View { @ObservedObject var context: PinnedEventsTimelineScreenViewModel.Context + @ObservedObject var timelineContext: TimelineViewModel.Context var body: some View { content @@ -28,22 +29,29 @@ struct PinnedEventsTimelineScreen: View { .background(.compound.bgCanvasDefault) } + @ViewBuilder private var content: some View { - // TODO: Implement switching between empty state and timeline - VStack(spacing: 16) { - HeroImage(icon: \.pin, style: .normal) - Text(L10n.screenPinnedTimelineEmptyStateHeadline) - .font(.compound.headingSMSemibold) - .foregroundStyle(.compound.textPrimary) - .multilineTextAlignment(.center) - Text(L10n.screenPinnedTimelineEmptyStateDescription(L10n.actionPin)) - .font(.compound.bodyMD) - .foregroundStyle(.compound.textSecondary) - .multilineTextAlignment(.center) - Spacer() + if timelineContext.viewState.timelineViewState.itemsDictionary.isEmpty { + VStack(spacing: 16) { + HeroImage(icon: \.pin, style: .normal) + Text(L10n.screenPinnedTimelineEmptyStateHeadline) + .font(.compound.headingSMSemibold) + .foregroundStyle(.compound.textPrimary) + .multilineTextAlignment(.center) + Text(L10n.screenPinnedTimelineEmptyStateDescription(L10n.actionPin)) + .font(.compound.bodyMD) + .foregroundStyle(.compound.textSecondary) + .multilineTextAlignment(.center) + Spacer() + } + .padding(.top, 48) + .padding(.horizontal, 16) + } else { + TimelineView() + .id(timelineContext.viewState.roomID) + .environmentObject(timelineContext) + .environment(\.focussedEventID, timelineContext.viewState.timelineViewState.focussedEvent?.eventID) } - .padding(.top, 48) - .padding(.horizontal, 16) } @ToolbarContentBuilder @@ -60,9 +68,24 @@ struct PinnedEventsTimelineScreen: View { struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = PinnedEventsTimelineScreenViewModel() + static let emptyTimelineViewModel: TimelineViewModel = { + let timelineController = MockRoomTimelineController() + timelineController.timelineItems = [] + return TimelineViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room")), + timelineController: timelineController, + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: UserIndicatorControllerMock(), + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) + }() + static var previews: some View { NavigationStack { - PinnedEventsTimelineScreen(context: viewModel.context) + PinnedEventsTimelineScreen(context: viewModel.context, timelineContext: emptyTimelineViewModel.context) } + .previewDisplayName("Empty") } } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift index 4188d05cc..cbdb9e568 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift @@ -31,13 +31,13 @@ enum ComposerToolbarVoiceMessageAction { } enum ComposerToolbarViewModelAction { - case sendMessage(plain: String, html: String?, mode: RoomScreenComposerMode, intentionalMentions: IntentionalMentions) + case sendMessage(plain: String, html: String?, mode: ComposerMode, intentionalMentions: IntentionalMentions) case editLastMessage case attach(ComposerAttachmentType) case handlePasteOrDrop(provider: NSItemProvider) - case composerModeChanged(mode: RoomScreenComposerMode) + case composerModeChanged(mode: ComposerMode) case composerFocusedChanged(isFocused: Bool) case voiceMessage(ComposerToolbarVoiceMessageAction) @@ -72,7 +72,7 @@ enum ComposerAttachmentType { } struct ComposerToolbarViewState: BindableState { - var composerMode: RoomScreenComposerMode = .default + var composerMode: ComposerMode = .default var composerEmpty = true var suggestions: [SuggestionItem] = [] var audioPlayerState: AudioPlayerState @@ -288,3 +288,61 @@ extension FormatType { } } } + +enum ComposerMode: Equatable { + case `default` + case reply(itemID: TimelineItemIdentifier, replyDetails: TimelineItemReplyDetails, isThread: Bool) + case edit(originalItemId: TimelineItemIdentifier) + case recordVoiceMessage(state: AudioRecorderState) + case previewVoiceMessage(state: AudioPlayerState, waveform: WaveformSource, isUploading: Bool) + + var isEdit: Bool { + switch self { + case .edit: + return true + default: + return false + } + } + + var isTextEditingEnabled: Bool { + switch self { + case .default, .reply, .edit: + return true + case .recordVoiceMessage, .previewVoiceMessage: + return false + } + } + + var isLoadingReply: Bool { + switch self { + case .reply(_, let replyDetails, _): + switch replyDetails { + case .loading: + return true + default: + return false + } + default: + return false + } + } + + var replyEventID: String? { + switch self { + case .reply(let itemID, _, _): + return itemID.eventID + default: + return nil + } + } + + var isComposingNewMessage: Bool { + switch self { + case .default, .reply: + return true + default: + return false + } + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift index c39e48db4..44a38f896 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift @@ -200,8 +200,8 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool } } - func process(roomAction: RoomScreenComposerAction) { - switch roomAction { + func process(timelineAction: TimelineComposerAction) { + switch timelineAction { case .setMode(mode: let mode): if state.composerMode.isComposingNewMessage, mode.isEdit { handleSaveDraft(isVolatile: true) @@ -223,19 +223,23 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool set(mode: .default) set(text: "") } - case .saveDraft: - handleSaveDraft(isVolatile: false) - case .loadDraft: - Task { - guard case let .success(draft) = await draftService.loadDraft(), - let draft else { - return - } - handleLoadDraft(draft) - } } } + func loadDraft() { + Task { + guard case let .success(draft) = await draftService.loadDraft(), + let draft else { + return + } + handleLoadDraft(draft) + } + } + + func saveDraft() { + handleSaveDraft(isVolatile: false) + } + var keyCommands: [WysiwygKeyCommand] { [ .enter { [weak self] in @@ -480,7 +484,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool } } - private func set(mode: RoomScreenComposerMode) { + private func set(mode: ComposerMode) { if state.composerMode.isLoadingReply, state.composerMode.replyEventID != mode.replyEventID { replyLoadingTask?.cancel() } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift index 2cbba8d4e..41ebd6159 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift @@ -15,10 +15,15 @@ // import Combine +import WysiwygComposer // periphery: ignore - markdown protocol protocol ComposerToolbarViewModelProtocol { var actions: AnyPublisher { get } var context: ComposerToolbarViewModelType.Context { get } - func process(roomAction: RoomScreenComposerAction) + var keyCommands: [WysiwygKeyCommand] { get } + + func process(timelineAction: TimelineComposerAction) + func loadDraft() + func saveDraft() } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift index 5514ac92b..def4bacba 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift @@ -24,7 +24,7 @@ typealias PasteHandler = (NSItemProvider) -> Void struct MessageComposer: View { @Binding var plainComposerText: NSAttributedString let composerView: WysiwygComposerView - let mode: RoomScreenComposerMode + let mode: ComposerMode let composerFormattingEnabled: Bool let showResizeGrabber: Bool @Binding var isExpanded: Bool @@ -208,7 +208,7 @@ private struct MessageComposerHeaderLabelStyle: LabelStyle { } struct MessageComposer_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static let replyTypes: [TimelineItemReplyDetails] = [ .loaded(sender: .init(id: "Dave"), @@ -241,7 +241,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview { ] static func messageComposer(_ content: NSAttributedString = .init(string: ""), - mode: RoomScreenComposerMode = .default) -> MessageComposer { + mode: ComposerMode = .default) -> MessageComposer { let viewModel = WysiwygComposerViewModel(minHeight: 22, maxExpandedHeight: 250) viewModel.setMarkdownContent(content.string) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift index 1bbf06235..b127e0894 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift @@ -55,7 +55,7 @@ struct MessageComposerTextField: View { } private struct UITextViewWrapper: UIViewRepresentable { - @Environment(\.roomContext) private var roomContext + @Environment(\.timelineContext) private var timelineContext @Binding var text: NSAttributedString @@ -69,7 +69,7 @@ private struct UITextViewWrapper: UIViewRepresentable { func makeUIView(context: UIViewRepresentableContext) -> UITextView { // Need to use TextKit 1 for mentions let textView = ElementTextView(usingTextLayoutManager: false) - textView.roomContext = roomContext + textView.timelineContext = timelineContext textView.delegate = context.coordinator textView.elementDelegate = context.coordinator @@ -182,7 +182,7 @@ private protocol ElementTextViewDelegate: AnyObject { } private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate { - var roomContext: RoomScreenViewModel.Context? + var timelineContext: TimelineViewModel.Context? weak var elementDelegate: ElementTextViewDelegate? diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 62b1b59ef..94fbe3dab 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -49,8 +49,9 @@ enum RoomScreenCoordinatorAction { } final class RoomScreenCoordinator: CoordinatorProtocol { - private var viewModel: RoomScreenViewModelProtocol - private var composerViewModel: ComposerToolbarViewModel + private var roomViewModel: RoomScreenViewModelProtocol + private var timelineViewModel: TimelineViewModelProtocol + private var composerViewModel: ComposerToolbarViewModelProtocol private var wysiwygViewModel: WysiwygComposerViewModel private var cancellables = Set() @@ -61,31 +62,33 @@ final class RoomScreenCoordinator: CoordinatorProtocol { } init(parameters: RoomScreenCoordinatorParameters) { - let viewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy, - focussedEventID: parameters.focussedEventID, - timelineController: parameters.timelineController, - mediaProvider: parameters.mediaProvider, - mediaPlayerProvider: parameters.mediaPlayerProvider, - voiceMessageMediaManager: parameters.voiceMessageMediaManager, - userIndicatorController: ServiceLocator.shared.userIndicatorController, - appMediator: parameters.appMediator, - appSettings: parameters.appSettings, - analyticsService: ServiceLocator.shared.analytics) - self.viewModel = viewModel + roomViewModel = RoomScreenViewModel() + + timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy, + focussedEventID: parameters.focussedEventID, + timelineController: parameters.timelineController, + mediaProvider: parameters.mediaProvider, + mediaPlayerProvider: parameters.mediaPlayerProvider, + voiceMessageMediaManager: parameters.voiceMessageMediaManager, + userIndicatorController: ServiceLocator.shared.userIndicatorController, + appMediator: parameters.appMediator, + appSettings: parameters.appSettings, + analyticsService: ServiceLocator.shared.analytics) wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight, maxCompressedHeight: ComposerConstant.maxHeight, maxExpandedHeight: ComposerConstant.maxHeight, parserStyle: .elementX) - composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, - completionSuggestionService: parameters.completionSuggestionService, - mediaProvider: parameters.mediaProvider, - mentionDisplayHelper: ComposerMentionDisplayHelper(roomContext: viewModel.context), - analyticsService: ServiceLocator.shared.analytics, - composerDraftService: parameters.composerDraftService) + let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel, + completionSuggestionService: parameters.completionSuggestionService, + mediaProvider: parameters.mediaProvider, + mentionDisplayHelper: ComposerMentionDisplayHelper(timelineContext: timelineViewModel.context), + analyticsService: ServiceLocator.shared.analytics, + composerDraftService: parameters.composerDraftService) + self.composerViewModel = composerViewModel NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification).sink { _ in - viewModel.saveDraft() + composerViewModel.saveDraft() } .store(in: &cancellables) } @@ -93,7 +96,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { // MARK: - Public func start() { - viewModel.actions + timelineViewModel.actions .sink { [weak self] action in guard let self else { return } @@ -123,7 +126,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { 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) + composerViewModel.process(timelineAction: action) case .displayCallScreen: actionsSubject.send(.presentCallScreen) case .displayPinnedEventsTimeline: @@ -136,21 +139,27 @@ final class RoomScreenCoordinator: CoordinatorProtocol { .sink { [weak self] action in guard let self else { return } - viewModel.process(composerAction: action) + timelineViewModel.process(composerAction: action) + } + .store(in: &cancellables) + + roomViewModel.actions + .sink { [weak self] _ in + guard let self else { return } } .store(in: &cancellables) // Loading the draft requires the subscriptions to be set up first otherwise the room won't be be able to propagate the information to the composer. - viewModel.loadDraft() + composerViewModel.loadDraft() } func focusOnEvent(eventID: String) { - Task { await viewModel.focusOnEvent(eventID: eventID) } + Task { await timelineViewModel.focusOnEvent(eventID: eventID) } } func stop() { - viewModel.saveDraft() - viewModel.stop() + composerViewModel.saveDraft() + timelineViewModel.stop() } func toPresentable() -> AnyView { @@ -158,10 +167,12 @@ final class RoomScreenCoordinator: CoordinatorProtocol { wysiwygViewModel: wysiwygViewModel, keyCommands: composerViewModel.keyCommands) - return AnyView(RoomScreen(context: viewModel.context, composerToolbar: composerToolbar) - .onDisappear { [weak self] in - self?.viewModel.saveDraft() - }) + return AnyView(RoomScreen(roomViewModel: roomViewModel, + timelineViewModel: timelineViewModel, + composerToolbar: composerToolbar) + .onDisappear { [weak self] in + self?.composerViewModel.saveDraft() + }) } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 550ffdecb..f356e149e 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -1,5 +1,5 @@ // -// Copyright 2022 New Vector Ltd +// Copyright 2024 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. @@ -14,431 +14,19 @@ // limitations under the License. // -import Combine -import OrderedCollections -import SwiftUI +import Foundation -enum RoomScreenViewModelAction { - case displayRoomDetails - case displayEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set) - case displayReportContent(itemID: TimelineItemIdentifier, senderID: String) - case displayCameraPicker - case displayMediaPicker - case displayDocumentPicker - case displayLocationPicker - case displayPollForm(mode: PollFormMode) - case displayMediaUploadPreviewScreen(url: URL) - case displayRoomMemberDetails(userID: String) - case displayMessageForwarding(forwardingItem: MessageForwardingItem) - case displayLocation(body: String, geoURI: GeoURI, description: String?) - case composer(action: RoomScreenComposerAction) - case displayCallScreen - case displayPinnedEventsTimeline +enum RoomScreenViewModelAction { } + +enum RoomScreenViewAction { } + +struct RoomScreenViewState: BindableState { + var bindings: RoomScreenViewStateBindings } -enum RoomScreenComposerMode: Equatable { - case `default` - case reply(itemID: TimelineItemIdentifier, replyDetails: TimelineItemReplyDetails, isThread: Bool) - case edit(originalItemId: TimelineItemIdentifier) - case recordVoiceMessage(state: AudioRecorderState) - case previewVoiceMessage(state: AudioPlayerState, waveform: WaveformSource, isUploading: Bool) - - var isEdit: Bool { - switch self { - case .edit: - return true - default: - return false - } - } - - var isTextEditingEnabled: Bool { - switch self { - case .default, .reply, .edit: - return true - case .recordVoiceMessage, .previewVoiceMessage: - return false - } - } - - var isLoadingReply: Bool { - switch self { - case .reply(_, let replyDetails, _): - switch replyDetails { - case .loading: - return true - default: - return false - } - default: - return false - } - } - - var replyEventID: String? { - switch self { - case .reply(let itemID, _, _): - return itemID.eventID - default: - return nil - } - } - - var isComposingNewMessage: Bool { - switch self { - case .default, .reply: - return true - default: - return false - } - } -} - -enum RoomScreenViewPollAction { - case selectOption(pollStartID: String, optionID: String) - case end(pollStartID: String) - case edit(pollStartID: String, poll: Poll) -} - -enum RoomScreenAudioPlayerAction { - case playPause(itemID: TimelineItemIdentifier) - case seek(itemID: TimelineItemIdentifier, progress: Double) -} - -enum RoomScreenViewAction { - case itemAppeared(itemID: TimelineItemIdentifier) - case itemDisappeared(itemID: TimelineItemIdentifier) - - case itemTapped(itemID: TimelineItemIdentifier) - case itemSendInfoTapped(itemID: TimelineItemIdentifier) - case toggleReaction(key: String, itemID: TimelineItemIdentifier) - case sendReadReceiptIfNeeded(TimelineItemIdentifier) - case paginateBackwards - case paginateForwards - case scrollToBottom - - case displayTimelineItemMenu(itemID: TimelineItemIdentifier) - case handleTimelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction) - - case displayRoomDetails - case displayRoomMemberDetails(userID: String) - case displayReactionSummary(itemID: TimelineItemIdentifier, key: String) - case displayEmojiPicker(itemID: TimelineItemIdentifier) - case displayReadReceipts(itemID: TimelineItemIdentifier) - case displayCall - - case handlePasteOrDrop(provider: NSItemProvider) - case handlePollAction(RoomScreenViewPollAction) - case handleAudioPlayerAction(RoomScreenAudioPlayerAction) - - /// Focus the timeline onto the specified event ID (switching to a detached timeline if needed). - case focusOnEventID(String) - /// Switch back to a live timeline (from a detached one). - case focusLive - /// The timeline scrolled to reveal the focussed item. - case scrolledToFocussedItem - /// The table view has loaded the first items for a new timeline. - case hasSwitchedTimeline - - case hasScrolled(direction: ScrollDirection) - case tappedPinnedEventsBanner - case viewAllPins -} +struct RoomScreenViewStateBindings { } enum RoomScreenComposerAction { - case setMode(mode: RoomScreenComposerMode) - case setText(plainText: String, htmlText: String?) - case removeFocus - case clear case saveDraft case loadDraft } - -struct RoomScreenViewState: BindableState { - var roomID: String - var roomTitle = "" - var roomAvatar: RoomAvatar - var members: [String: RoomMemberState] = [:] - var typingMembers: [String] = [] - var showLoading = false - var showReadReceipts = false - var isEncryptedOneToOneRoom = false - var timelineViewState: TimelineViewState // check the doc before changing this - - var ownUserID: String - var canCurrentUserRedactOthers = false - var canCurrentUserRedactSelf = false - var canCurrentUserPin = false - var isViewSourceEnabled: Bool - - var isPinningEnabled = false - var lastScrollDirection: ScrollDirection? - - // The `pinnedEventIDs` are used only to determine if an item is already pinned or not. - // It's updated from the room info, so it's faster than using the timeline - var pinnedEventIDs: Set = [] - // This is used to control the banner - var pinnedEventsBannerState: PinnedEventsBannerState = .loading(numbersOfEvents: 0) - - var shouldShowPinnedEventsBanner: Bool { - isPinningEnabled && !pinnedEventsBannerState.isEmpty && lastScrollDirection != .top - } - - var canJoinCall = false - var hasOngoingCall = false - - var bindings: RoomScreenViewStateBindings - - /// A closure providing the associated audio player state for an item in the timeline. - var audioPlayerStateProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> AudioPlayerState?)? -} - -struct RoomScreenViewStateBindings { - var isScrolledToBottom = true - - /// The state of wether reactions listed on the timeline are expanded/collapsed. - /// Key is itemID, value is the collapsed state. - var reactionsCollapsed: [TimelineItemIdentifier: Bool] - - /// A media item that will be previewed with QuickLook. - var mediaPreviewItem: MediaPreviewItem? - - var alertInfo: AlertInfo? - - var debugInfo: TimelineItemDebugInfo? - - var actionMenuInfo: TimelineItemActionMenuInfo? - - var reactionSummaryInfo: ReactionSummaryInfo? - - var readReceiptsSummaryInfo: ReadReceiptSummaryInfo? -} - -struct TimelineItemActionMenuInfo: Equatable, Identifiable { - static func == (lhs: TimelineItemActionMenuInfo, rhs: TimelineItemActionMenuInfo) -> Bool { - lhs.id == rhs.id - } - - let item: EventBasedTimelineItemProtocol - - var id: TimelineItemIdentifier { - item.id - } -} - -struct ReactionSummaryInfo: Identifiable { - let reactions: [AggregatedReaction] - let selectedKey: String - - var id: String { - selectedKey - } -} - -struct ReadReceiptSummaryInfo: Identifiable { - let orderedReceipts: [ReadReceipt] - let id: TimelineItemIdentifier -} - -enum RoomScreenAlertInfoType: Hashable { - case audioRecodingPermissionError - case pollEndConfirmation(String) - case sendingFailed - case encryptionAuthenticity(String) -} - -struct RoomMemberState { - let displayName: String? - let avatarURL: URL? -} - -/// Used as the state for the TimelineView, to avoid having the context continuously refresh the list of items on each small change. -/// Is also nice to have this as a wrapper for any state that is directly connected to the timeline. -struct TimelineViewState { - var isLive = true - var paginationState = PaginationState.initial - - /// The room is in the process of loading items from a new timeline (switching to/from a detached timeline). - var isSwitchingTimelines = false - - struct FocussedEvent: Equatable { - enum Appearance { - /// The event should be shown using an animated scroll. - case animated - /// The event should be shown immediately, without any animation. - case immediate - /// The event has already been shown. - case hasAppeared - } - - /// The ID of the event. - let eventID: String - /// How the event should be shown, or whether it has already appeared. - var appearance: Appearance - } - - /// A focussed event that was navigated to via a permalink. - var focussedEvent: FocussedEvent? - - // These can be removed when we have full swiftUI and moved as @State values in the view - var scrollToBottomPublisher = PassthroughSubject() - - var itemsDictionary = OrderedDictionary() - - var timelineIDs: [String] { - itemsDictionary.keys.elements - } - - var itemViewStates: [RoomTimelineItemViewState] { - itemsDictionary.values.elements - } - - func hasLoadedItem(with eventID: String) -> Bool { - itemViewStates.contains { $0.identifier.eventID == eventID } - } -} - -enum ScrollDirection: Equatable { - case top - case bottom -} - -struct PinnedEventsState: Equatable { - var pinnedEventContents: OrderedDictionary = [:] { - didSet { - if selectedPinEventID == nil, !pinnedEventContents.keys.isEmpty { - // The default selected event should always be the last one. - selectedPinEventID = pinnedEventContents.keys.last - } else if pinnedEventContents.isEmpty { - selectedPinEventID = nil - } else if let selectedPinEventID, !pinnedEventContents.keys.set.contains(selectedPinEventID) { - self.selectedPinEventID = pinnedEventContents.keys.last - } - } - } - - private(set) var selectedPinEventID: String? - - var selectedPinIndex: Int { - let defaultValue = pinnedEventContents.isEmpty ? 0 : pinnedEventContents.count - 1 - guard let selectedPinEventID else { - return defaultValue - } - return pinnedEventContents.keys.firstIndex(of: selectedPinEventID) ?? defaultValue - } - - var selectedPinContent: AttributedString { - var content = AttributedString(" ") - if let selectedPinEventID, - let pinnedEventContent = pinnedEventContents[selectedPinEventID] { - content = pinnedEventContent - } - content.font = .compound.bodyMD - content.link = nil - return content - } - - mutating func previousPin() { - guard !pinnedEventContents.isEmpty else { - return - } - let currentIndex = selectedPinIndex - let nextIndex = currentIndex - 1 - if nextIndex == -1 { - selectedPinEventID = pinnedEventContents.keys.last - } else { - selectedPinEventID = pinnedEventContents.keys[nextIndex % pinnedEventContents.count] - } - } -} - -enum PinnedEventsBannerState: Equatable { - case loading(numbersOfEvents: Int) - case loaded(state: PinnedEventsState) - - var isEmpty: Bool { - switch self { - case .loaded(let state): - return state.pinnedEventContents.isEmpty - case .loading(let numberOfEvents): - return numberOfEvents == 0 - } - } - - var isLoading: Bool { - switch self { - case .loading: - return true - default: - return false - } - } - - var selectedPinEventID: String? { - switch self { - case .loaded(let state): - return state.selectedPinEventID - default: - return nil - } - } - - var count: Int { - switch self { - case .loaded(let state): - return state.pinnedEventContents.count - case .loading(let numberOfEvents): - return numberOfEvents - } - } - - var selectedPinIndex: Int { - switch self { - case .loaded(let state): - return state.selectedPinIndex - case .loading(let numbersOfEvents): - // We always want the index to be the last one when loading, since is the default one. - return numbersOfEvents - 1 - } - } - - var displayedMessage: AttributedString { - switch self { - case .loading: - return AttributedString(L10n.screenRoomPinnedBannerLoadingDescription) - case .loaded(let state): - return state.selectedPinContent - } - } - - var bannerIndicatorDescription: AttributedString { - let index = selectedPinIndex + 1 - let boldPlaceholder = "{bold}" - var finalString = AttributedString(L10n.screenRoomPinnedBannerIndicatorDescription(boldPlaceholder)) - var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, count)) - boldString.bold() - finalString.replace(boldPlaceholder, with: boldString) - return finalString - } - - mutating func previousPin() { - switch self { - case .loaded(var state): - state.previousPin() - self = .loaded(state: state) - default: - break - } - } - - mutating func setPinnedEventContents(_ pinnedEventContents: OrderedDictionary) { - switch self { - case .loading: - // The default selected event should always be the last one. - self = .loaded(state: .init(pinnedEventContents: pinnedEventContents, selectedPinEventID: pinnedEventContents.keys.last)) - case .loaded(var state): - state.pinnedEventContents = pinnedEventContents - self = .loaded(state: state) - } - } -} diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 309bca1a7..169141391 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -1,5 +1,5 @@ // -// Copyright 2022 New Vector Ltd +// Copyright 2024 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. @@ -14,957 +14,37 @@ // limitations under the License. // -import Algorithms import Combine -import OrderedCollections +import Foundation import SwiftUI typealias RoomScreenViewModelType = StateStoreViewModel class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol { - private enum Constants { - static let paginationEventLimit: UInt16 = 20 - static let detachedTimelineSize: UInt16 = 100 - static let focusTimelineToastIndicatorID = "RoomScreenFocusTimelineToastIndicator" - static let toastErrorID = "RoomScreenToastError" - } - - private let roomProxy: RoomProxyProtocol - private let timelineController: RoomTimelineControllerProtocol - private let mediaPlayerProvider: MediaPlayerProviderProtocol - private let userIndicatorController: UserIndicatorControllerProtocol - private let appMediator: AppMediatorProtocol - private let appSettings: AppSettings - private let analyticsService: AnalyticsService - private let pinnedEventStringBuilder: RoomEventStringBuilder - - private let roomScreenInteractionHandler: RoomScreenInteractionHandler - - private let composerFocusedSubject = PassthroughSubject() - private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - private var paginateBackwardsTask: Task? - private var paginateForwardsTask: Task? - - private var pinnedEventsTimelineProvider: RoomTimelineProviderProtocol? { - didSet { - guard let pinnedEventsTimelineProvider else { - return - } - - buildPinnedEventContent(timelineItems: pinnedEventsTimelineProvider.itemProxies) - pinnedEventsTimelineProvider.updatePublisher - // When pinning or unpinning an item, the timeline might return empty for a short while, so we need to debounce it to prevent weird UI behaviours like the banner disappearing - .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) - .sink { [weak self] updatedItems, _ in - guard let self else { return } - buildPinnedEventContent(timelineItems: updatedItems) - } - .store(in: &cancellables) - } - } - - init(roomProxy: RoomProxyProtocol, - focussedEventID: String? = nil, - timelineController: RoomTimelineControllerProtocol, - mediaProvider: MediaProviderProtocol, - mediaPlayerProvider: MediaPlayerProviderProtocol, - voiceMessageMediaManager: VoiceMessageMediaManagerProtocol, - userIndicatorController: UserIndicatorControllerProtocol, - appMediator: AppMediatorProtocol, - appSettings: AppSettings, - analyticsService: AnalyticsService) { - self.timelineController = timelineController - self.mediaPlayerProvider = mediaPlayerProvider - self.roomProxy = roomProxy - self.appSettings = appSettings - self.analyticsService = analyticsService - self.userIndicatorController = userIndicatorController - self.appMediator = appMediator - pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID) - - let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider) - - roomScreenInteractionHandler = RoomScreenInteractionHandler(roomProxy: roomProxy, - timelineController: timelineController, - mediaProvider: mediaProvider, - mediaPlayerProvider: mediaPlayerProvider, - voiceMessageMediaManager: voiceMessageMediaManager, - voiceMessageRecorder: voiceMessageRecorder, - userIndicatorController: userIndicatorController, - appMediator: appMediator, - appSettings: appSettings, - analyticsService: analyticsService) - - super.init(initialViewState: RoomScreenViewState(roomID: roomProxy.id, - roomTitle: roomProxy.roomTitle, - roomAvatar: roomProxy.avatar, - isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom, - timelineViewState: TimelineViewState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }), - ownUserID: roomProxy.ownUserID, - isViewSourceEnabled: appSettings.viewSourceEnabled, - hasOngoingCall: roomProxy.hasOngoingCall, - bindings: .init(reactionsCollapsed: [:])), - imageProvider: mediaProvider) - - if focussedEventID != nil { - // The timeline controller will start loading a detached timeline. - showFocusLoadingIndicator() - } - - setupSubscriptions() - setupDirectRoomSubscriptionsIfNeeded() - - // Set initial values for redacting from the macOS context menu. - Task { await updatePermissions() } - - state.audioPlayerStateProvider = { [weak self] itemID -> AudioPlayerState? in - guard let self else { - return nil - } - - return self.roomScreenInteractionHandler.audioPlayerState(for: itemID) - } - - buildTimelineViews(timelineItems: timelineController.timelineItems) - - updateMembers(roomProxy.membersPublisher.value) - - // Note: beware if we get to e.g. restore a reply / edit, - // maybe we are tracking a non-needed first initial state - trackComposerMode(.default) - - Task { - let userID = roomProxy.ownUserID - if case let .success(permission) = await roomProxy.canUserJoinCall(userID: userID) { - state.canJoinCall = permission - } - } - } - - // MARK: - Public - - func loadDraft() { - actionsSubject.send(.composer(action: .loadDraft)) - } - - func stop() { - // Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier. - state.bindings.mediaPreviewItem = nil - } - - func saveDraft() { - actionsSubject.send(.composer(action: .saveDraft)) - } - - override func process(viewAction: RoomScreenViewAction) { - switch viewAction { - case .itemAppeared(let id): - Task { await timelineController.processItemAppearance(id) } - case .itemDisappeared(let id): - Task { await timelineController.processItemDisappearance(id) } - - case .itemTapped(let id): - Task { await handleItemTapped(with: id) } - case .itemSendInfoTapped(let itemID): - handleItemSendInfoTapped(itemID: itemID) - case .toggleReaction(let emoji, let itemId): - Task { await timelineController.toggleReaction(emoji, to: itemId) } - case .sendReadReceiptIfNeeded(let lastVisibleItemID): - Task { await sendReadReceiptIfNeeded(for: lastVisibleItemID) } - case .paginateBackwards: - paginateBackwards() - case .paginateForwards: - paginateForwards() - case .scrollToBottom: - scrollToBottom() - - case .displayTimelineItemMenu(let itemID): - roomScreenInteractionHandler.displayTimelineItemActionMenu(for: itemID) - case .handleTimelineItemMenuAction(let itemID, let action): - roomScreenInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID) - - case .displayRoomDetails: - actionsSubject.send(.displayRoomDetails) - case .displayRoomMemberDetails(userID: let userID): - Task { await roomScreenInteractionHandler.displayRoomMemberDetails(userID: userID) } - case .displayEmojiPicker(let itemID): - roomScreenInteractionHandler.displayEmojiPicker(for: itemID) - case .displayReactionSummary(let itemID, let key): - displayReactionSummary(for: itemID, selectedKey: key) - case .displayReadReceipts(itemID: let itemID): - displayReadReceipts(for: itemID) - case .displayCall: - actionsSubject.send(.displayCallScreen) - analyticsService.trackInteraction(name: .MobileRoomCallButton) - case .handlePasteOrDrop(let provider): - roomScreenInteractionHandler.handlePasteOrDrop(provider) - case .handlePollAction(let pollAction): - handlePollAction(pollAction) - case .handleAudioPlayerAction(let audioPlayerAction): - handleAudioPlayerAction(audioPlayerAction) - - case .focusOnEventID(let eventID): - Task { await focusOnEvent(eventID: eventID) } - case .focusLive: - focusLive() - case .scrolledToFocussedItem: - didScrollToFocussedItem() - case .hasSwitchedTimeline: - Task { state.timelineViewState.isSwitchingTimelines = false } - case let .hasScrolled(direction): - state.lastScrollDirection = direction - case .tappedPinnedEventsBanner: - if let eventID = state.pinnedEventsBannerState.selectedPinEventID { - Task { await focusOnEvent(eventID: eventID) } - } - state.pinnedEventsBannerState.previousPin() - case .viewAllPins: - actionsSubject.send(.displayPinnedEventsTimeline) - } - } - - func process(composerAction: ComposerToolbarViewModelAction) { - switch composerAction { - case .sendMessage(let message, let html, let mode, let intentionalMentions): - Task { - await sendCurrentMessage(message, - html: html, - mode: mode, - intentionalMentions: intentionalMentions) - } - case .editLastMessage: - editLastMessage() - case .attach(let attachment): - attach(attachment) - case .handlePasteOrDrop(let provider): - roomScreenInteractionHandler.handlePasteOrDrop(provider) - case .composerModeChanged(mode: let mode): - trackComposerMode(mode) - case .composerFocusedChanged(isFocused: let isFocused): - composerFocusedSubject.send(isFocused) - case .voiceMessage(let voiceMessageAction): - processVoiceMessageAction(voiceMessageAction) - case .contentChanged(let isEmpty): - guard appSettings.sharePresence else { - return - } - - Task { - await roomProxy.sendTypingNotification(isTyping: !isEmpty) - } - } - } - - func focusOnEvent(eventID: String) async { - if state.timelineViewState.hasLoadedItem(with: eventID) { - state.timelineViewState.focussedEvent = .init(eventID: eventID, appearance: .animated) - return - } - - showFocusLoadingIndicator() - defer { hideFocusLoadingIndicator() } - - switch await timelineController.focusOnEvent(eventID, timelineSize: Constants.detachedTimelineSize) { - case .success: - state.timelineViewState.focussedEvent = .init(eventID: eventID, appearance: .immediate) - case .failure(let error): - MXLog.error("Failed to focus on event \(eventID)") - - if case .eventNotFound = error { - displayErrorToast(L10n.errorMessageNotFound) - } else { - displayErrorToast(L10n.commonFailed) - } - } - } - - // MARK: - Private - - private func focusLive() { - timelineController.focusLive() - } - - private func didScrollToFocussedItem() { - if var focussedEvent = state.timelineViewState.focussedEvent { - focussedEvent.appearance = .hasAppeared - state.timelineViewState.focussedEvent = focussedEvent - hideFocusLoadingIndicator() - } - } - - private func editLastMessage() { - guard let item = timelineController.timelineItems.reversed().first(where: { - guard let item = $0 as? EventBasedMessageTimelineItemProtocol else { - return false - } - - return item.sender.id == roomProxy.ownUserID && item.isEditable - }) else { - return - } - - roomScreenInteractionHandler.handleTimelineItemMenuAction(.edit, itemID: item.id) - } - - private func attach(_ attachment: ComposerAttachmentType) { - switch attachment { - case .camera: - actionsSubject.send(.displayCameraPicker) - case .photoLibrary: - actionsSubject.send(.displayMediaPicker) - case .file: - actionsSubject.send(.displayDocumentPicker) - case .location: - actionsSubject.send(.displayLocationPicker) - case .poll: - actionsSubject.send(.displayPollForm(mode: .new)) - } - } - - private func handlePollAction(_ action: RoomScreenViewPollAction) { - switch action { - case let .selectOption(pollStartID, optionID): - roomScreenInteractionHandler.sendPollResponse(pollStartID: pollStartID, optionID: optionID) - case let .end(pollStartID): - displayAlert(.pollEndConfirmation(pollStartID)) - case .edit(let pollStartID, let poll): - actionsSubject.send(.displayPollForm(mode: .edit(eventID: pollStartID, poll: poll))) - } - } - - private func handleAudioPlayerAction(_ action: RoomScreenAudioPlayerAction) { - switch action { - case .playPause(let itemID): - Task { await roomScreenInteractionHandler.playPauseAudio(for: itemID) } - case .seek(let itemID, let progress): - Task { await roomScreenInteractionHandler.seekAudio(for: itemID, progress: progress) } - } - } - - private func processVoiceMessageAction(_ action: ComposerToolbarVoiceMessageAction) { - switch action { - case .startRecording: - Task { - await mediaPlayerProvider.detachAllStates(except: nil) - await roomScreenInteractionHandler.startRecordingVoiceMessage() - } - case .stopRecording: - Task { await roomScreenInteractionHandler.stopRecordingVoiceMessage() } - case .cancelRecording: - Task { await roomScreenInteractionHandler.cancelRecordingVoiceMessage() } - case .deleteRecording: - Task { await roomScreenInteractionHandler.deleteCurrentVoiceMessage() } - case .send: - Task { await roomScreenInteractionHandler.sendCurrentVoiceMessage() } - case .startPlayback: - Task { await roomScreenInteractionHandler.startPlayingRecordedVoiceMessage() } - case .pausePlayback: - roomScreenInteractionHandler.pausePlayingRecordedVoiceMessage() - case .seekPlayback(let progress): - Task { await roomScreenInteractionHandler.seekRecordedVoiceMessage(to: progress) } - case .scrubPlayback(let scrubbing): - Task { await roomScreenInteractionHandler.scrubVoiceMessagePlayback(scrubbing: scrubbing) } - } - } - - private func updateMembers(_ members: [RoomMemberProxyProtocol]) { - state.members = members.reduce(into: [String: RoomMemberState]()) { dictionary, member in - dictionary[member.userID] = RoomMemberState(displayName: member.displayName, avatarURL: member.avatarURL) - } - } - - private func updatePermissions() async { - if case let .success(value) = await roomProxy.canUserRedactOther(userID: roomProxy.ownUserID) { - state.canCurrentUserRedactOthers = value - } else { - state.canCurrentUserRedactOthers = false - } - - if case let .success(value) = await roomProxy.canUserRedactOwn(userID: roomProxy.ownUserID) { - state.canCurrentUserRedactSelf = value - } else { - state.canCurrentUserRedactSelf = false - } - - if state.isPinningEnabled, - case let .success(value) = await roomProxy.canUserPinOrUnpin(userID: roomProxy.ownUserID) { - state.canCurrentUserPin = value - } else { - state.canCurrentUserPin = false - } - } - - private func setupSubscriptions() { - timelineController.callbacks - .receive(on: DispatchQueue.main) - .sink { [weak self] callback in - guard let self else { return } - - switch callback { - case .updatedTimelineItems(let updatedItems, let isSwitchingTimelines): - buildTimelineViews(timelineItems: updatedItems, isSwitchingTimelines: isSwitchingTimelines) - case .paginationState(let paginationState): - if state.timelineViewState.paginationState != paginationState { - state.timelineViewState.paginationState = paginationState - } - case .isLive(let isLive): - if state.timelineViewState.isLive != isLive { - state.timelineViewState.isLive = isLive - - // Remove the event highlight *only* when transitioning from non-live to live. - if isLive, state.timelineViewState.focussedEvent != nil { - state.timelineViewState.focussedEvent = nil - } - } - } - } - .store(in: &cancellables) - - let roomInfoSubscription = roomProxy - .actionsPublisher - .filter { $0 == .roomInfoUpdate } - - roomInfoSubscription - .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) - .sink { [weak self] _ in - guard let self else { return } - state.roomTitle = roomProxy.roomTitle - state.roomAvatar = roomProxy.avatar - state.hasOngoingCall = roomProxy.hasOngoingCall - } - .store(in: &cancellables) - - Task { [weak self] in - // Don't guard let self here, otherwise the for await will strongify the self reference creating a strong reference cycle. - // If the subscription has sent a value before the Task has started it might be lost, so before entering the loop we always do an update. - await self?.updatePinnedEventIDs() - for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values { - guard !Task.isCancelled else { - return - } - await self?.updatePinnedEventIDs() - } - } - .store(in: &cancellables) - - setupAppSettingsSubscriptions() - - roomProxy.membersPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] in self?.updateMembers($0) } - .store(in: &cancellables) - - roomProxy.typingMembersPublisher - .receive(on: DispatchQueue.main) - .filter { [weak self] _ in self?.appSettings.sharePresence ?? false } - .weakAssign(to: \.state.typingMembers, on: self) - .store(in: &cancellables) - - roomScreenInteractionHandler.actions - .receive(on: DispatchQueue.main) - .sink { [weak self] action in - guard let self else { return } - - switch action { - case .composer(let action): - actionsSubject.send(.composer(action: action)) - case .displayAudioRecorderPermissionError: - displayAlert(.audioRecodingPermissionError) - case .displayErrorToast(let title): - displayErrorToast(title) - case .displayEmojiPicker(let itemID, let selectedEmojis): - actionsSubject.send(.displayEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis)) - case .displayMessageForwarding(let itemID): - Task { await self.forwardMessage(itemID: itemID) } - case .displayPollForm(let mode): - actionsSubject.send(.displayPollForm(mode: mode)) - case .displayReportContent(let itemID, let senderID): - actionsSubject.send(.displayReportContent(itemID: itemID, senderID: senderID)) - case .displayMediaUploadPreviewScreen(let url): - actionsSubject.send(.displayMediaUploadPreviewScreen(url: url)) - case .displayRoomMemberDetails(userID: let userID): - actionsSubject.send(.displayRoomMemberDetails(userID: userID)) - case .showActionMenu(let actionMenuInfo): - Task { - await self.updatePermissions() - self.state.bindings.actionMenuInfo = actionMenuInfo - } - case .showDebugInfo(let debugInfo): - state.bindings.debugInfo = debugInfo - } - } - .store(in: &cancellables) - - appSettings.$pinningEnabled - .combineLatest(appMediator.networkMonitor.reachabilityPublisher) - .filter { $0.0 && $0.1 == .reachable } - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.setupPinnedEventsTimelineProviderIfNeeded() - } - .store(in: &cancellables) - } - - private func setupAppSettingsSubscriptions() { - appSettings.$sharePresence - .weakAssign(to: \.state.showReadReceipts, on: self) - .store(in: &cancellables) - - appSettings.$viewSourceEnabled - .weakAssign(to: \.state.isViewSourceEnabled, on: self) - .store(in: &cancellables) - - appSettings.$pinningEnabled - .weakAssign(to: \.state.isPinningEnabled, on: self) - .store(in: &cancellables) - } - - private func setupPinnedEventsTimelineProviderIfNeeded() { - guard pinnedEventsTimelineProvider == nil else { - return - } - - Task { - guard let timelineProvider = await roomProxy.pinnedEventsTimeline?.timelineProvider else { - return - } - - if pinnedEventsTimelineProvider == nil { - pinnedEventsTimelineProvider = timelineProvider - } - } - } - - private func updatePinnedEventIDs() async { - let pinnedEventIDs = await roomProxy.pinnedEventIDs - // Only update the loading state of the banner - if state.pinnedEventsBannerState.isLoading { - state.pinnedEventsBannerState = .loading(numbersOfEvents: pinnedEventIDs.count) - } - state.pinnedEventIDs = pinnedEventIDs - } - - private func setupDirectRoomSubscriptionsIfNeeded() { - guard roomProxy.isDirect else { - return - } - - let shouldShowInviteAlert = composerFocusedSubject - .removeDuplicates() - .map { [weak self] isFocused in - guard let self else { return false } - - return isFocused && self.roomProxy.isUserAloneInDirectRoom - } - // We want to show the alert just once, so we are taking the first "true" emitted - .first { $0 } - - shouldShowInviteAlert - .sink { [weak self] _ in - self?.showInviteAlert() - } - .store(in: &cancellables) - } - - private func paginateBackwards() { - guard paginateBackwardsTask == nil else { - return - } - - paginateBackwardsTask = Task { [weak self] in - guard let self else { - return - } - - switch await timelineController.paginateBackwards(requestSize: Constants.paginationEventLimit) { - case .failure: - displayErrorToast(L10n.errorFailedLoadingMessages) - default: - break - } - paginateBackwardsTask = nil - } - } - - private func paginateForwards() { - guard paginateForwardsTask == nil else { - return - } - - paginateForwardsTask = Task { [weak self] in - guard let self else { - return - } - - switch await timelineController.paginateForwards(requestSize: Constants.paginationEventLimit) { - case .failure: - displayErrorToast(L10n.errorFailedLoadingMessages) - default: - break - } - - if state.timelineViewState.paginationState.forward == .timelineEndReached { - focusLive() - } - - paginateForwardsTask = nil - } - } - - private func scrollToBottom() { - if state.timelineViewState.isLive { - state.timelineViewState.scrollToBottomPublisher.send(()) - } else { - focusLive() - } - } - - private func sendReadReceiptIfNeeded(for lastVisibleItemID: TimelineItemIdentifier) async { - guard appMediator.appState == .active else { return } - - await timelineController.sendReadReceipt(for: lastVisibleItemID) - } - - private func handleItemTapped(with itemID: TimelineItemIdentifier) async { - state.showLoading = true - let action = await roomScreenInteractionHandler.processItemTap(itemID) - - switch action { - case .displayMediaFile(let file, let title): - 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): - actionsSubject.send(.displayLocation(body: body, geoURI: geoURI, description: description)) - case .none: - break - } - state.showLoading = false - } - - private func handleItemSendInfoTapped(itemID: TimelineItemIdentifier) { - guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) else { - MXLog.warning("Couldn't find timeline item.") - return - } - - guard let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { - fatalError("Only events can have send info.") - } - - if eventTimelineItem.properties.deliveryStatus == .sendingFailed { - displayAlert(.sendingFailed) - } else if let authenticityMessage = eventTimelineItem.properties.encryptionAuthenticity?.message { - displayAlert(.encryptionAuthenticity(authenticityMessage)) - } - } - - private func sendCurrentMessage(_ message: String, html: String?, mode: RoomScreenComposerMode, intentionalMentions: IntentionalMentions) async { - guard !message.isEmpty else { - fatalError("This message should never be empty") - } - - actionsSubject.send(.composer(action: .clear)) - - switch mode { - case .reply(let itemId, _, _): - await timelineController.sendMessage(message, - html: html, - inReplyTo: itemId, - intentionalMentions: intentionalMentions) - case .edit(let originalItemId): - await timelineController.edit(originalItemId, - message: message, - html: html, - intentionalMentions: intentionalMentions) - case .default: - await timelineController.sendMessage(message, - html: html, - intentionalMentions: intentionalMentions) - case .recordVoiceMessage, .previewVoiceMessage: - fatalError("invalid composer mode.") - } - - scrollToBottom() - } - - private func trackComposerMode(_ mode: RoomScreenComposerMode) { - var isEdit = false - var isReply = false - switch mode { - case .edit: - isEdit = true - case .reply: - isReply = true - default: - break - } - - analyticsService.trackComposer(inThread: false, isEditing: isEdit, isReply: isReply, startsThread: nil) - } - - // MARK: - Timeline Item Building - - private func buildPinnedEventContent(timelineItems: [TimelineItemProxy]) { - var pinnedEventContents = OrderedDictionary() - - for item in timelineItems { - // Only remote events are pinned - if case let .event(event) = item, - let eventID = event.id.eventID { - pinnedEventContents.updateValue(pinnedEventStringBuilder.buildAttributedString(for: event) ?? AttributedString(L10n.commonUnsupportedEvent), - forKey: eventID) - } - } - - state.pinnedEventsBannerState.setPinnedEventContents(pinnedEventContents) - } - - private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) { - var timelineItemsDictionary = OrderedDictionary() - - timelineItems.filter { $0 is RedactedRoomTimelineItem }.forEach { timelineItem in - // Stops the audio player when a voice message is redacted. - guard let playerState = mediaPlayerProvider.playerState(for: .timelineItemIdentifier(timelineItem.id)) else { - return - } - - Task { @MainActor in - playerState.detachAudioPlayer() - mediaPlayerProvider.unregister(audioPlayerState: playerState) - } - } - - let itemsGroupedByTimelineDisplayStyle = timelineItems.chunked { current, next in - canGroupItem(timelineItem: current, with: next) - } - - for itemGroup in itemsGroupedByTimelineDisplayStyle { - guard !itemGroup.isEmpty else { - MXLog.error("Found empty item group") - continue - } - - if itemGroup.count == 1 { - if let firstItem = itemGroup.first { - timelineItemsDictionary.updateValue(updateViewState(item: firstItem, groupStyle: .single), - forKey: firstItem.id.timelineID) - } - } else { - for (index, item) in itemGroup.enumerated() { - if index == 0 { - timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: .first), - forKey: item.id.timelineID) - } else if index == itemGroup.count - 1 { - timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: .last), - forKey: item.id.timelineID) - } else { - timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: .middle), - forKey: item.id.timelineID) - } - } - } - } - - if isSwitchingTimelines { - state.timelineViewState.isSwitchingTimelines = true - } - - state.timelineViewState.itemsDictionary = timelineItemsDictionary - } - - private func updateViewState(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) -> RoomTimelineItemViewState { - if let timelineItemViewState = state.timelineViewState.itemsDictionary[item.id.timelineID] { - timelineItemViewState.groupStyle = groupStyle - timelineItemViewState.type = .init(item: item) - return timelineItemViewState - } else { - return RoomTimelineItemViewState(item: item, groupStyle: groupStyle) - } - } - - private func canGroupItem(timelineItem: RoomTimelineItemProtocol, with otherTimelineItem: RoomTimelineItemProtocol) -> Bool { - if timelineItem is CollapsibleTimelineItem || otherTimelineItem is CollapsibleTimelineItem { - return false - } - - guard let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol, - let otherEventTimelineItem = otherTimelineItem as? EventBasedTimelineItemProtocol else { - return false - } - - // State events aren't rendered as messages so shouldn't be grouped. - if eventTimelineItem is StateRoomTimelineItem || otherEventTimelineItem is StateRoomTimelineItem { - return false - } - - // can be improved by adding a date threshold - return eventTimelineItem.properties.reactions.isEmpty && eventTimelineItem.sender == otherEventTimelineItem.sender - } - - // MARK: - Direct chats logics - - private func showInviteAlert() { - userIndicatorController.alertInfo = .init(id: .init(), - title: L10n.screenRoomInviteAgainAlertTitle, - message: L10n.screenRoomInviteAgainAlertMessage, - primaryButton: .init(title: L10n.actionInvite, action: { [weak self] in self?.inviteOtherDMUserBack() }), - secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) - } - - private let inviteLoadingIndicatorID = UUID().uuidString - - private func inviteOtherDMUserBack() { - guard roomProxy.isUserAloneInDirectRoom else { - userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError) - return - } - - Task { - userIndicatorController.submitIndicator(.init(id: inviteLoadingIndicatorID, type: .toast, title: L10n.commonLoading)) - defer { - userIndicatorController.retractIndicatorWithId(inviteLoadingIndicatorID) - } - - guard - let members = await roomProxy.members(), - members.count == 2, - let otherPerson = members.first(where: { $0.userID != roomProxy.ownUserID && $0.membership == .leave }) - else { - userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError) - return - } - - switch await roomProxy.invite(userID: otherPerson.userID) { - case .success: - break - case .failure: - userIndicatorController.alertInfo = .init(id: .init(), - title: L10n.commonUnableToInviteTitle, - message: L10n.commonUnableToInviteMessage) - } - } - } - - // MARK: - Reactions - - private func displayReactionSummary(for itemID: TimelineItemIdentifier, selectedKey: String) { - guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), - let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { - return - } - - state.bindings.reactionSummaryInfo = .init(reactions: eventTimelineItem.properties.reactions, selectedKey: selectedKey) - } - - // MARK: - Read Receipts - - private func displayReadReceipts(for itemID: TimelineItemIdentifier) { - guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), - let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { - return - } - - state.bindings.readReceiptsSummaryInfo = .init(orderedReceipts: eventTimelineItem.properties.orderedReadReceipts, id: eventTimelineItem.id) - } - - // MARK: - Message forwarding - - private func forwardMessage(itemID: TimelineItemIdentifier) async { - guard let content = await timelineController.messageEventContent(for: itemID) else { return } - actionsSubject.send(.displayMessageForwarding(forwardingItem: .init(id: itemID, roomID: roomProxy.id, content: content))) - } - - // MARK: - User Indicators - - private func showFocusLoadingIndicator() { - userIndicatorController.submitIndicator(UserIndicator(id: Constants.focusTimelineToastIndicatorID, - type: .toast(progress: .indeterminate), - title: L10n.commonLoading, - persistent: true)) - } - - private func hideFocusLoadingIndicator() { - userIndicatorController.retractIndicatorWithId(Constants.focusTimelineToastIndicatorID) - } - - private func displayAlert(_ type: RoomScreenAlertInfoType) { - switch type { - case .audioRecodingPermissionError: - state.bindings.alertInfo = .init(id: type, - title: L10n.dialogPermissionMicrophoneTitleIos(InfoPlistReader.main.bundleDisplayName), - message: L10n.dialogPermissionMicrophoneDescriptionIos, - primaryButton: .init(title: L10n.commonSettings, action: { [weak self] in self?.appMediator.openAppSettings() }), - secondaryButton: .init(title: L10n.actionNotNow, role: .cancel, action: nil)) - case .pollEndConfirmation(let pollStartID): - state.bindings.alertInfo = .init(id: type, - title: L10n.actionEndPoll, - message: L10n.commonPollEndConfirmation, - primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), - secondaryButton: .init(title: L10n.actionOk, action: { self.roomScreenInteractionHandler.endPoll(pollStartID: pollStartID) })) - case .sendingFailed: - state.bindings.alertInfo = .init(id: type, - title: L10n.commonSendingFailed, - primaryButton: .init(title: L10n.actionOk, action: nil)) - case .encryptionAuthenticity(let message): - state.bindings.alertInfo = .init(id: type, - title: message, - primaryButton: .init(title: L10n.actionOk, action: nil)) - } - } - - private func displayErrorToast(_ title: String) { - userIndicatorController.submitIndicator(UserIndicator(id: Constants.toastErrorID, - type: .toast, - title: title, - iconName: "xmark")) + init() { + super.init(initialViewState: .init(bindings: .init())) } } -private extension RoomProxyProtocol { - /// Checks if the other person left the room in a direct chat - var isUserAloneInDirectRoom: Bool { - isDirect && activeMembersCount == 1 - } -} - -// MARK: - Mocks - extension RoomScreenViewModel { - static let mock = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room")), - focussedEventID: nil, - timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + static func mock() -> RoomScreenViewModel { + RoomScreenViewModel() + } } private struct RoomContextKey: EnvironmentKey { @MainActor static let defaultValue: RoomScreenViewModel.Context? = nil } -private struct FocussedEventID: EnvironmentKey { - static let defaultValue: String? = nil -} - extension EnvironmentValues { /// Used to access and inject the room context without observing it var roomContext: RoomScreenViewModel.Context? { get { self[RoomContextKey.self] } set { self[RoomContextKey.self] = newValue } } - - /// An event ID which will be non-nil when a timeline item should show as focussed. - var focussedEventID: String? { - get { self[FocussedEventID.self] } - set { self[FocussedEventID.self] = newValue } - } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift index 757d36c41..558f3acb6 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift @@ -1,5 +1,5 @@ // -// Copyright 2022 New Vector Ltd +// Copyright 2024 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. @@ -16,16 +16,8 @@ import Combine import Foundation -import SwiftUI -@MainActor protocol RoomScreenViewModelProtocol { var actions: AnyPublisher { get } - var context: RoomScreenViewModelType.Context { get } - func process(composerAction: ComposerToolbarViewModelAction) - /// Updates the timeline to show and highlight the item with the corresponding event ID. - func focusOnEvent(eventID: String) async - func stop() - func loadDraft() - func saveDraft() + var context: RoomScreenViewModel.Context { get } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 0655395a9..7b258a9d4 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -19,13 +19,17 @@ import SwiftUI import WysiwygComposer struct RoomScreen: View { - @ObservedObject var context: RoomScreenViewModel.Context + @ObservedObject var roomContext: RoomScreenViewModel.Context + @ObservedObject var timelineContext: TimelineViewModel.Context @ObservedObject private var composerToolbarContext: ComposerToolbarViewModel.Context @State private var dragOver = false let composerToolbar: ComposerToolbar - init(context: RoomScreenViewModel.Context, composerToolbar: ComposerToolbar) { - self.context = context + init(roomViewModel: RoomScreenViewModelProtocol, + timelineViewModel: TimelineViewModelProtocol, + composerToolbar: ComposerToolbar) { + roomContext = roomViewModel.context + timelineContext = timelineViewModel.context self.composerToolbar = composerToolbar composerToolbarContext = composerToolbar.context } @@ -45,16 +49,16 @@ struct RoomScreen: View { } .padding(.top, 8) .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) - .environmentObject(context) - .environment(\.roomContext, context) + .environmentObject(timelineContext) + .environment(\.timelineContext, timelineContext) } .overlay(alignment: .top) { Group { - if context.viewState.shouldShowPinnedEventsBanner { + if timelineContext.viewState.shouldShowPinnedEventsBanner { pinnedItemsBanner } } - .animation(.elementDefault, value: context.viewState.shouldShowPinnedEventsBanner) + .animation(.elementDefault, value: timelineContext.viewState.shouldShowPinnedEventsBanner) } .navigationTitle(L10n.screenRoomTitle) // Hidden but used for back button text. .navigationBarTitleDisplayMode(.inline) @@ -62,30 +66,30 @@ struct RoomScreen: View { .toolbar { toolbar } .toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background. .overlay { loadingIndicator } - .alert(item: $context.alertInfo) - .sheet(item: $context.debugInfo) { TimelineItemDebugView(info: $0) } - .sheet(item: $context.actionMenuInfo) { info in + .alert(item: $timelineContext.alertInfo) + .sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) } + .sheet(item: $timelineContext.actionMenuInfo) { info in let actions = TimelineItemMenuActionProvider(timelineItem: info.item, - canCurrentUserRedactSelf: context.viewState.canCurrentUserRedactSelf, - canCurrentUserRedactOthers: context.viewState.canCurrentUserRedactOthers, - canCurrentUserPin: context.viewState.canCurrentUserPin, - pinnedEventIDs: context.viewState.pinnedEventIDs, - isDM: context.viewState.isEncryptedOneToOneRoom, - isViewSourceEnabled: context.viewState.isViewSourceEnabled).makeActions() + canCurrentUserRedactSelf: timelineContext.viewState.canCurrentUserRedactSelf, + canCurrentUserRedactOthers: timelineContext.viewState.canCurrentUserRedactOthers, + canCurrentUserPin: timelineContext.viewState.canCurrentUserPin, + pinnedEventIDs: timelineContext.viewState.pinnedEventIDs, + isDM: timelineContext.viewState.isEncryptedOneToOneRoom, + isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled).makeActions() if let actions { TimelineItemMenu(item: info.item, actions: actions) - .environmentObject(context) + .environmentObject(timelineContext) } } - .sheet(item: $context.reactionSummaryInfo) { - ReactionsSummaryView(reactions: $0.reactions, members: context.viewState.members, imageProvider: context.imageProvider, selectedReactionKey: $0.selectedKey) + .sheet(item: $timelineContext.reactionSummaryInfo) { + ReactionsSummaryView(reactions: $0.reactions, members: timelineContext.viewState.members, imageProvider: timelineContext.imageProvider, selectedReactionKey: $0.selectedKey) .edgesIgnoringSafeArea([.bottom]) } - .sheet(item: $context.readReceiptsSummaryInfo) { + .sheet(item: $timelineContext.readReceiptsSummaryInfo) { ReadReceiptsSummaryView(orderedReadReceipts: $0.orderedReceipts) - .environmentObject(context) + .environmentObject(timelineContext) } - .interactiveQuickLook(item: $context.mediaPreviewItem) + .interactiveQuickLook(item: $timelineContext.mediaPreviewItem) .track(screen: .Room) .onDrop(of: ["public.item", "public.file-url"], isTargeted: $dragOver) { providers -> Bool in guard let provider = providers.first, @@ -93,7 +97,7 @@ struct RoomScreen: View { return false } - context.send(viewAction: .handlePasteOrDrop(provider: provider)) + timelineContext.send(viewAction: .handlePasteOrDrop(provider: provider)) return true } .sentryTrace("\(Self.self)") @@ -101,23 +105,23 @@ struct RoomScreen: View { private var timeline: some View { TimelineView() - .id(context.viewState.roomID) - .environmentObject(context) - .environment(\.focussedEventID, context.viewState.timelineViewState.focussedEvent?.eventID) + .id(timelineContext.viewState.roomID) + .environmentObject(timelineContext) + .environment(\.focussedEventID, timelineContext.viewState.timelineViewState.focussedEvent?.eventID) .overlay(alignment: .bottomTrailing) { scrollToBottomButton } } private var pinnedItemsBanner: some View { - PinnedItemsBannerView(state: context.viewState.pinnedEventsBannerState, - onMainButtonTap: { context.send(viewAction: .tappedPinnedEventsBanner) }, - onViewAllButtonTap: { context.send(viewAction: .viewAllPins) }) + PinnedItemsBannerView(state: timelineContext.viewState.pinnedEventsBannerState, + onMainButtonTap: { timelineContext.send(viewAction: .tappedPinnedEventsBanner) }, + onViewAllButtonTap: { timelineContext.send(viewAction: .viewAllPins) }) .transition(.move(edge: .top)) } private var scrollToBottomButton: some View { - Button { context.send(viewAction: .scrollToBottom) } label: { + Button { timelineContext.send(viewAction: .scrollToBottom) } label: { Image(systemName: "chevron.down") .font(.compound.bodyLG) .fontWeight(.semibold) @@ -139,12 +143,12 @@ struct RoomScreen: View { } private var isAtBottomAndLive: Bool { - context.isScrolledToBottom && context.viewState.timelineViewState.isLive + timelineContext.isScrolledToBottom && timelineContext.viewState.timelineViewState.isLive } @ViewBuilder private var loadingIndicator: some View { - if context.viewState.showLoading { + if timelineContext.viewState.showLoading { ProgressView() .progressViewStyle(.circular) .tint(.compound.textPrimary) @@ -159,29 +163,29 @@ struct RoomScreen: View { // .principal + .primaryAction works better than .navigation leading + trailing // as the latter disables interaction in the action button for rooms with long names ToolbarItem(placement: .principal) { - RoomHeaderView(roomName: context.viewState.roomTitle, - roomAvatar: context.viewState.roomAvatar, - imageProvider: context.imageProvider) + RoomHeaderView(roomName: timelineContext.viewState.roomTitle, + roomAvatar: timelineContext.viewState.roomAvatar, + imageProvider: timelineContext.imageProvider) // Using a button stops it from getting truncated in the navigation bar .contentShape(.rect) .onTapGesture { - context.send(viewAction: .displayRoomDetails) + timelineContext.send(viewAction: .displayRoomDetails) } } if !ProcessInfo.processInfo.isiOSAppOnMac { ToolbarItem(placement: .primaryAction) { callButton - .disabled(context.viewState.canJoinCall == false) + .disabled(timelineContext.viewState.canJoinCall == false) } } } @ViewBuilder private var callButton: some View { - if context.viewState.hasOngoingCall { + if timelineContext.viewState.hasOngoingCall { Button { - context.send(viewAction: .displayCall) + timelineContext.send(viewAction: .displayCall) } label: { Label(L10n.actionJoin, icon: \.videoCallSolid) .labelStyle(.titleAndIcon) @@ -190,7 +194,7 @@ struct RoomScreen: View { .accessibilityIdentifier(A11yIdentifiers.roomScreen.joinCall) } else { Button { - context.send(viewAction: .displayCall) + timelineContext.send(viewAction: .displayCall) } label: { CompoundIcon(\.videoCallSolid) } @@ -206,21 +210,24 @@ struct RoomScreen: View { // MARK: - Previews struct RoomScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(id: "stable_id", - name: "Preview room", - hasOngoingCall: true)), - timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + static let roomViewModel = RoomScreenViewModel.mock() + static let timelineViewModel = TimelineViewModel(roomProxy: RoomProxyMock(.init(id: "stable_id", + name: "Preview room", + hasOngoingCall: true)), + timelineController: MockRoomTimelineController(), + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: ServiceLocator.shared.userIndicatorController, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) static var previews: some View { NavigationStack { - RoomScreen(context: viewModel.context, composerToolbar: ComposerToolbar.mock()) + RoomScreen(roomViewModel: roomViewModel, + timelineViewModel: timelineViewModel, + composerToolbar: ComposerToolbar.mock()) } } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift similarity index 98% rename from ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift rename to ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index 452407077..ad694a0f0 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -17,8 +17,8 @@ import Combine import UIKit -enum RoomScreenInteractionHandlerAction { - case composer(action: RoomScreenComposerAction) +enum TimelineInteractionHandlerAction { + case composer(action: TimelineComposerAction) case displayEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set) case displayReportContent(itemID: TimelineItemIdentifier, senderID: String) @@ -35,7 +35,7 @@ enum RoomScreenInteractionHandlerAction { } @MainActor -class RoomScreenInteractionHandler { +class TimelineInteractionHandler { private let roomProxy: RoomProxyProtocol private let timelineController: RoomTimelineControllerProtocol private let mediaProvider: MediaProviderProtocol @@ -48,8 +48,8 @@ class RoomScreenInteractionHandler { private let analyticsService: AnalyticsService private let pollInteractionHandler: PollInteractionHandlerProtocol - private let actionsSubject: PassthroughSubject = .init() - var actions: AnyPublisher { + private let actionsSubject: PassthroughSubject = .init() + var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift new file mode 100644 index 000000000..9743c8587 --- /dev/null +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -0,0 +1,384 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import OrderedCollections +import SwiftUI + +enum TimelineViewModelAction { + case displayRoomDetails + case displayEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set) + case displayReportContent(itemID: TimelineItemIdentifier, senderID: String) + case displayCameraPicker + case displayMediaPicker + case displayDocumentPicker + case displayLocationPicker + case displayPollForm(mode: PollFormMode) + case displayMediaUploadPreviewScreen(url: URL) + case displayRoomMemberDetails(userID: String) + case displayMessageForwarding(forwardingItem: MessageForwardingItem) + case displayLocation(body: String, geoURI: GeoURI, description: String?) + case composer(action: TimelineComposerAction) + case displayCallScreen + case displayPinnedEventsTimeline +} + +enum TimelineViewPollAction { + case selectOption(pollStartID: String, optionID: String) + case end(pollStartID: String) + case edit(pollStartID: String, poll: Poll) +} + +enum TimelineAudioPlayerAction { + case playPause(itemID: TimelineItemIdentifier) + case seek(itemID: TimelineItemIdentifier, progress: Double) +} + +enum TimelineViewAction { + case itemAppeared(itemID: TimelineItemIdentifier) // t + case itemDisappeared(itemID: TimelineItemIdentifier) // t + + case itemTapped(itemID: TimelineItemIdentifier) // t + case itemSendInfoTapped(itemID: TimelineItemIdentifier) // t + case toggleReaction(key: String, itemID: TimelineItemIdentifier) // t + case sendReadReceiptIfNeeded(TimelineItemIdentifier) // t + case paginateBackwards // t + case paginateForwards // t + case scrollToBottom // t + + case displayTimelineItemMenu(itemID: TimelineItemIdentifier) // t + case handleTimelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction) // not t + + case displayRoomDetails // not t + case displayRoomMemberDetails(userID: String) // t -> change name + case displayReactionSummary(itemID: TimelineItemIdentifier, key: String) // t -> handle externally + case displayEmojiPicker(itemID: TimelineItemIdentifier) // t -> handle externally + case displayReadReceipts(itemID: TimelineItemIdentifier) // t -> handle externally + case displayCall // not t + + case handlePasteOrDrop(provider: NSItemProvider) // not t + case handlePollAction(TimelineViewPollAction) // t + case handleAudioPlayerAction(TimelineAudioPlayerAction) // t + + /// Focus the timeline onto the specified event ID (switching to a detached timeline if needed). + case focusOnEventID(String) // t + /// Switch back to a live timeline (from a detached one). + case focusLive // t + /// The timeline scrolled to reveal the focussed item. + case scrolledToFocussedItem // t + /// The table view has loaded the first items for a new timeline. + case hasSwitchedTimeline // t + + case hasScrolled(direction: ScrollDirection) // t + case tappedPinnedEventsBanner // not t + case viewAllPins // not t +} + +enum TimelineComposerAction { + case setMode(mode: ComposerMode) + case setText(plainText: String, htmlText: String?) + case removeFocus + case clear +} + +struct TimelineViewState: BindableState { + var roomID: String + var roomTitle = "" + var roomAvatar: RoomAvatar + var members: [String: RoomMemberState] = [:] + var typingMembers: [String] = [] + var showLoading = false + var showReadReceipts = false + var isEncryptedOneToOneRoom = false + var timelineViewState: TimelineState // check the doc before changing this + + var ownUserID: String + var canCurrentUserRedactOthers = false + var canCurrentUserRedactSelf = false + var canCurrentUserPin = false + var isViewSourceEnabled: Bool + + var isPinningEnabled = false + var lastScrollDirection: ScrollDirection? + + // The `pinnedEventIDs` are used only to determine if an item is already pinned or not. + // It's updated from the room info, so it's faster than using the timeline + var pinnedEventIDs: Set = [] + // This is used to control the banner + var pinnedEventsBannerState: PinnedEventsBannerState = .loading(numbersOfEvents: 0) + + var shouldShowPinnedEventsBanner: Bool { + isPinningEnabled && !pinnedEventsBannerState.isEmpty && lastScrollDirection != .top + } + + var canJoinCall = false + var hasOngoingCall = false + + var bindings: TimelineViewStateBindings + + /// A closure providing the associated audio player state for an item in the timeline. + var audioPlayerStateProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> AudioPlayerState?)? +} + +struct TimelineViewStateBindings { + var isScrolledToBottom = true + + /// The state of wether reactions listed on the timeline are expanded/collapsed. + /// Key is itemID, value is the collapsed state. + var reactionsCollapsed: [TimelineItemIdentifier: Bool] + + /// A media item that will be previewed with QuickLook. + var mediaPreviewItem: MediaPreviewItem? + + var alertInfo: AlertInfo? + + var debugInfo: TimelineItemDebugInfo? + + var actionMenuInfo: TimelineItemActionMenuInfo? + + var reactionSummaryInfo: ReactionSummaryInfo? + + var readReceiptsSummaryInfo: ReadReceiptSummaryInfo? +} + +struct TimelineItemActionMenuInfo: Equatable, Identifiable { + static func == (lhs: TimelineItemActionMenuInfo, rhs: TimelineItemActionMenuInfo) -> Bool { + lhs.id == rhs.id + } + + let item: EventBasedTimelineItemProtocol + + var id: TimelineItemIdentifier { + item.id + } +} + +struct ReactionSummaryInfo: Identifiable { + let reactions: [AggregatedReaction] + let selectedKey: String + + var id: String { + selectedKey + } +} + +struct ReadReceiptSummaryInfo: Identifiable { + let orderedReceipts: [ReadReceipt] + let id: TimelineItemIdentifier +} + +enum RoomScreenAlertInfoType: Hashable { + case audioRecodingPermissionError + case pollEndConfirmation(String) + case sendingFailed + case encryptionAuthenticity(String) +} + +struct RoomMemberState { + let displayName: String? + let avatarURL: URL? +} + +/// Used as the state for the TimelineView, to avoid having the context continuously refresh the list of items on each small change. +/// Is also nice to have this as a wrapper for any state that is directly connected to the timeline. +struct TimelineState { + var isLive = true + var paginationState = PaginationState.initial + + /// The room is in the process of loading items from a new timeline (switching to/from a detached timeline). + var isSwitchingTimelines = false + + struct FocussedEvent: Equatable { + enum Appearance { + /// The event should be shown using an animated scroll. + case animated + /// The event should be shown immediately, without any animation. + case immediate + /// The event has already been shown. + case hasAppeared + } + + /// The ID of the event. + let eventID: String + /// How the event should be shown, or whether it has already appeared. + var appearance: Appearance + } + + /// A focussed event that was navigated to via a permalink. + var focussedEvent: FocussedEvent? + + // These can be removed when we have full swiftUI and moved as @State values in the view + var scrollToBottomPublisher = PassthroughSubject() + + var itemsDictionary = OrderedDictionary() + + var timelineIDs: [String] { + itemsDictionary.keys.elements + } + + var itemViewStates: [RoomTimelineItemViewState] { + itemsDictionary.values.elements + } + + func hasLoadedItem(with eventID: String) -> Bool { + itemViewStates.contains { $0.identifier.eventID == eventID } + } +} + +enum ScrollDirection: Equatable { + case top + case bottom +} + +struct PinnedEventsState: Equatable { + var pinnedEventContents: OrderedDictionary = [:] { + didSet { + if selectedPinEventID == nil, !pinnedEventContents.keys.isEmpty { + // The default selected event should always be the last one. + selectedPinEventID = pinnedEventContents.keys.last + } else if pinnedEventContents.isEmpty { + selectedPinEventID = nil + } else if let selectedPinEventID, !pinnedEventContents.keys.set.contains(selectedPinEventID) { + self.selectedPinEventID = pinnedEventContents.keys.last + } + } + } + + private(set) var selectedPinEventID: String? + + var selectedPinIndex: Int { + let defaultValue = pinnedEventContents.isEmpty ? 0 : pinnedEventContents.count - 1 + guard let selectedPinEventID else { + return defaultValue + } + return pinnedEventContents.keys.firstIndex(of: selectedPinEventID) ?? defaultValue + } + + var selectedPinContent: AttributedString { + var content = AttributedString(" ") + if let selectedPinEventID, + let pinnedEventContent = pinnedEventContents[selectedPinEventID] { + content = pinnedEventContent + } + content.font = .compound.bodyMD + content.link = nil + return content + } + + mutating func previousPin() { + guard !pinnedEventContents.isEmpty else { + return + } + let currentIndex = selectedPinIndex + let nextIndex = currentIndex - 1 + if nextIndex == -1 { + selectedPinEventID = pinnedEventContents.keys.last + } else { + selectedPinEventID = pinnedEventContents.keys[nextIndex % pinnedEventContents.count] + } + } +} + +enum PinnedEventsBannerState: Equatable { + case loading(numbersOfEvents: Int) + case loaded(state: PinnedEventsState) + + var isEmpty: Bool { + switch self { + case .loaded(let state): + return state.pinnedEventContents.isEmpty + case .loading(let numberOfEvents): + return numberOfEvents == 0 + } + } + + var isLoading: Bool { + switch self { + case .loading: + return true + default: + return false + } + } + + var selectedPinEventID: String? { + switch self { + case .loaded(let state): + return state.selectedPinEventID + default: + return nil + } + } + + var count: Int { + switch self { + case .loaded(let state): + return state.pinnedEventContents.count + case .loading(let numberOfEvents): + return numberOfEvents + } + } + + var selectedPinIndex: Int { + switch self { + case .loaded(let state): + return state.selectedPinIndex + case .loading(let numbersOfEvents): + // We always want the index to be the last one when loading, since is the default one. + return numbersOfEvents - 1 + } + } + + var displayedMessage: AttributedString { + switch self { + case .loading: + return AttributedString(L10n.screenRoomPinnedBannerLoadingDescription) + case .loaded(let state): + return state.selectedPinContent + } + } + + var bannerIndicatorDescription: AttributedString { + let index = selectedPinIndex + 1 + let boldPlaceholder = "{bold}" + var finalString = AttributedString(L10n.screenRoomPinnedBannerIndicatorDescription(boldPlaceholder)) + var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, count)) + boldString.bold() + finalString.replace(boldPlaceholder, with: boldString) + return finalString + } + + mutating func previousPin() { + switch self { + case .loaded(var state): + state.previousPin() + self = .loaded(state: state) + default: + break + } + } + + mutating func setPinnedEventContents(_ pinnedEventContents: OrderedDictionary) { + switch self { + case .loading: + // The default selected event should always be the last one. + self = .loaded(state: .init(pinnedEventContents: pinnedEventContents, selectedPinEventID: pinnedEventContents.keys.last)) + case .loaded(var state): + state.pinnedEventContents = pinnedEventContents + self = .loaded(state: state) + } + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift similarity index 99% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift rename to ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift index 42994abe2..0799bf384 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift @@ -127,7 +127,7 @@ class TimelineTableViewController: UIViewController { var isSwitchingTimelines = false /// The focussed event if navigating to an event permalink within the room. - var focussedEvent: TimelineViewState.FocussedEvent? { + var focussedEvent: TimelineState.FocussedEvent? { didSet { guard let focussedEvent, focussedEvent.appearance != .hasAppeared else { return } scrollToItem(eventID: focussedEvent.eventID, animated: focussedEvent.appearance == .animated) @@ -283,7 +283,7 @@ class TimelineTableViewController: UIViewController { .id(id) .frame(maxWidth: .infinity, alignment: .leading) .environmentObject(coordinator.context) // Attempted fix at a crash in TimelineItemContextMenu - .environment(\.roomContext, coordinator.context) + .environment(\.timelineContext, coordinator.context) } .margins(.all, 0) // Margins are handled in the stylers .minSize(height: 1) diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift new file mode 100644 index 000000000..82e7c3fb2 --- /dev/null +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -0,0 +1,962 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Algorithms +import Combine +import OrderedCollections +import SwiftUI + +typealias TimelineViewModelType = StateStoreViewModel + +class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { + private enum Constants { + static let paginationEventLimit: UInt16 = 20 + static let detachedTimelineSize: UInt16 = 100 + static let focusTimelineToastIndicatorID = "RoomScreenFocusTimelineToastIndicator" + static let toastErrorID = "RoomScreenToastError" + } + + private let roomProxy: RoomProxyProtocol + private let timelineController: RoomTimelineControllerProtocol + private let mediaPlayerProvider: MediaPlayerProviderProtocol + private let userIndicatorController: UserIndicatorControllerProtocol + private let appMediator: AppMediatorProtocol + private let appSettings: AppSettings + private let analyticsService: AnalyticsService + private let pinnedEventStringBuilder: RoomEventStringBuilder + + private let timelineInteractionHandler: TimelineInteractionHandler + + private let composerFocusedSubject = PassthroughSubject() + + private let actionsSubject: PassthroughSubject = .init() + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + private var paginateBackwardsTask: Task? + private var paginateForwardsTask: Task? + + private var pinnedEventsTimelineProvider: RoomTimelineProviderProtocol? { + didSet { + guard let pinnedEventsTimelineProvider else { + return + } + + buildPinnedEventContent(timelineItems: pinnedEventsTimelineProvider.itemProxies) + pinnedEventsTimelineProvider.updatePublisher + // When pinning or unpinning an item, the timeline might return empty for a short while, so we need to debounce it to prevent weird UI behaviours like the banner disappearing + .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) + .sink { [weak self] updatedItems, _ in + guard let self else { return } + buildPinnedEventContent(timelineItems: updatedItems) + } + .store(in: &cancellables) + } + } + + init(roomProxy: RoomProxyProtocol, + focussedEventID: String? = nil, + timelineController: RoomTimelineControllerProtocol, + mediaProvider: MediaProviderProtocol, + mediaPlayerProvider: MediaPlayerProviderProtocol, + voiceMessageMediaManager: VoiceMessageMediaManagerProtocol, + userIndicatorController: UserIndicatorControllerProtocol, + appMediator: AppMediatorProtocol, + appSettings: AppSettings, + analyticsService: AnalyticsService) { + self.timelineController = timelineController + self.mediaPlayerProvider = mediaPlayerProvider + self.roomProxy = roomProxy + self.appSettings = appSettings + self.analyticsService = analyticsService + self.userIndicatorController = userIndicatorController + self.appMediator = appMediator + pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID) + + let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider) + + timelineInteractionHandler = TimelineInteractionHandler(roomProxy: roomProxy, + timelineController: timelineController, + mediaProvider: mediaProvider, + mediaPlayerProvider: mediaPlayerProvider, + voiceMessageMediaManager: voiceMessageMediaManager, + voiceMessageRecorder: voiceMessageRecorder, + userIndicatorController: userIndicatorController, + appMediator: appMediator, + appSettings: appSettings, + analyticsService: analyticsService) + + super.init(initialViewState: TimelineViewState(roomID: roomProxy.id, + roomTitle: roomProxy.roomTitle, + roomAvatar: roomProxy.avatar, + isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom, + timelineViewState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }), + ownUserID: roomProxy.ownUserID, + isViewSourceEnabled: appSettings.viewSourceEnabled, + hasOngoingCall: roomProxy.hasOngoingCall, + bindings: .init(reactionsCollapsed: [:])), + imageProvider: mediaProvider) + + if focussedEventID != nil { + // The timeline controller will start loading a detached timeline. + showFocusLoadingIndicator() + } + + setupSubscriptions() + setupDirectRoomSubscriptionsIfNeeded() + + // Set initial values for redacting from the macOS context menu. + Task { await updatePermissions() } + + state.audioPlayerStateProvider = { [weak self] itemID -> AudioPlayerState? in + guard let self else { + return nil + } + + return self.timelineInteractionHandler.audioPlayerState(for: itemID) + } + + buildTimelineViews(timelineItems: timelineController.timelineItems) + + updateMembers(roomProxy.membersPublisher.value) + + // Note: beware if we get to e.g. restore a reply / edit, + // maybe we are tracking a non-needed first initial state + trackComposerMode(.default) + + Task { + let userID = roomProxy.ownUserID + if case let .success(permission) = await roomProxy.canUserJoinCall(userID: userID) { + state.canJoinCall = permission + } + } + } + + // MARK: - Public + + func stop() { + // Work around QLPreviewController dismissal issues, see the InteractiveQuickLookModifier. + state.bindings.mediaPreviewItem = nil + } + + override func process(viewAction: TimelineViewAction) { + switch viewAction { + case .itemAppeared(let id): + Task { await timelineController.processItemAppearance(id) } + case .itemDisappeared(let id): + Task { await timelineController.processItemDisappearance(id) } + + case .itemTapped(let id): + Task { await handleItemTapped(with: id) } + case .itemSendInfoTapped(let itemID): + handleItemSendInfoTapped(itemID: itemID) + case .toggleReaction(let emoji, let itemId): + Task { await timelineController.toggleReaction(emoji, to: itemId) } + case .sendReadReceiptIfNeeded(let lastVisibleItemID): + Task { await sendReadReceiptIfNeeded(for: lastVisibleItemID) } + case .paginateBackwards: + paginateBackwards() + case .paginateForwards: + paginateForwards() + case .scrollToBottom: + scrollToBottom() + + case .displayTimelineItemMenu(let itemID): + timelineInteractionHandler.displayTimelineItemActionMenu(for: itemID) + case .handleTimelineItemMenuAction(let itemID, let action): + timelineInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID) + + case .displayRoomDetails: + actionsSubject.send(.displayRoomDetails) + case .displayRoomMemberDetails(userID: let userID): + Task { await timelineInteractionHandler.displayRoomMemberDetails(userID: userID) } + case .displayEmojiPicker(let itemID): + timelineInteractionHandler.displayEmojiPicker(for: itemID) + case .displayReactionSummary(let itemID, let key): + displayReactionSummary(for: itemID, selectedKey: key) + case .displayReadReceipts(itemID: let itemID): + displayReadReceipts(for: itemID) + case .displayCall: + actionsSubject.send(.displayCallScreen) + analyticsService.trackInteraction(name: .MobileRoomCallButton) + case .handlePasteOrDrop(let provider): + timelineInteractionHandler.handlePasteOrDrop(provider) + case .handlePollAction(let pollAction): + handlePollAction(pollAction) + case .handleAudioPlayerAction(let audioPlayerAction): + handleAudioPlayerAction(audioPlayerAction) + + case .focusOnEventID(let eventID): + Task { await focusOnEvent(eventID: eventID) } + case .focusLive: + focusLive() + case .scrolledToFocussedItem: + didScrollToFocussedItem() + case .hasSwitchedTimeline: + Task { state.timelineViewState.isSwitchingTimelines = false } + case let .hasScrolled(direction): + state.lastScrollDirection = direction + case .tappedPinnedEventsBanner: + if let eventID = state.pinnedEventsBannerState.selectedPinEventID { + Task { await focusOnEvent(eventID: eventID) } + } + state.pinnedEventsBannerState.previousPin() + case .viewAllPins: + actionsSubject.send(.displayPinnedEventsTimeline) + } + } + + func process(composerAction: ComposerToolbarViewModelAction) { + switch composerAction { + case .sendMessage(let message, let html, let mode, let intentionalMentions): + Task { + await sendCurrentMessage(message, + html: html, + mode: mode, + intentionalMentions: intentionalMentions) + } + case .editLastMessage: + editLastMessage() + case .attach(let attachment): + attach(attachment) + case .handlePasteOrDrop(let provider): + timelineInteractionHandler.handlePasteOrDrop(provider) + case .composerModeChanged(mode: let mode): + trackComposerMode(mode) + case .composerFocusedChanged(isFocused: let isFocused): + composerFocusedSubject.send(isFocused) + case .voiceMessage(let voiceMessageAction): + processVoiceMessageAction(voiceMessageAction) + case .contentChanged(let isEmpty): + guard appSettings.sharePresence else { + return + } + + Task { + await roomProxy.sendTypingNotification(isTyping: !isEmpty) + } + } + } + + func focusOnEvent(eventID: String) async { + if state.timelineViewState.hasLoadedItem(with: eventID) { + state.timelineViewState.focussedEvent = .init(eventID: eventID, appearance: .animated) + return + } + + showFocusLoadingIndicator() + defer { hideFocusLoadingIndicator() } + + switch await timelineController.focusOnEvent(eventID, timelineSize: Constants.detachedTimelineSize) { + case .success: + state.timelineViewState.focussedEvent = .init(eventID: eventID, appearance: .immediate) + case .failure(let error): + MXLog.error("Failed to focus on event \(eventID)") + + if case .eventNotFound = error { + displayErrorToast(L10n.errorMessageNotFound) + } else { + displayErrorToast(L10n.commonFailed) + } + } + } + + // MARK: - Private + + private func focusLive() { + timelineController.focusLive() + } + + private func didScrollToFocussedItem() { + if var focussedEvent = state.timelineViewState.focussedEvent { + focussedEvent.appearance = .hasAppeared + state.timelineViewState.focussedEvent = focussedEvent + hideFocusLoadingIndicator() + } + } + + private func editLastMessage() { + guard let item = timelineController.timelineItems.reversed().first(where: { + guard let item = $0 as? EventBasedMessageTimelineItemProtocol else { + return false + } + + return item.sender.id == roomProxy.ownUserID && item.isEditable + }) else { + return + } + + timelineInteractionHandler.handleTimelineItemMenuAction(.edit, itemID: item.id) + } + + private func attach(_ attachment: ComposerAttachmentType) { + switch attachment { + case .camera: + actionsSubject.send(.displayCameraPicker) + case .photoLibrary: + actionsSubject.send(.displayMediaPicker) + case .file: + actionsSubject.send(.displayDocumentPicker) + case .location: + actionsSubject.send(.displayLocationPicker) + case .poll: + actionsSubject.send(.displayPollForm(mode: .new)) + } + } + + private func handlePollAction(_ action: TimelineViewPollAction) { + switch action { + case let .selectOption(pollStartID, optionID): + timelineInteractionHandler.sendPollResponse(pollStartID: pollStartID, optionID: optionID) + case let .end(pollStartID): + displayAlert(.pollEndConfirmation(pollStartID)) + case .edit(let pollStartID, let poll): + actionsSubject.send(.displayPollForm(mode: .edit(eventID: pollStartID, poll: poll))) + } + } + + private func handleAudioPlayerAction(_ action: TimelineAudioPlayerAction) { + switch action { + case .playPause(let itemID): + Task { await timelineInteractionHandler.playPauseAudio(for: itemID) } + case .seek(let itemID, let progress): + Task { await timelineInteractionHandler.seekAudio(for: itemID, progress: progress) } + } + } + + private func processVoiceMessageAction(_ action: ComposerToolbarVoiceMessageAction) { + switch action { + case .startRecording: + Task { + await mediaPlayerProvider.detachAllStates(except: nil) + await timelineInteractionHandler.startRecordingVoiceMessage() + } + case .stopRecording: + Task { await timelineInteractionHandler.stopRecordingVoiceMessage() } + case .cancelRecording: + Task { await timelineInteractionHandler.cancelRecordingVoiceMessage() } + case .deleteRecording: + Task { await timelineInteractionHandler.deleteCurrentVoiceMessage() } + case .send: + Task { await timelineInteractionHandler.sendCurrentVoiceMessage() } + case .startPlayback: + Task { await timelineInteractionHandler.startPlayingRecordedVoiceMessage() } + case .pausePlayback: + timelineInteractionHandler.pausePlayingRecordedVoiceMessage() + case .seekPlayback(let progress): + Task { await timelineInteractionHandler.seekRecordedVoiceMessage(to: progress) } + case .scrubPlayback(let scrubbing): + Task { await timelineInteractionHandler.scrubVoiceMessagePlayback(scrubbing: scrubbing) } + } + } + + private func updateMembers(_ members: [RoomMemberProxyProtocol]) { + state.members = members.reduce(into: [String: RoomMemberState]()) { dictionary, member in + dictionary[member.userID] = RoomMemberState(displayName: member.displayName, avatarURL: member.avatarURL) + } + } + + private func updatePermissions() async { + if case let .success(value) = await roomProxy.canUserRedactOther(userID: roomProxy.ownUserID) { + state.canCurrentUserRedactOthers = value + } else { + state.canCurrentUserRedactOthers = false + } + + if case let .success(value) = await roomProxy.canUserRedactOwn(userID: roomProxy.ownUserID) { + state.canCurrentUserRedactSelf = value + } else { + state.canCurrentUserRedactSelf = false + } + + if state.isPinningEnabled, + case let .success(value) = await roomProxy.canUserPinOrUnpin(userID: roomProxy.ownUserID) { + state.canCurrentUserPin = value + } else { + state.canCurrentUserPin = false + } + } + + private func setupSubscriptions() { + timelineController.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + guard let self else { return } + + switch callback { + case .updatedTimelineItems(let updatedItems, let isSwitchingTimelines): + buildTimelineViews(timelineItems: updatedItems, isSwitchingTimelines: isSwitchingTimelines) + case .paginationState(let paginationState): + if state.timelineViewState.paginationState != paginationState { + state.timelineViewState.paginationState = paginationState + } + case .isLive(let isLive): + if state.timelineViewState.isLive != isLive { + state.timelineViewState.isLive = isLive + + // Remove the event highlight *only* when transitioning from non-live to live. + if isLive, state.timelineViewState.focussedEvent != nil { + state.timelineViewState.focussedEvent = nil + } + } + } + } + .store(in: &cancellables) + + let roomInfoSubscription = roomProxy + .actionsPublisher + .filter { $0 == .roomInfoUpdate } + + roomInfoSubscription + .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) + .sink { [weak self] _ in + guard let self else { return } + state.roomTitle = roomProxy.roomTitle + state.roomAvatar = roomProxy.avatar + state.hasOngoingCall = roomProxy.hasOngoingCall + } + .store(in: &cancellables) + + Task { [weak self] in + // Don't guard let self here, otherwise the for await will strongify the self reference creating a strong reference cycle. + // If the subscription has sent a value before the Task has started it might be lost, so before entering the loop we always do an update. + await self?.updatePinnedEventIDs() + for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values { + guard !Task.isCancelled else { + return + } + await self?.updatePinnedEventIDs() + } + } + .store(in: &cancellables) + + setupAppSettingsSubscriptions() + + roomProxy.membersPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.updateMembers($0) } + .store(in: &cancellables) + + roomProxy.typingMembersPublisher + .receive(on: DispatchQueue.main) + .filter { [weak self] _ in self?.appSettings.sharePresence ?? false } + .weakAssign(to: \.state.typingMembers, on: self) + .store(in: &cancellables) + + timelineInteractionHandler.actions + .receive(on: DispatchQueue.main) + .sink { [weak self] action in + guard let self else { return } + + switch action { + case .composer(let action): + actionsSubject.send(.composer(action: action)) + case .displayAudioRecorderPermissionError: + displayAlert(.audioRecodingPermissionError) + case .displayErrorToast(let title): + displayErrorToast(title) + case .displayEmojiPicker(let itemID, let selectedEmojis): + actionsSubject.send(.displayEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis)) + case .displayMessageForwarding(let itemID): + Task { await self.forwardMessage(itemID: itemID) } + case .displayPollForm(let mode): + actionsSubject.send(.displayPollForm(mode: mode)) + case .displayReportContent(let itemID, let senderID): + actionsSubject.send(.displayReportContent(itemID: itemID, senderID: senderID)) + case .displayMediaUploadPreviewScreen(let url): + actionsSubject.send(.displayMediaUploadPreviewScreen(url: url)) + case .displayRoomMemberDetails(userID: let userID): + actionsSubject.send(.displayRoomMemberDetails(userID: userID)) + case .showActionMenu(let actionMenuInfo): + Task { + await self.updatePermissions() + self.state.bindings.actionMenuInfo = actionMenuInfo + } + case .showDebugInfo(let debugInfo): + state.bindings.debugInfo = debugInfo + } + } + .store(in: &cancellables) + + appSettings.$pinningEnabled + .combineLatest(appMediator.networkMonitor.reachabilityPublisher) + .filter { $0.0 && $0.1 == .reachable } + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.setupPinnedEventsTimelineProviderIfNeeded() + } + .store(in: &cancellables) + } + + private func setupAppSettingsSubscriptions() { + appSettings.$sharePresence + .weakAssign(to: \.state.showReadReceipts, on: self) + .store(in: &cancellables) + + appSettings.$viewSourceEnabled + .weakAssign(to: \.state.isViewSourceEnabled, on: self) + .store(in: &cancellables) + + appSettings.$pinningEnabled + .weakAssign(to: \.state.isPinningEnabled, on: self) + .store(in: &cancellables) + } + + private func setupPinnedEventsTimelineProviderIfNeeded() { + guard pinnedEventsTimelineProvider == nil else { + return + } + + Task { + guard let timelineProvider = await roomProxy.pinnedEventsTimeline?.timelineProvider else { + return + } + + if pinnedEventsTimelineProvider == nil { + pinnedEventsTimelineProvider = timelineProvider + } + } + } + + private func updatePinnedEventIDs() async { + let pinnedEventIDs = await roomProxy.pinnedEventIDs + // Only update the loading state of the banner + if state.pinnedEventsBannerState.isLoading { + state.pinnedEventsBannerState = .loading(numbersOfEvents: pinnedEventIDs.count) + } + state.pinnedEventIDs = pinnedEventIDs + } + + private func setupDirectRoomSubscriptionsIfNeeded() { + guard roomProxy.isDirect else { + return + } + + let shouldShowInviteAlert = composerFocusedSubject + .removeDuplicates() + .map { [weak self] isFocused in + guard let self else { return false } + + return isFocused && self.roomProxy.isUserAloneInDirectRoom + } + // We want to show the alert just once, so we are taking the first "true" emitted + .first { $0 } + + shouldShowInviteAlert + .sink { [weak self] _ in + self?.showInviteAlert() + } + .store(in: &cancellables) + } + + private func paginateBackwards() { + guard paginateBackwardsTask == nil else { + return + } + + paginateBackwardsTask = Task { [weak self] in + guard let self else { + return + } + + switch await timelineController.paginateBackwards(requestSize: Constants.paginationEventLimit) { + case .failure: + displayErrorToast(L10n.errorFailedLoadingMessages) + default: + break + } + paginateBackwardsTask = nil + } + } + + private func paginateForwards() { + guard paginateForwardsTask == nil else { + return + } + + paginateForwardsTask = Task { [weak self] in + guard let self else { + return + } + + switch await timelineController.paginateForwards(requestSize: Constants.paginationEventLimit) { + case .failure: + displayErrorToast(L10n.errorFailedLoadingMessages) + default: + break + } + + if state.timelineViewState.paginationState.forward == .timelineEndReached { + focusLive() + } + + paginateForwardsTask = nil + } + } + + private func scrollToBottom() { + if state.timelineViewState.isLive { + state.timelineViewState.scrollToBottomPublisher.send(()) + } else { + focusLive() + } + } + + private func sendReadReceiptIfNeeded(for lastVisibleItemID: TimelineItemIdentifier) async { + guard appMediator.appState == .active else { return } + + await timelineController.sendReadReceipt(for: lastVisibleItemID) + } + + private func handleItemTapped(with itemID: TimelineItemIdentifier) async { + state.showLoading = true + let action = await timelineInteractionHandler.processItemTap(itemID) + + switch action { + case .displayMediaFile(let file, let title): + 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): + actionsSubject.send(.displayLocation(body: body, geoURI: geoURI, description: description)) + case .none: + break + } + state.showLoading = false + } + + private func handleItemSendInfoTapped(itemID: TimelineItemIdentifier) { + guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) else { + MXLog.warning("Couldn't find timeline item.") + return + } + + guard let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { + fatalError("Only events can have send info.") + } + + if eventTimelineItem.properties.deliveryStatus == .sendingFailed { + displayAlert(.sendingFailed) + } else if let authenticityMessage = eventTimelineItem.properties.encryptionAuthenticity?.message { + displayAlert(.encryptionAuthenticity(authenticityMessage)) + } + } + + private func sendCurrentMessage(_ message: String, html: String?, mode: ComposerMode, intentionalMentions: IntentionalMentions) async { + guard !message.isEmpty else { + fatalError("This message should never be empty") + } + + actionsSubject.send(.composer(action: .clear)) + + switch mode { + case .reply(let itemId, _, _): + await timelineController.sendMessage(message, + html: html, + inReplyTo: itemId, + intentionalMentions: intentionalMentions) + case .edit(let originalItemId): + await timelineController.edit(originalItemId, + message: message, + html: html, + intentionalMentions: intentionalMentions) + case .default: + await timelineController.sendMessage(message, + html: html, + intentionalMentions: intentionalMentions) + case .recordVoiceMessage, .previewVoiceMessage: + fatalError("invalid composer mode.") + } + + scrollToBottom() + } + + private func trackComposerMode(_ mode: ComposerMode) { + var isEdit = false + var isReply = false + switch mode { + case .edit: + isEdit = true + case .reply: + isReply = true + default: + break + } + + analyticsService.trackComposer(inThread: false, isEditing: isEdit, isReply: isReply, startsThread: nil) + } + + // MARK: - Timeline Item Building + + private func buildPinnedEventContent(timelineItems: [TimelineItemProxy]) { + var pinnedEventContents = OrderedDictionary() + + for item in timelineItems { + // Only remote events are pinned + if case let .event(event) = item, + let eventID = event.id.eventID { + pinnedEventContents.updateValue(pinnedEventStringBuilder.buildAttributedString(for: event) ?? AttributedString(L10n.commonUnsupportedEvent), + forKey: eventID) + } + } + + state.pinnedEventsBannerState.setPinnedEventContents(pinnedEventContents) + } + + private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) { + var timelineItemsDictionary = OrderedDictionary() + + timelineItems.filter { $0 is RedactedRoomTimelineItem }.forEach { timelineItem in + // Stops the audio player when a voice message is redacted. + guard let playerState = mediaPlayerProvider.playerState(for: .timelineItemIdentifier(timelineItem.id)) else { + return + } + + Task { @MainActor in + playerState.detachAudioPlayer() + mediaPlayerProvider.unregister(audioPlayerState: playerState) + } + } + + let itemsGroupedByTimelineDisplayStyle = timelineItems.chunked { current, next in + canGroupItem(timelineItem: current, with: next) + } + + for itemGroup in itemsGroupedByTimelineDisplayStyle { + guard !itemGroup.isEmpty else { + MXLog.error("Found empty item group") + continue + } + + if itemGroup.count == 1 { + if let firstItem = itemGroup.first { + timelineItemsDictionary.updateValue(updateViewState(item: firstItem, groupStyle: .single), + forKey: firstItem.id.timelineID) + } + } else { + for (index, item) in itemGroup.enumerated() { + if index == 0 { + timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: .first), + forKey: item.id.timelineID) + } else if index == itemGroup.count - 1 { + timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: .last), + forKey: item.id.timelineID) + } else { + timelineItemsDictionary.updateValue(updateViewState(item: item, groupStyle: .middle), + forKey: item.id.timelineID) + } + } + } + } + + if isSwitchingTimelines { + state.timelineViewState.isSwitchingTimelines = true + } + + state.timelineViewState.itemsDictionary = timelineItemsDictionary + } + + private func updateViewState(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) -> RoomTimelineItemViewState { + if let timelineItemViewState = state.timelineViewState.itemsDictionary[item.id.timelineID] { + timelineItemViewState.groupStyle = groupStyle + timelineItemViewState.type = .init(item: item) + return timelineItemViewState + } else { + return RoomTimelineItemViewState(item: item, groupStyle: groupStyle) + } + } + + private func canGroupItem(timelineItem: RoomTimelineItemProtocol, with otherTimelineItem: RoomTimelineItemProtocol) -> Bool { + if timelineItem is CollapsibleTimelineItem || otherTimelineItem is CollapsibleTimelineItem { + return false + } + + guard let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol, + let otherEventTimelineItem = otherTimelineItem as? EventBasedTimelineItemProtocol else { + return false + } + + // State events aren't rendered as messages so shouldn't be grouped. + if eventTimelineItem is StateRoomTimelineItem || otherEventTimelineItem is StateRoomTimelineItem { + return false + } + + // can be improved by adding a date threshold + return eventTimelineItem.properties.reactions.isEmpty && eventTimelineItem.sender == otherEventTimelineItem.sender + } + + // MARK: - Direct chats logics + + private func showInviteAlert() { + userIndicatorController.alertInfo = .init(id: .init(), + title: L10n.screenRoomInviteAgainAlertTitle, + message: L10n.screenRoomInviteAgainAlertMessage, + primaryButton: .init(title: L10n.actionInvite, action: { [weak self] in self?.inviteOtherDMUserBack() }), + secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) + } + + private let inviteLoadingIndicatorID = UUID().uuidString + + private func inviteOtherDMUserBack() { + guard roomProxy.isUserAloneInDirectRoom else { + userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError) + return + } + + Task { + userIndicatorController.submitIndicator(.init(id: inviteLoadingIndicatorID, type: .toast, title: L10n.commonLoading)) + defer { + userIndicatorController.retractIndicatorWithId(inviteLoadingIndicatorID) + } + + guard + let members = await roomProxy.members(), + members.count == 2, + let otherPerson = members.first(where: { $0.userID != roomProxy.ownUserID && $0.membership == .leave }) + else { + userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError) + return + } + + switch await roomProxy.invite(userID: otherPerson.userID) { + case .success: + break + case .failure: + userIndicatorController.alertInfo = .init(id: .init(), + title: L10n.commonUnableToInviteTitle, + message: L10n.commonUnableToInviteMessage) + } + } + } + + // MARK: - Reactions + + private func displayReactionSummary(for itemID: TimelineItemIdentifier, selectedKey: String) { + guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), + let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { + return + } + + state.bindings.reactionSummaryInfo = .init(reactions: eventTimelineItem.properties.reactions, selectedKey: selectedKey) + } + + // MARK: - Read Receipts + + private func displayReadReceipts(for itemID: TimelineItemIdentifier) { + guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), + let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { + return + } + + state.bindings.readReceiptsSummaryInfo = .init(orderedReceipts: eventTimelineItem.properties.orderedReadReceipts, id: eventTimelineItem.id) + } + + // MARK: - Message forwarding + + private func forwardMessage(itemID: TimelineItemIdentifier) async { + guard let content = await timelineController.messageEventContent(for: itemID) else { return } + actionsSubject.send(.displayMessageForwarding(forwardingItem: .init(id: itemID, roomID: roomProxy.id, content: content))) + } + + // MARK: - User Indicators + + private func showFocusLoadingIndicator() { + userIndicatorController.submitIndicator(UserIndicator(id: Constants.focusTimelineToastIndicatorID, + type: .toast(progress: .indeterminate), + title: L10n.commonLoading, + persistent: true)) + } + + private func hideFocusLoadingIndicator() { + userIndicatorController.retractIndicatorWithId(Constants.focusTimelineToastIndicatorID) + } + + private func displayAlert(_ type: RoomScreenAlertInfoType) { + switch type { + case .audioRecodingPermissionError: + state.bindings.alertInfo = .init(id: type, + title: L10n.dialogPermissionMicrophoneTitleIos(InfoPlistReader.main.bundleDisplayName), + message: L10n.dialogPermissionMicrophoneDescriptionIos, + primaryButton: .init(title: L10n.commonSettings, action: { [weak self] in self?.appMediator.openAppSettings() }), + secondaryButton: .init(title: L10n.actionNotNow, role: .cancel, action: nil)) + case .pollEndConfirmation(let pollStartID): + state.bindings.alertInfo = .init(id: type, + title: L10n.actionEndPoll, + message: L10n.commonPollEndConfirmation, + primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), + secondaryButton: .init(title: L10n.actionOk, action: { self.timelineInteractionHandler.endPoll(pollStartID: pollStartID) })) + case .sendingFailed: + state.bindings.alertInfo = .init(id: type, + title: L10n.commonSendingFailed, + primaryButton: .init(title: L10n.actionOk, action: nil)) + case .encryptionAuthenticity(let message): + state.bindings.alertInfo = .init(id: type, + title: message, + primaryButton: .init(title: L10n.actionOk, action: nil)) + } + } + + private func displayErrorToast(_ title: String) { + userIndicatorController.submitIndicator(UserIndicator(id: Constants.toastErrorID, + type: .toast, + title: title, + iconName: "xmark")) + } +} + +private extension RoomProxyProtocol { + /// Checks if the other person left the room in a direct chat + var isUserAloneInDirectRoom: Bool { + isDirect && activeMembersCount == 1 + } +} + +// MARK: - Mocks + +extension TimelineViewModel { + static let mock = TimelineViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room")), + focussedEventID: nil, + timelineController: MockRoomTimelineController(), + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: ServiceLocator.shared.userIndicatorController, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) +} + +private struct TimelineContextKey: EnvironmentKey { + @MainActor static let defaultValue: TimelineViewModel.Context? = nil +} + +private struct FocussedEventID: EnvironmentKey { + static let defaultValue: String? = nil +} + +extension EnvironmentValues { + /// Used to access and inject the room context without observing it + var timelineContext: TimelineViewModel.Context? { + get { self[TimelineContextKey.self] } + set { self[TimelineContextKey.self] = newValue } + } + + /// An event ID which will be non-nil when a timeline item should show as focussed. + var focussedEventID: String? { + get { self[FocussedEventID.self] } + set { self[FocussedEventID.self] = newValue } + } +} diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModelProtocol.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModelProtocol.swift new file mode 100644 index 000000000..85aa77cb7 --- /dev/null +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModelProtocol.swift @@ -0,0 +1,29 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import Foundation +import SwiftUI + +@MainActor +protocol TimelineViewModelProtocol { + var actions: AnyPublisher { get } + var context: TimelineViewModel.Context { get } + func process(composerAction: ComposerToolbarViewModelAction) + /// Updates the timeline to show and highlight the item with the corresponding event ID. + func focusOnEvent(eventID: String) async + func stop() +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMacContextMenu.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMacContextMenu.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMacContextMenu.swift rename to ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMacContextMenu.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenu.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift similarity index 98% rename from ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenu.swift rename to ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift index 1f007a43a..e1adbf978 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift @@ -18,7 +18,7 @@ import Compound import SwiftUI struct TimelineItemMenu: View { - @EnvironmentObject private var context: RoomScreenViewModel.Context + @EnvironmentObject private var context: TimelineViewModel.Context @Environment(\.dismiss) private var dismiss @State private var reactionsFrame = CGRect.zero @@ -191,7 +191,7 @@ private extension EncryptionAuthenticity { // MARK: - Previews struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static let (item, actions) = makeItem() static let (backupItem, _) = makeItem(authenticity: .notGuaranteed(color: .gray)) static let (unsignedItem, _) = makeItem(authenticity: .unsignedDevice(color: .red)) diff --git a/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenuAction.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenuAction.swift rename to ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenuActionProvider.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenuActionProvider.swift rename to ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Polls/PollOptionView.swift b/ElementX/Sources/Screens/Timeline/View/Polls/PollOptionView.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Polls/PollOptionView.swift rename to ElementX/Sources/Screens/Timeline/View/Polls/PollOptionView.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Polls/PollView.swift b/ElementX/Sources/Screens/Timeline/View/Polls/PollView.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Polls/PollView.swift rename to ElementX/Sources/Screens/Timeline/View/Polls/PollView.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptCell.swift b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptCell.swift rename to ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift similarity index 75% rename from ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift rename to ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift index 165c8aae9..5673860a7 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift +++ b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift @@ -18,7 +18,7 @@ import SwiftUI struct ReadReceiptsSummaryView: View { let orderedReadReceipts: [ReadReceipt] - @EnvironmentObject private var context: RoomScreenViewModel.Context + @EnvironmentObject private var context: TimelineViewModel.Context var body: some View { VStack(alignment: .leading, spacing: 16) { @@ -52,15 +52,15 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview { .mockDan ] let roomProxyMock = RoomProxyMock(.init(name: "Room", members: members)) - let mock = RoomScreenViewModel(roomProxy: roomProxyMock, - timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: UserIndicatorControllerMock(), - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + let mock = TimelineViewModel(roomProxy: roomProxyMock, + timelineController: MockRoomTimelineController(), + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: UserIndicatorControllerMock(), + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) return mock }() diff --git a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift b/ElementX/Sources/Screens/Timeline/View/Replies/TimelineReplyView.swift similarity index 99% rename from ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift rename to ElementX/Sources/Screens/Timeline/View/Replies/TimelineReplyView.swift index 7b670b280..31829c5fe 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Replies/TimelineReplyView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Replies/TimelineReplyView.swift @@ -122,7 +122,7 @@ struct TimelineReplyView: View { let cornerRadii: Double } - @EnvironmentObject private var context: RoomScreenViewModel.Context + @EnvironmentObject private var context: TimelineViewModel.Context @ScaledMetric private var imageContainerSize = 36.0 let sender: TimelineItemSender @@ -216,7 +216,7 @@ struct TimelineReplyView: View { } struct TimelineReplyView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static let attributedStringWithMention = { var attributedString = AttributedString("To be replaced") diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/LongPressWithFeedback.swift b/ElementX/Sources/Screens/Timeline/View/Style/LongPressWithFeedback.swift similarity index 98% rename from ElementX/Sources/Screens/RoomScreen/View/Style/LongPressWithFeedback.swift rename to ElementX/Sources/Screens/Timeline/View/Style/LongPressWithFeedback.swift index 6c68f1d3e..0dc187e42 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/LongPressWithFeedback.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/LongPressWithFeedback.swift @@ -67,7 +67,7 @@ struct LongPressWithFeedback_Previews: PreviewProvider, TestablePreview { static var previews: some View { Preview() } struct Preview: View { - private let viewModel = RoomScreenViewModel.mock + private let viewModel = TimelineViewModel.mock @State private var isPresentingSheet = false var body: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/SwipeToReplyView.swift b/ElementX/Sources/Screens/Timeline/View/Style/SwipeToReplyView.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Style/SwipeToReplyView.swift rename to ElementX/Sources/Screens/Timeline/View/Style/SwipeToReplyView.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/ThreadDecorator.swift b/ElementX/Sources/Screens/Timeline/View/Style/ThreadDecorator.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Style/ThreadDecorator.swift rename to ElementX/Sources/Screens/Timeline/View/Style/ThreadDecorator.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineBubbleLayout.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineBubbleLayout.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Style/TimelineBubbleLayout.swift rename to ElementX/Sources/Screens/Timeline/View/Style/TimelineBubbleLayout.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemAccessibilityModifier.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemAccessibilityModifier.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemAccessibilityModifier.swift rename to ElementX/Sources/Screens/Timeline/View/Style/TimelineItemAccessibilityModifier.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift similarity index 99% rename from ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift rename to ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index 088369785..66b76abd4 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -18,7 +18,7 @@ import Compound import SwiftUI struct TimelineItemBubbledStylerView: View { - @EnvironmentObject private var context: RoomScreenViewModel.Context + @EnvironmentObject private var context: TimelineViewModel.Context @Environment(\.timelineGroupStyle) private var timelineGroupStyle @Environment(\.focussedEventID) private var focussedEventID @@ -318,7 +318,7 @@ private extension EdgeInsets { // MARK: - Previews struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { mockTimeline diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemSendInfoLabel.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift similarity index 98% rename from ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemSendInfoLabel.swift rename to ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift index d12b7d37f..853dfef82 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemSendInfoLabel.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift @@ -21,7 +21,7 @@ extension View { /// Adds the send info (timestamp along indicators for edits and delivery/encryption issues) for the given timeline item to this view. func timelineItemSendInfo(timelineItem: EventBasedTimelineItemProtocol, adjustedDeliveryStatus: TimelineItemDeliveryStatus?, - context: RoomScreenViewModel.Context) -> some View { + context: TimelineViewModel.Context) -> some View { modifier(TimelineItemSendInfoModifier(sendInfo: .init(timelineItem: timelineItem, adjustedDeliveryStatus: adjustedDeliveryStatus), context: context)) @@ -31,7 +31,7 @@ extension View { /// Adds the send info to a view with the correct layout. private struct TimelineItemSendInfoModifier: ViewModifier { let sendInfo: TimelineItemSendInfo - let context: RoomScreenViewModel.Context + let context: TimelineViewModel.Context var layout: AnyLayout { switch sendInfo.layoutType { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyle.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyle.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyle.swift rename to ElementX/Sources/Screens/Timeline/View/Style/TimelineStyle.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift similarity index 99% rename from ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift rename to ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift index a9216b508..86c620dab 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyler.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift @@ -62,7 +62,7 @@ struct TimelineStyler: View { } struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static let base = TextRoomTimelineItem(id: .random, timestamp: "Now", diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/ReactionsSummaryView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/ReactionsSummaryView.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Supplementary/ReactionsSummaryView.swift rename to ElementX/Sources/Screens/Timeline/View/Supplementary/ReactionsSummaryView.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineDeliveryStatusView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineDeliveryStatusView.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineDeliveryStatusView.swift rename to ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineDeliveryStatusView.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemStatusView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift similarity index 96% rename from ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemStatusView.swift rename to ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift index cf400c7ba..660ccf090 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemStatusView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift @@ -20,7 +20,7 @@ import SwiftUI struct TimelineItemStatusView: View { let timelineItem: EventBasedTimelineItemProtocol let adjustedDeliveryStatus: TimelineItemDeliveryStatus? - @EnvironmentObject private var context: RoomScreenViewModel.Context + @EnvironmentObject private var context: TimelineViewModel.Context private var isLastOutgoingMessage: Bool { timelineItem.isOutgoing && context.viewState.timelineViewState.timelineIDs.last == timelineItem.id.timelineID diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift similarity index 95% rename from ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift rename to ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift index f0b83a6f9..592594e01 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReactionsView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift @@ -22,14 +22,14 @@ struct TimelineReactionsView: View { private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) @Environment(\.layoutDirection) private var layoutDirection: LayoutDirection - let context: RoomScreenViewModel.Context + let context: TimelineViewModel.Context let itemID: TimelineItemIdentifier let reactions: [AggregatedReaction] let isLayoutRTL: Bool private var collapsed: Binding - init(context: RoomScreenViewModel.Context, + init(context: TimelineViewModel.Context, itemID: TimelineItemIdentifier, reactions: [AggregatedReaction], isLayoutRTL: Bool = false) { @@ -204,20 +204,20 @@ struct TimelineReactionAddMoreButtonLabel: View { struct TimelineReactionViewPreviewsContainer: View { var body: some View { VStack(spacing: 8) { - TimelineReactionsView(context: RoomScreenViewModel.mock.context, + TimelineReactionsView(context: TimelineViewModel.mock.context, itemID: .init(timelineID: "1"), reactions: [AggregatedReaction.mockReactionWithLongText, AggregatedReaction.mockReactionWithLongTextRTL]) Divider() - TimelineReactionsView(context: RoomScreenViewModel.mock.context, + TimelineReactionsView(context: TimelineViewModel.mock.context, itemID: .init(timelineID: "2"), reactions: Array(AggregatedReaction.mockReactions.prefix(3))) Divider() - TimelineReactionsView(context: RoomScreenViewModel.mock.context, + TimelineReactionsView(context: TimelineViewModel.mock.context, itemID: .init(timelineID: "3"), reactions: AggregatedReaction.mockReactions) Divider() - TimelineReactionsView(context: RoomScreenViewModel.mock.context, + TimelineReactionsView(context: TimelineViewModel.mock.context, itemID: .init(timelineID: "4"), reactions: AggregatedReaction.mockReactions, isLayoutRTL: true) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift similarity index 87% rename from ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift rename to ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift index e4933ee34..2a82468b0 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift @@ -19,7 +19,7 @@ import SwiftUI struct TimelineReadReceiptsView: View { let displayNumber = 3 let timelineItem: EventBasedTimelineItemProtocol - @EnvironmentObject private var context: RoomScreenViewModel.Context + @EnvironmentObject private var context: TimelineViewModel.Context var body: some View { HStack(spacing: 2) { @@ -90,15 +90,15 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { .mockMe ] - static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Test", members: members)), - timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + static let viewModel = TimelineViewModel(roomProxy: RoomProxyMock(.init(name: "Test", members: members)), + timelineController: MockRoomTimelineController(), + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: ServiceLocator.shared.userIndicatorController, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")] static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"), diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemDebugView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemDebugView.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/TimelineItemDebugView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemDebugView.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/AudioRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift similarity index 97% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/AudioRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift index 5367a295d..88ebafaa7 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/AudioRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/AudioRoomTimelineView.swift @@ -36,7 +36,7 @@ struct AudioRoomTimelineView: View { } struct AudioRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { body.environmentObject(viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/CallInviteRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallInviteRoomTimelineView.swift similarity index 97% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/CallInviteRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallInviteRoomTimelineView.swift index ec28835fc..8af71980c 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/CallInviteRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallInviteRoomTimelineView.swift @@ -32,7 +32,7 @@ struct CallInviteRoomTimelineView: View { } struct CallInviteRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { body.environmentObject(viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/CallNotificationRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift similarity index 96% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/CallNotificationRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift index 4452c056e..3b07f9e12 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/CallNotificationRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift @@ -19,7 +19,7 @@ import Foundation import SwiftUI struct CallNotificationRoomTimelineView: View { - @EnvironmentObject private var context: RoomScreenViewModel.Context + @EnvironmentObject private var context: TimelineViewModel.Context let timelineItem: CallNotificationRoomTimelineItem @@ -62,7 +62,7 @@ struct CallNotificationRoomTimelineView: View { } struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { body.environmentObject(viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/CollapsibleRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CollapsibleRoomTimelineView.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/CollapsibleRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CollapsibleRoomTimelineView.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EmoteRoomTimelineView.swift similarity index 98% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EmoteRoomTimelineView.swift index 3d3057af5..cf2a70cc4 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EmoteRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EmoteRoomTimelineView.swift @@ -32,7 +32,7 @@ struct EmoteRoomTimelineView: View, TextBasedRoomTimelineViewProtocol { } struct EmoteRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { body.environmentObject(viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EncryptedRoomTimelineView.swift similarity index 98% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EncryptedRoomTimelineView.swift index c72d383c7..c10ae1a3a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/EncryptedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/EncryptedRoomTimelineView.swift @@ -56,7 +56,7 @@ struct RoomTimelineViewLabelStyle: LabelStyle { } struct EncryptedRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { body.environmentObject(viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift similarity index 98% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift index 8fd7d4714..ccb90c96c 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FileRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FileRoomTimelineView.swift @@ -36,7 +36,7 @@ struct FileRoomTimelineView: View { } struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { body.environmentObject(viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift similarity index 99% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift index fe569b33a..352a30525 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/FormattedBodyText.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift @@ -224,7 +224,7 @@ private struct PreviewBubbleModifier: ViewModifier { .padding(8) .background(Color.compound._bgBubbleOutgoing) .cornerRadius(12) - .environmentObject(RoomScreenViewModel.mock.context) + .environmentObject(TimelineViewModel.mock.context) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift similarity index 77% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift index 3152077a9..38aee5b64 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift @@ -93,21 +93,24 @@ struct HighlightedTimelineItemModifier_Previews: PreviewProvider, TestablePrevie /// A preview that allows quick testing of the highlight appearance across various timeline scenarios. struct HighlightedTimelineItemTimeline_Previews: PreviewProvider { + static let roomViewModel = RoomScreenViewModel.mock() static let focussedEventID = "RoomTimelineItemFixtures.default.5" - static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room")), - focussedEventID: focussedEventID, - timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + static let timelineViewModel = TimelineViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room")), + focussedEventID: focussedEventID, + timelineController: MockRoomTimelineController(), + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: ServiceLocator.shared.userIndicatorController, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) static var previews: some View { NavigationStack { - RoomScreen(context: viewModel.context, composerToolbar: ComposerToolbar.mock()) + RoomScreen(roomViewModel: roomViewModel, + timelineViewModel: timelineViewModel, + composerToolbar: ComposerToolbar.mock()) } .previewDisplayName("Timeline") } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift similarity index 97% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift index ffcf8e6cd..1490c5921 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ImageRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift @@ -18,7 +18,7 @@ import Foundation import SwiftUI struct ImageRoomTimelineView: View { - @EnvironmentObject private var context: RoomScreenViewModel.Context + @EnvironmentObject private var context: TimelineViewModel.Context let timelineItem: ImageRoomTimelineItem var body: some View { @@ -56,7 +56,7 @@ struct ImageRoomTimelineView: View { } struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { body diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift similarity index 99% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift index 3b70e530c..c38307b8a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/LocationRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift @@ -81,7 +81,7 @@ private extension MapLibreStaticMapView { } struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { ScrollView { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/NoticeRoomTimelineView.swift similarity index 98% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/NoticeRoomTimelineView.swift index 0232a638d..939a1450c 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/NoticeRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/NoticeRoomTimelineView.swift @@ -44,7 +44,7 @@ struct NoticeRoomTimelineView: View, TextBasedRoomTimelineViewProtocol { } struct NoticeRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { body.environmentObject(viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PaginationIndicatorRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PaginationIndicatorRoomTimelineView.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/PaginationIndicatorRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PaginationIndicatorRoomTimelineView.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift similarity index 96% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift index d7b830a80..056474531 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/PollRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift @@ -18,7 +18,7 @@ import SwiftUI struct PollRoomTimelineView: View { let timelineItem: PollRoomTimelineItem - @EnvironmentObject private var context: RoomScreenViewModel.Context + @EnvironmentObject private var context: TimelineViewModel.Context var body: some View { TimelineStyler(timelineItem: timelineItem) { @@ -50,7 +50,7 @@ struct PollRoomTimelineView: View { } struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(), isOutgoing: false)) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ReadMarkerRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift similarity index 98% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/ReadMarkerRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift index ce552baba..87233653e 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/ReadMarkerRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift @@ -36,7 +36,7 @@ struct ReadMarkerRoomTimelineView: View { } struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static let item = ReadMarkerRoomTimelineItem(id: .init(timelineID: .init(UUID().uuidString))) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/RedactedRoomTimelineView.swift similarity index 97% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/RedactedRoomTimelineView.swift index 5537c19e3..b1d803bbf 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/RedactedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/RedactedRoomTimelineView.swift @@ -30,7 +30,7 @@ struct RedactedRoomTimelineView: View { } struct RedactedRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { VStack(alignment: .leading, spacing: 20.0) { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/SeparatorRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/SeparatorRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/SeparatorRoomTimelineView.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StateRoomTimelineView.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/StateRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StateRoomTimelineView.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StickerRoomTimelineView.swift similarity index 97% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StickerRoomTimelineView.swift index e8805f79d..69582327d 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/StickerRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/StickerRoomTimelineView.swift @@ -18,7 +18,7 @@ import Foundation import SwiftUI struct StickerRoomTimelineView: View { - @EnvironmentObject private var context: RoomScreenViewModel.Context + @EnvironmentObject private var context: TimelineViewModel.Context let timelineItem: StickerRoomTimelineItem var body: some View { @@ -48,7 +48,7 @@ struct StickerRoomTimelineView: View { } struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { body.environmentObject(viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextBasedRoomTimelineViewProtocol.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextBasedRoomTimelineViewProtocol.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/TextBasedRoomTimelineViewProtocol.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextBasedRoomTimelineViewProtocol.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift similarity index 99% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift index b2e1cea86..534021a24 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextRoomTimelineView.swift @@ -36,7 +36,7 @@ struct TextRoomTimelineView: View, TextBasedRoomTimelineViewProtocol { } struct TextRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { body.environmentObject(viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineStartRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TimelineStartRoomTimelineView.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineStartRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TimelineStartRoomTimelineView.swift diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/UnsupportedRoomTimelineView.swift similarity index 98% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/UnsupportedRoomTimelineView.swift index d76d5509c..55c1fb1ff 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/UnsupportedRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/UnsupportedRoomTimelineView.swift @@ -40,7 +40,7 @@ struct UnsupportedRoomTimelineView: View { } struct UnsupportedRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { body.environmentObject(viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift similarity index 97% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift index 7df573380..3612c9a49 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/VideoRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VideoRoomTimelineView.swift @@ -18,7 +18,7 @@ import Foundation import SwiftUI struct VideoRoomTimelineView: View { - @EnvironmentObject private var context: RoomScreenViewModel.Context + @EnvironmentObject private var context: TimelineViewModel.Context let timelineItem: VideoRoomTimelineItem var body: some View { @@ -68,7 +68,7 @@ struct VideoRoomTimelineView: View { } struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static var previews: some View { body.environmentObject(viewModel.context) diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineSenderAvatarView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineSenderAvatarView.swift similarity index 94% rename from ElementX/Sources/Screens/RoomScreen/View/TimelineSenderAvatarView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineSenderAvatarView.swift index f855a953b..173d6ce9d 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineSenderAvatarView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineSenderAvatarView.swift @@ -18,7 +18,7 @@ import Foundation import SwiftUI struct TimelineSenderAvatarView: View { - @EnvironmentObject private var context: RoomScreenViewModel.Context + @EnvironmentObject private var context: TimelineViewModel.Context let timelineItem: EventBasedTimelineItemProtocol diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift similarity index 71% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift rename to ElementX/Sources/Screens/Timeline/View/TimelineView.swift index 5a0b15423..010f0aaf7 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift @@ -19,7 +19,7 @@ import WysiwygComposer /// A table view wrapper that displays the timeline of a room. struct TimelineView: UIViewControllerRepresentable { - @EnvironmentObject private var viewModelContext: RoomScreenViewModel.Context + @EnvironmentObject private var viewModelContext: TimelineViewModel.Context func makeUIViewController(context: Context) -> TimelineTableViewController { let tableViewController = TimelineTableViewController(coordinator: context.coordinator, @@ -40,9 +40,9 @@ struct TimelineView: UIViewControllerRepresentable { @MainActor class Coordinator { - let context: RoomScreenViewModel.Context + let context: TimelineViewModel.Context - init(viewModelContext: RoomScreenViewModel.Context) { + init(viewModelContext: TimelineViewModel.Context) { context = viewModelContext } @@ -70,7 +70,7 @@ struct TimelineView: UIViewControllerRepresentable { } } - func send(viewAction: RoomScreenViewAction) { + func send(viewAction: TimelineViewAction) { context.send(viewAction: viewAction) } } @@ -79,20 +79,23 @@ struct TimelineView: UIViewControllerRepresentable { // MARK: - Previews struct TimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(id: "stable_id", - name: "Preview room")), - timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + static let roomViewModel = RoomScreenViewModel.mock() + static let timelineViewModel = TimelineViewModel(roomProxy: RoomProxyMock(.init(id: "stable_id", + name: "Preview room")), + timelineController: MockRoomTimelineController(), + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: ServiceLocator.shared.userIndicatorController, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) static var previews: some View { NavigationStack { - RoomScreen(context: viewModel.context, composerToolbar: ComposerToolbar.mock()) + RoomScreen(roomViewModel: roomViewModel, + timelineViewModel: timelineViewModel, + composerToolbar: ComposerToolbar.mock()) } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TypingIndicatorView.swift b/ElementX/Sources/Screens/Timeline/View/TypingIndicatorView.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/TypingIndicatorView.swift rename to ElementX/Sources/Screens/Timeline/View/TypingIndicatorView.swift diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift index 182fe417d..a30ea96a1 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift @@ -26,4 +26,16 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol { timelineItemFactory: timelineItemFactory, appSettings: ServiceLocator.shared.settings) } + + func buildRoomPinnedTimelineController(roomProxy: RoomProxyProtocol, + timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? { + guard let pinnedEventsTimeline = await roomProxy.pinnedEventsTimeline else { + return nil + } + return RoomTimelineController(roomProxy: roomProxy, + timelineProxy: pinnedEventsTimeline, + initialFocussedEventID: nil, + timelineItemFactory: timelineItemFactory, + appSettings: ServiceLocator.shared.settings) + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift index 3a5d9d551..c8c79874f 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift @@ -21,6 +21,8 @@ protocol RoomTimelineControllerFactoryProtocol { func buildRoomTimelineController(roomProxy: RoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol + func buildRoomPinnedTimelineController(roomProxy: RoomProxyProtocol, + timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? } // sourcery: AutoMockable diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift index a58679616..f7727bdaf 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessages/VoiceMessageRoomTimelineView.swift @@ -18,7 +18,7 @@ import Foundation import SwiftUI struct VoiceMessageRoomTimelineView: View { - @EnvironmentObject private var context: RoomScreenViewModel.Context + @EnvironmentObject private var context: TimelineViewModel.Context private let timelineItem: VoiceMessageRoomTimelineItem private let playerState: AudioPlayerState @State private var resumePlaybackAfterScrubbing = false @@ -63,7 +63,7 @@ struct VoiceMessageRoomTimelineView: View { } struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview { - static let viewModel = RoomScreenViewModel.mock + static let viewModel = TimelineViewModel.mock static let timelineItemIdentifier = TimelineItemIdentifier.random static let voiceRoomTimelineItem = VoiceMessageRoomTimelineItem(id: timelineItemIdentifier, timestamp: "Now", diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift index b5fd2dccd..6be7bbcf9 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift @@ -16,7 +16,7 @@ import SwiftUI struct RoomTimelineItemView: View { - @Environment(\.roomContext) var context + @Environment(\.timelineContext) var context @ObservedObject var viewState: RoomTimelineItemViewState var body: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextBasedRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/TextBasedRoomTimelineItem.swift similarity index 100% rename from ElementX/Sources/Screens/RoomScreen/View/Timeline/TextBasedRoomTimelineItem.swift rename to ElementX/Sources/Services/Timeline/TimelineItems/TextBasedRoomTimelineItem.swift diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPad-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPad-en-GB.1.png deleted file mode 100644 index 576e6a047..000000000 --- a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPad-en-GB.1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:792b9704be2c179392b93540fd3efd20697921f1fa9979bd9c6297ab180e7d6e -size 95185 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPad-en-GB.Empty.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPad-en-GB.Empty.png new file mode 100644 index 000000000..4b3421234 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPad-en-GB.Empty.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04dbe65920b28bd6dca48bdf0b1c350c62a3af2dfb0c31f5f98cc584169c0052 +size 96615 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPad-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPad-pseudo.1.png deleted file mode 100644 index 2695fab3f..000000000 --- a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPad-pseudo.1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa12b52b37641df2270df083d64a8266212b4d95960e70b052a002baef129201 -size 109865 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPad-pseudo.Empty.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPad-pseudo.Empty.png new file mode 100644 index 000000000..12a39f1ba --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPad-pseudo.Empty.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5b7b5017683816189d8c37b5893ae8e6ac0b65b4379d0d6c06a319532225475 +size 113037 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPhone-15-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPhone-15-en-GB.Empty.png similarity index 100% rename from PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPhone-15-en-GB.1.png rename to PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPhone-15-en-GB.Empty.png diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPhone-15-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPhone-15-pseudo.Empty.png similarity index 100% rename from PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPhone-15-pseudo.1.png rename to PreviewTests/__Snapshots__/PreviewTests/test_pinnedEventsTimelineScreen-iPhone-15-pseudo.Empty.png diff --git a/UnitTests/Sources/ComposerToolbarViewModelTests.swift b/UnitTests/Sources/ComposerToolbarViewModelTests.swift index 8cc1004bc..c4345eccf 100644 --- a/UnitTests/Sources/ComposerToolbarViewModelTests.swift +++ b/UnitTests/Sources/ComposerToolbarViewModelTests.swift @@ -50,22 +50,22 @@ class ComposerToolbarViewModelTests: XCTestCase { } func testComposerFocus() { - viewModel.process(roomAction: .setMode(mode: .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock")))) + viewModel.process(timelineAction: .setMode(mode: .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock")))) XCTAssertTrue(viewModel.state.bindings.composerFocused) - viewModel.process(roomAction: .removeFocus) + viewModel.process(timelineAction: .removeFocus) XCTAssertFalse(viewModel.state.bindings.composerFocused) } func testComposerMode() { - let mode: RoomScreenComposerMode = .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock")) - viewModel.process(roomAction: .setMode(mode: mode)) + let mode: ComposerMode = .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock")) + viewModel.process(timelineAction: .setMode(mode: mode)) XCTAssertEqual(viewModel.state.composerMode, mode) - viewModel.process(roomAction: .clear) + viewModel.process(timelineAction: .clear) XCTAssertEqual(viewModel.state.composerMode, .default) } func testComposerModeIsPublished() { - let mode: RoomScreenComposerMode = .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock")) + let mode: ComposerMode = .edit(originalItemId: TimelineItemIdentifier(timelineID: "mock")) let expectation = expectation(description: "Composer mode is published") let cancellable = viewModel .context @@ -78,7 +78,7 @@ class ComposerToolbarViewModelTests: XCTestCase { expectation.fulfill() }) - viewModel.process(roomAction: .setMode(mode: mode)) + viewModel.process(timelineAction: .setMode(mode: mode)) wait(for: [expectation], timeout: 2.0) cancellable.cancel() @@ -206,7 +206,7 @@ class ComposerToolbarViewModelTests: XCTestCase { viewModel.context.composerFormattingEnabled = false viewModel.context.plainComposerText = .init(string: "Hello world!") - viewModel.process(roomAction: .saveDraft) + viewModel.saveDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertEqual(draftServiceMock.saveDraftCallsCount, 1) @@ -226,7 +226,7 @@ class ComposerToolbarViewModelTests: XCTestCase { viewModel.context.composerFormattingEnabled = true wysiwygViewModel.setHtmlContent("Hello world!") - viewModel.process(roomAction: .saveDraft) + viewModel.saveDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertEqual(draftServiceMock.saveDraftCallsCount, 1) @@ -245,9 +245,9 @@ class ComposerToolbarViewModelTests: XCTestCase { } viewModel.context.composerFormattingEnabled = false - viewModel.process(roomAction: .setMode(mode: .edit(originalItemId: .init(timelineID: "", eventID: "testID")))) + viewModel.process(timelineAction: .setMode(mode: .edit(originalItemId: .init(timelineID: "", eventID: "testID")))) viewModel.context.plainComposerText = .init(string: "Hello world!") - viewModel.process(roomAction: .saveDraft) + viewModel.saveDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertEqual(draftServiceMock.saveDraftCallsCount, 1) @@ -266,14 +266,14 @@ class ComposerToolbarViewModelTests: XCTestCase { } viewModel.context.composerFormattingEnabled = false - viewModel.process(roomAction: .setMode(mode: .reply(itemID: .init(timelineID: "", - eventID: "testID"), + viewModel.process(timelineAction: .setMode(mode: .reply(itemID: .init(timelineID: "", + eventID: "testID"), replyDetails: .loaded(sender: .init(id: ""), eventID: "testID", eventContent: .message(.text(.init(body: "reply text")))), isThread: false))) viewModel.context.plainComposerText = .init(string: "Hello world!") - viewModel.process(roomAction: .saveDraft) + viewModel.saveDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertEqual(draftServiceMock.saveDraftCallsCount, 1) @@ -292,13 +292,13 @@ class ComposerToolbarViewModelTests: XCTestCase { } viewModel.context.composerFormattingEnabled = false - viewModel.process(roomAction: .setMode(mode: .reply(itemID: .init(timelineID: "", - eventID: "testID"), + viewModel.process(timelineAction: .setMode(mode: .reply(itemID: .init(timelineID: "", + eventID: "testID"), replyDetails: .loaded(sender: .init(id: ""), eventID: "testID", eventContent: .message(.text(.init(body: "reply text")))), isThread: false))) - viewModel.process(roomAction: .saveDraft) + viewModel.saveDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertEqual(draftServiceMock.saveDraftCallsCount, 1) @@ -314,7 +314,7 @@ class ComposerToolbarViewModelTests: XCTestCase { } viewModel.context.composerFormattingEnabled = false - viewModel.process(roomAction: .saveDraft) + viewModel.saveDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertFalse(draftServiceMock.saveDraftCalled) @@ -332,8 +332,8 @@ class ComposerToolbarViewModelTests: XCTestCase { viewModel.context.composerFormattingEnabled = false let waveformData: [Float] = Array(repeating: 1.0, count: 1000) viewModel.context.plainComposerText = .init(string: "Hello world!") - viewModel.process(roomAction: .setMode(mode: .previewVoiceMessage(state: AudioPlayerState(id: .recorderPreview, duration: 10.0), waveform: .data(waveformData), isUploading: false))) - viewModel.process(roomAction: .saveDraft) + viewModel.process(timelineAction: .setMode(mode: .previewVoiceMessage(state: AudioPlayerState(id: .recorderPreview, duration: 10.0), waveform: .data(waveformData), isUploading: false))) + viewModel.saveDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertFalse(draftServiceMock.saveDraftCalled) @@ -349,7 +349,7 @@ class ComposerToolbarViewModelTests: XCTestCase { return .success(nil) } - viewModel.process(roomAction: .loadDraft) + viewModel.loadDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) XCTAssertTrue(viewModel.state.composerEmpty) @@ -365,7 +365,7 @@ class ComposerToolbarViewModelTests: XCTestCase { htmlText: nil, draftType: .newMessage)) } - viewModel.process(roomAction: .loadDraft) + viewModel.loadDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) @@ -382,7 +382,7 @@ class ComposerToolbarViewModelTests: XCTestCase { htmlText: "Hello world!", draftType: .newMessage)) } - viewModel.process(roomAction: .loadDraft) + viewModel.loadDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertTrue(viewModel.context.composerFormattingEnabled) @@ -400,7 +400,7 @@ class ComposerToolbarViewModelTests: XCTestCase { htmlText: nil, draftType: .edit(eventID: "testID"))) } - viewModel.process(roomAction: .loadDraft) + viewModel.loadDraft() await fulfillment(of: [expectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) @@ -433,7 +433,7 @@ class ComposerToolbarViewModelTests: XCTestCase { return .success(.init(details: loadedReply, isThreaded: true)) } - viewModel.process(roomAction: .loadDraft) + viewModel.loadDraft() await fulfillment(of: [draftExpectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) @@ -474,7 +474,7 @@ class ComposerToolbarViewModelTests: XCTestCase { return .success(.init(details: loadedReply, isThreaded: true)) } - viewModel.process(roomAction: .loadDraft) + viewModel.loadDraft() await fulfillment(of: [draftExpectation], timeout: 10) XCTAssertFalse(viewModel.context.composerFormattingEnabled) @@ -493,7 +493,7 @@ class ComposerToolbarViewModelTests: XCTestCase { func testSaveVolatileDraftWhenEditing() { viewModel.context.composerFormattingEnabled = false viewModel.context.plainComposerText = .init(string: "Hello world!") - viewModel.process(roomAction: .setMode(mode: .edit(originalItemId: .random))) + viewModel.process(timelineAction: .setMode(mode: .edit(originalItemId: .random))) let draft = draftServiceMock.saveVolatileDraftReceivedDraft XCTAssertNotNil(draft) @@ -530,7 +530,7 @@ class ComposerToolbarViewModelTests: XCTestCase { expectation2.fulfill() } - viewModel.process(roomAction: .clear) + viewModel.process(timelineAction: .clear) await fulfillment(of: [expectation1, expectation2]) XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: "Hello world")) } @@ -549,7 +549,7 @@ class ComposerToolbarViewModelTests: XCTestCase { expectation2.fulfill() } - viewModel.process(roomAction: .clear) + viewModel.process(timelineAction: .clear) await fulfillment(of: [expectation1, expectation2]) XCTAssertEqual(viewModel.context.plainComposerText, NSAttributedString(string: "Hello world")) } @@ -557,7 +557,7 @@ class ComposerToolbarViewModelTests: XCTestCase { func testRestoreUserMentionInPlainText() async throws { viewModel.context.composerFormattingEnabled = false let text = "Hello [TestName](https://matrix.to/#/@test:matrix.org)!" - viewModel.process(roomAction: .setText(plainText: text, htmlText: nil)) + viewModel.process(timelineAction: .setText(plainText: text, htmlText: nil)) let deferred = deferFulfillment(viewModel.actions) { action in switch action { @@ -577,7 +577,7 @@ class ComposerToolbarViewModelTests: XCTestCase { func testRestoreAllUsersMentionInPlainText() async throws { viewModel.context.composerFormattingEnabled = false let text = "Hello @room" - viewModel.process(roomAction: .setText(plainText: text, htmlText: nil)) + viewModel.process(timelineAction: .setText(plainText: text, htmlText: nil)) let deferred = deferFulfillment(viewModel.actions) { action in switch action { @@ -596,7 +596,7 @@ class ComposerToolbarViewModelTests: XCTestCase { func testRestoreMixedMentionsInPlainText() async throws { viewModel.context.composerFormattingEnabled = false let text = "Hello [User1](https://matrix.to/#/@user1:matrix.org), [User2](https://matrix.to/#/@user2:matrix.org) and @room" - viewModel.process(roomAction: .setText(plainText: text, htmlText: nil)) + viewModel.process(timelineAction: .setText(plainText: text, htmlText: nil)) let deferred = deferFulfillment(viewModel.actions) { action in switch action { @@ -616,7 +616,7 @@ class ComposerToolbarViewModelTests: XCTestCase { func testRestoreAmbiguousMention() async throws { viewModel.context.composerFormattingEnabled = false let text = "Hello [User1](https://matrix.to/#/@roomuser:matrix.org)" - viewModel.process(roomAction: .setText(plainText: text, htmlText: nil)) + viewModel.process(timelineAction: .setText(plainText: text, htmlText: nil)) let deferred = deferFulfillment(viewModel.actions) { action in switch action { diff --git a/UnitTests/Sources/PillContextTests.swift b/UnitTests/Sources/PillContextTests.swift index 3ddbd4f78..91b61644a 100644 --- a/UnitTests/Sources/PillContextTests.swift +++ b/UnitTests/Sources/PillContextTests.swift @@ -26,16 +26,16 @@ class PillContextTests: XCTestCase { let proxyMock = RoomProxyMock(.init(name: "Test")) let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([]) proxyMock.membersPublisher = subject.asCurrentValuePublisher() - let mock = RoomScreenViewModel(roomProxy: proxyMock, - timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) - let context = PillContext(roomContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) + let mock = TimelineViewModel(roomProxy: proxyMock, + timelineController: MockRoomTimelineController(), + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: ServiceLocator.shared.userIndicatorController, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) + let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) XCTAssertFalse(context.viewState.isOwnMention) XCTAssertEqual(context.viewState.displayText, id) @@ -54,16 +54,16 @@ class PillContextTests: XCTestCase { let proxyMock = RoomProxyMock(.init(name: "Test", ownUserID: id)) let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([]) proxyMock.membersPublisher = subject.asCurrentValuePublisher() - let mock = RoomScreenViewModel(roomProxy: proxyMock, - timelineController: MockRoomTimelineController(), - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) - let context = PillContext(roomContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) + let mock = TimelineViewModel(roomProxy: proxyMock, + timelineController: MockRoomTimelineController(), + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: ServiceLocator.shared.userIndicatorController, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) + let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) XCTAssertTrue(context.viewState.isOwnMention) } @@ -75,16 +75,16 @@ class PillContextTests: XCTestCase { let proxyMock = RoomProxyMock(.init(id: id, name: displayName, avatarURL: avatarURL)) let mockController = MockRoomTimelineController() mockController.roomProxy = proxyMock - let mock = RoomScreenViewModel(roomProxy: proxyMock, - timelineController: mockController, - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: ServiceLocator.shared.userIndicatorController, - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) - let context = PillContext(roomContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body))) + let mock = TimelineViewModel(roomProxy: proxyMock, + timelineController: mockController, + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: ServiceLocator.shared.userIndicatorController, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) + let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body))) XCTAssertTrue(context.viewState.isOwnMention) XCTAssertEqual(context.viewState.displayText, PillConstants.atRoom) diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/TimelineViewModelTests.swift similarity index 89% rename from UnitTests/Sources/RoomScreenViewModelTests.swift rename to UnitTests/Sources/TimelineViewModelTests.swift index 0d837da72..897e968c9 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/TimelineViewModelTests.swift @@ -20,7 +20,7 @@ import Combine import XCTest @MainActor -class RoomScreenViewModelTests: XCTestCase { +class TimelineViewModelTests: XCTestCase { var userIndicatorControllerMock: UserIndicatorControllerMock! var cancellables = Set() @@ -327,7 +327,7 @@ class RoomScreenViewModelTests: XCTestCase { // swiftlint:enable force_unwrapping // swiftlint:disable:next large_tuple - private func readReceiptsConfiguration(with items: [RoomTimelineItemProtocol]) -> (RoomScreenViewModel, + private func readReceiptsConfiguration(with items: [RoomTimelineItemProtocol]) -> (TimelineViewModel, RoomProxyMock, TimelineProxyMock, MockRoomTimelineController) { @@ -343,15 +343,15 @@ class RoomScreenViewModelTests: XCTestCase { timelineController.timelineItems = items timelineController.roomProxy = roomProxy - let viewModel = RoomScreenViewModel(roomProxy: roomProxy, - timelineController: timelineController, - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: userIndicatorControllerMock, - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + let viewModel = TimelineViewModel(roomProxy: roomProxy, + timelineController: timelineController, + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: userIndicatorControllerMock, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) return (viewModel, roomProxy, timelineProxy, timelineController) } @@ -367,15 +367,15 @@ class RoomScreenViewModelTests: XCTestCase { // When showing them in a timeline. let timelineController = MockRoomTimelineController() timelineController.timelineItems = [message] - let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAlice, RoomMemberProxyMock.mockCharlie])), - timelineController: timelineController, - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: userIndicatorControllerMock, - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + let viewModel = TimelineViewModel(roomProxy: RoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAlice, RoomMemberProxyMock.mockCharlie])), + timelineController: timelineController, + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: userIndicatorControllerMock, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) let deferred = deferFulfillment(viewModel.context.$viewState) { value in value.bindings.readReceiptsSummaryInfo?.orderedReceipts == receipts @@ -389,17 +389,17 @@ class RoomScreenViewModelTests: XCTestCase { private func makeViewModel(roomProxy: RoomProxyProtocol? = nil, focussedEventID: String? = nil, - timelineController: RoomTimelineControllerProtocol) -> RoomScreenViewModel { - RoomScreenViewModel(roomProxy: roomProxy ?? RoomProxyMock(.init(name: "")), - focussedEventID: focussedEventID, - timelineController: timelineController, - mediaProvider: MockMediaProvider(), - mediaPlayerProvider: MediaPlayerProviderMock(), - voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - userIndicatorController: userIndicatorControllerMock, - appMediator: AppMediatorMock.default, - appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + timelineController: RoomTimelineControllerProtocol) -> TimelineViewModel { + TimelineViewModel(roomProxy: roomProxy ?? RoomProxyMock(.init(name: "")), + focussedEventID: focussedEventID, + timelineController: timelineController, + mediaProvider: MockMediaProvider(), + mediaPlayerProvider: MediaPlayerProviderMock(), + voiceMessageMediaManager: VoiceMessageMediaManagerMock(), + userIndicatorController: userIndicatorControllerMock, + appMediator: AppMediatorMock.default, + appSettings: ServiceLocator.shared.settings, + analyticsService: ServiceLocator.shared.analytics) } }