Add UI to record a voice message from the composer toolbar (#1892)

This commit is contained in:
Nicolas Mauri 2023-10-16 17:18:51 +02:00 committed by GitHub
parent 0d72554e6f
commit 9ab3b20a49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1241 additions and 87 deletions

View File

@ -28,12 +28,14 @@
06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */; };
071A017E415AD378F2961B11 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227AC5D71A4CE43512062243 /* URL.swift */; };
07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */; };
086D01E79C8E8D3F004FAF21 /* AudioPlayerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC9104846487244648D32C6D /* AudioPlayerProtocol.swift */; };
08CB4BD12CEEDE6AAE4A18DD /* WindowManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 035177BCD8E8308B098AC3C2 /* WindowManager.swift */; };
095C0ACFC234E0550A6404C5 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */; };
095D3906CF2F940C2D2D17CC /* RoomFlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */; };
09713669577CDA8D012EE380 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 6647C55D93508C7CE9D954A5 /* MatrixRustSDK */; };
09AAF04B27732046C755D914 /* SoftLogoutViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */; };
09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7074991B3267B26D89B22 /* MockRoomTimelineController.swift */; };
09EF4222EEBBA1A7B8F4071E /* VoiceMessageRecordingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E61DDB42C0DE429C0955D8 /* VoiceMessageRecordingButton.swift */; };
0A194F5E70B5A628C1BF4476 /* AdvancedSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */; };
0AA0477E063E72B786A983CF /* AnalyticsPromptScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E1FF2DA52B1DE7CAEC5422 /* AnalyticsPromptScreenViewModel.swift */; };
0ACAA31FD0399CEEBA3ECC21 /* UserDetailsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */; };
@ -65,6 +67,7 @@
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 */; };
1471A080552631358D152C18 /* AudioPlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BDB3E65A79779EDA5D33D8A /* AudioPlayerState.swift */; };
149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */; };
14E99D27628B1A6F0CB46FEA /* SeparatorRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A9F49B3EE59147AF2F70BB /* SeparatorRoomTimelineItem.swift */; };
152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */; };
@ -146,6 +149,7 @@
2C4C750D0039AFABDF24236C /* TemplateScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342BEBC3C5FC3F9943C41C4C /* TemplateScreenViewModelProtocol.swift */; };
2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */; };
2CA6ABBC9A88EB89EA52FCCB /* ConfettiScene.scn in Resources */ = {isa = PBXBuildFile; fileRef = B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */; };
2DA27D78560D5F79B917E163 /* AudioConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */; };
2DA90E38FF4E696825810C1A /* WaitlistScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECB08484CD5D77C9BF97AA78 /* WaitlistScreenUITests.swift */; };
2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; };
2E8C6672D0EE7D5B1BEDB8E2 /* ServerConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7478623CECC9438014244BA /* ServerConfirmationScreen.swift */; };
@ -153,6 +157,7 @@
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 */; };
3042527CB344A9EF1157FC26 /* AudioRecorderStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55CC239AE12339C565F6C9A /* AudioRecorderStateTests.swift */; };
308BD9343B95657FAA583FB7 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 19CD5B074D7DD44AF4C58BB6 /* SwiftState */; };
3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */; };
30CC1DB7CE357659C82AA115 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */; };
@ -162,6 +167,7 @@
32B7891D937377A59606EDFC /* UserFlowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21DD8599815136EFF5B73F38 /* UserFlowTests.swift */; };
33094DB91C3A4131E76B2C07 /* AppLockScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5574FD6FC3C2DC0DF160A85 /* AppLockScreenViewModelProtocol.swift */; };
339BC18777912E1989F2F17D /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584A61D9C459FAFEF038A7C0 /* Section.swift */; };
33CA777C9DF263582D77A67F /* VoiceMessagePreviewComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC9F57320EC80C7CE34FE4A /* VoiceMessagePreviewComposer.swift */; };
33CAC1226DFB8B5D8447D286 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; };
340D39DB87F3800D53A6A621 /* EmojiPickerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */; };
34357B287357BC0B9715DD51 /* UserAgentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */; };
@ -194,6 +200,7 @@
3A7DD0D13B0FB8876D69D829 /* TextBasedRoomTimelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */; };
3B0F9B57D25B07E66F15762A /* MediaUploadPreviewScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2E7C987AE5DC9087BB19F7D /* MediaUploadPreviewScreenModels.swift */; };
3B28408450BCAED911283AA2 /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35FA991289149D31F4286747 /* UserPreference.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 */; };
3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DC38E64A5ED3FDB201029A /* BugReportService.swift */; };
@ -229,6 +236,7 @@
4681820102DAC8BA586357D4 /* VoiceMessageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB8D7926A5684E18196B538 /* VoiceMessageCache.swift */; };
46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCE3636E3D01477C8B2E9D0 /* ReportContentScreenModels.swift */; };
46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565F1B2B300597C616B37888 /* FullscreenDialog.swift */; };
46C9F8FE3810A04A005FE16B /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B19B2BCC779ED934E0BBC2A /* AudioPlayer.swift */; };
46D1E2940ED8CCBF62FE8854 /* CreatePollScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27EA0F71A3A400A202E15318 /* CreatePollScreen.swift */; };
4714991754A08B58B4D7ED85 /* OnboardingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F27BAB69EB568369F1F6B3 /* OnboardingScreenViewModelProtocol.swift */; };
47305C0911C9E1AA774A4000 /* TemplateScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */; };
@ -288,6 +296,7 @@
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 */; };
5992EF10AA157EBD97D88910 /* AudioRecorderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6569593FA36B22259E806A67 /* AudioRecorderState.swift */; };
5995C63B1C61DE1373AA2BCE /* ComposerToolbarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD95B3714F806AC9CF9A557B /* ComposerToolbarViewModel.swift */; };
59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; };
5B2D1210B40570D87B11BD3B /* ThreadDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CA3F8E905DF50BF22ECC18F /* ThreadDecorator.swift */; };
@ -308,7 +317,6 @@
5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */; };
5F28C9146694B381BB82E18C /* AnalyticsPromptScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B65A314DF40B6BBF775C2BC /* AnalyticsPromptScreenCoordinator.swift */; };
5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; };
5F8E96263497FFB7D3254EB2 /* AudioConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1CD5EC6265A09315772DB7 /* AudioConverter.swift */; };
60ED66E63A169E47489348A8 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 886A0A498FA01E8EDD451D05 /* Sentry */; };
6146996D5C4DDD5DA816FC87 /* AuthenticationTextFieldStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCACD75595C40EACD6AD4A74 /* AuthenticationTextFieldStyle.swift */; };
617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */; };
@ -319,6 +327,7 @@
62418EA4E3EB597AD184AEB6 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; };
62910B515BCB4B455E24D7C1 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */; };
6298AB0906DDD3525CD78C6B /* LRUCache in Frameworks */ = {isa = PBXBuildFile; productRef = 1081D3630AAD3ACEDDEC3A98 /* LRUCache */; };
62A7FC3A0191BC7181AA432B /* AudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 907FA4DE17DEA1A3738EFB83 /* AudioRecorder.swift */; };
63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */; };
642DF13C49ED4121C148230E /* TestablePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1E227F34BE43B08E098796E /* TestablePreview.swift */; };
6448F8D1D3CA4CD27BB4CADD /* RoomMemberProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */; };
@ -432,7 +441,6 @@
829062DD3C3F7016FE1A6476 /* RoomDetailsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */; };
8317E1314C00DCCC99D30DA8 /* TextBasedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9227F7495DA43324050A863 /* TextBasedRoomTimelineItem.swift */; };
83A4DAB181C56987C3E804FF /* MapTilerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */; };
8418688282763F4B9DDC42FB /* AudioConverterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB4D7B3FBCA261927536FE64 /* AudioConverterProtocol.swift */; };
84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; };
84EFCB95F9DA2979C8042B26 /* UITestsSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */; };
8544657DEEE717ED2E22E382 /* RoomNotificationSettingsProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */; };
@ -472,6 +480,7 @@
8BC8EF6705A78946C1F22891 /* SoftLogoutScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71A7D4DDEEE5D2CA0C8D63CD /* SoftLogoutScreen.swift */; };
8C050A8012E6078BEAEF5BC8 /* PillTextAttachmentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */; };
8C1A5ECAF895D4CAF8C4D461 /* AppActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F21ED7205048668BEB44A38 /* AppActivityView.swift */; };
8C27BEB00B903D953F31F962 /* VoiceMessageRecordingButtonTooltipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF449205DF1E9817115245C4 /* VoiceMessageRecordingButtonTooltipView.swift */; };
8C454500B8073E1201F801A9 /* MXLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34A814CBD56230BC74FFCF4 /* MXLogger.swift */; };
8C706DA7EAC0974CA2F8F1CD /* MentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15748C254911E3654C93B0ED /* MentionBuilder.swift */; };
8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; };
@ -667,7 +676,6 @@
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 */; };
C19085A284D54A166A64A86C /* AudioPlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA88C615D8BFCCEF0D2FEAC9 /* AudioPlayerState.swift */; };
C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */; };
C1A5C386319835FB0C77736B /* ReportContentScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */; };
C1D0AB8222D7BAFC9AF9C8C0 /* MapLibreMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 622D09D4ECE759189009AEAF /* MapLibreMapView.swift */; };
@ -699,6 +707,7 @@
C97325EFDCCEE457432A9E82 /* MessageText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E0B4A34E69BD2132BEC521 /* MessageText.swift */; };
C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81B6170DB690013CEB646F4 /* MapLibreModels.swift */; };
C9F5B48D15B9BCAE1F8D564E /* RoomNotificationModeProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1511766C534367700C8DD75 /* RoomNotificationModeProxy.swift */; };
CA5BFF0C2EF5A8EF40CA2D69 /* VoiceMessageRecordingComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCB6F36CCE44A29A06FCAF1C /* VoiceMessageRecordingComposer.swift */; };
CAF8755E152204F55F8D6B5B /* RoomMembersListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B63F817FE305548DB4B512 /* RoomMembersListViewModelTests.swift */; };
CB137BFB3E083C33E398A6CB /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 020597E28A4BC8E1BE8EDF6E /* KeychainAccess */; };
CB498F4E27AA0545DCEF0F6F /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 36B7FC232711031AA2B0D188 /* DTCoreText */; };
@ -711,7 +720,6 @@
CCBEC2100CAF2EEBE9DB4156 /* TemplateScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */; };
CCC3802A3C019A6FFAAA547A /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E65E613F057697A1A0BC03 /* NotificationViewController.swift */; };
CD0088B763CD970CF1CBF8CB /* DateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5E97E9615A158C76B2AB77 /* DateTests.swift */; };
CD1C6943F42F29079E5E7511 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EDDE503C9BAC82B661E4164 /* AudioPlayer.swift */; };
CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */; };
CDCA8A559E098503DDE29477 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; };
CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; };
@ -809,6 +817,7 @@
EAC6FE2CD4F50A43068ADCD8 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; };
EAF2B3E6C6AEC4AD3A8BD454 /* RoomMemberDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A87D0471D438A233C2CF4A /* RoomMemberDetailsScreenViewModel.swift */; };
EB88DBD77221E2CFE463018C /* NSE.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0D8F620C8B314840D8602E3F /* NSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
EBDB339A7C127F068B6E52E5 /* VoiceMessageRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A634D8DD1E10D858CF7995D /* VoiceMessageRecordingView.swift */; };
EBE13FAB4E29738AC41BD3E5 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; };
EC280623A42904341363EAAF /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = A20EA00CCB9DBE0FFB17DD09 /* Collections */; };
EC658A57E715699C52DFBC77 /* CreatePollScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EB1D0C69FEDD93404DF927E /* CreatePollScreenViewModelTests.swift */; };
@ -829,7 +838,6 @@
F06CE9132855E81EBB6DDC32 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 940C605265DD82DA0C655E23 /* Kingfisher */; };
F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */; };
F0A26CD502C3A5868353B0FA /* ServerConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24DEE0682C95F897B6C7CB0D /* ServerConfirmationScreenViewModel.swift */; };
F0B196905CD23E3B4505CB7B /* AudioPlayerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 048A3590E8379BCED4D30D5C /* AudioPlayerProtocol.swift */; };
F0F82C3C848C865C3098AA52 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 21C83087604B154AA30E9A8F /* SnapshotTesting */; };
F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */; };
F12F6BED7B6D7EE4BEE55039 /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; };
@ -839,6 +847,7 @@
F32B271F60531BE92C6E62A1 /* StickerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612EF972F2A1800682D32C5E /* StickerRoomTimelineView.swift */; };
F37629BAA5E8F50AAF2A131D /* SoftLogoutScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7BAD55A4E2B8E5828CD64C /* SoftLogoutScreenViewModel.swift */; };
F3E2D3F7ACDED65A4E5CD8DE /* RoomMembersListScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */; };
F3F38062C6CA21CF403C5C90 /* AudioConverterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2757B1BE23DF8AA239937243 /* AudioConverterProtocol.swift */; };
F40B097470D3110DFDB1FAAA /* LegalInformationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */; };
F4433EF57B4BB3C077F8B00E /* SessionVerificationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD9E0FFA29EAACFF3AB9732 /* SessionVerificationScreenViewModel.swift */; };
F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */; };
@ -942,7 +951,6 @@
03DD998E523D4EC93C7ED703 /* RoomNotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModelTests.swift; sourceTree = "<group>"; };
045253F9967A535EE5B16691 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = "<group>"; };
048A3590E8379BCED4D30D5C /* AudioPlayerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerProtocol.swift; sourceTree = "<group>"; };
04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityIdentifiers.swift; sourceTree = "<group>"; };
04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
052B2F924572AFD70B5F500E /* StartChatScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenViewModel.swift; sourceTree = "<group>"; };
@ -962,6 +970,7 @@
099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = "<group>"; };
09CE2B7AD979BDEE09FEDB08 /* WaitlistScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenModels.swift; sourceTree = "<group>"; };
0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = "<group>"; };
0A634D8DD1E10D858CF7995D /* VoiceMessageRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingView.swift; sourceTree = "<group>"; };
0AE449DFBA7CC863EEB2FD2A /* FormattingToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattingToolbar.swift; sourceTree = "<group>"; };
0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineItem.swift; sourceTree = "<group>"; };
0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSenderAvatarView.swift; sourceTree = "<group>"; };
@ -1056,6 +1065,7 @@
24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelProtocol.swift; sourceTree = "<group>"; };
25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = "<group>"; };
260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = "<group>"; };
2757B1BE23DF8AA239937243 /* AudioConverterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverterProtocol.swift; sourceTree = "<group>"; };
277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfigurationScreenViewStateTests.swift; sourceTree = "<group>"; };
27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = "<group>"; };
27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = "<group>"; };
@ -1070,13 +1080,13 @@
2AB2C848BB9A7A9B618B7B89 /* TextBasedRoomTimelineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineTests.swift; sourceTree = "<group>"; };
2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
2BB385E148DE55C85C0A02D6 /* SoftLogoutScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenModels.swift; sourceTree = "<group>"; };
2BDB3E65A79779EDA5D33D8A /* AudioPlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerState.swift; sourceTree = "<group>"; };
2BFDCA5A09EE70BC17F2EFA7 /* URLComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponents.swift; sourceTree = "<group>"; };
2C0197EAE9D45A662B8847B6 /* RoomTimelineControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerProtocol.swift; sourceTree = "<group>"; };
2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskProtocol.swift; sourceTree = "<group>"; };
2CEBCB9676FCD1D0F13188DD /* StringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringTests.swift; sourceTree = "<group>"; };
2D0946F77B696176E062D037 /* RoomMembersListScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenModels.swift; sourceTree = "<group>"; };
2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemProxy.swift; sourceTree = "<group>"; };
2EDDE503C9BAC82B661E4164 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = "<group>"; };
2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsViewModelTests.swift; sourceTree = "<group>"; };
2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxy.swift; sourceTree = "<group>"; };
2FD0E68C42CA7DDCD4CAD68D /* MentionSuggestionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionSuggestionItemView.swift; sourceTree = "<group>"; };
@ -1243,6 +1253,7 @@
6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenCoordinator.swift; sourceTree = "<group>"; };
64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerScreenCoordinator.swift; sourceTree = "<group>"; };
653610CB5F9776EAAAB98155 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
6569593FA36B22259E806A67 /* AudioRecorderState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderState.swift; sourceTree = "<group>"; };
65AAD845E53B0C8B5E0812C2 /* UserDiscoveryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryService.swift; sourceTree = "<group>"; };
65C2B80DD0BF6F10BB5FA922 /* MockAuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthenticationServiceProxy.swift; sourceTree = "<group>"; };
6654859746B0BE9611459391 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@ -1307,6 +1318,7 @@
7A5D2323D7B6BF4913EB7EED /* landscape_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = landscape_test_image.jpg; sourceTree = "<group>"; };
7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelTests.swift; sourceTree = "<group>"; };
7B04BD3874D736127A8156B8 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
7B19B2BCC779ED934E0BBC2A /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = "<group>"; };
7B25F959A434BB9923A3223F /* ExpiringTaskRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringTaskRunner.swift; sourceTree = "<group>"; };
7B849D2FF2CC12BA411A1651 /* CreateRoomModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomModels.swift; sourceTree = "<group>"; };
7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowCoordinatorProtocol.swift; sourceTree = "<group>"; };
@ -1370,6 +1382,7 @@
8F61A0DD8243B395499C99A2 /* InvitesScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenUITests.swift; sourceTree = "<group>"; };
8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = "<group>"; };
8FC803282F9268D49F4ABF14 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
907FA4DE17DEA1A3738EFB83 /* AudioRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorder.swift; sourceTree = "<group>"; };
90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = "<group>"; };
90F2F8998E5632668B0AD848 /* RoomTimelineItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemView.swift; sourceTree = "<group>"; };
913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachmentData.swift; sourceTree = "<group>"; };
@ -1451,6 +1464,7 @@
AC3F82523D6F48B926D6AF68 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
AC4F10BDD56FA77FEC742333 /* VoiceMessageMediaManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManagerTests.swift; sourceTree = "<group>"; };
AC5F5209279A752D98AAC4B2 /* CollapsibleFlowLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleFlowLayoutTests.swift; sourceTree = "<group>"; };
AC9104846487244648D32C6D /* AudioPlayerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerProtocol.swift; sourceTree = "<group>"; };
ACCC1874C122E2BBE648B8F5 /* LegalInformationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenUITests.swift; sourceTree = "<group>"; };
AD378D580A41E42560C60E9C /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; };
AD6B522BD637845AB9570B10 /* RoomNotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxy.swift; sourceTree = "<group>"; };
@ -1505,6 +1519,7 @@
BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = "<group>"; };
BB23BEAF8831DC6A57E39F52 /* CreatePollScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenCoordinator.swift; sourceTree = "<group>"; };
BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = "<group>"; };
BBEC57C204D77908E355EF42 /* AudioRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderProtocol.swift; sourceTree = "<group>"; };
BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = "<group>"; };
BC930E5F7F138112CAE5AC63 /* AppLockScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenModels.swift; sourceTree = "<group>"; };
BCF54536699ACEE3DB6BA3CB /* CompletionSuggestionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionService.swift; sourceTree = "<group>"; };
@ -1514,6 +1529,7 @@
BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenCoordinator.swift; sourceTree = "<group>"; };
BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreProtocol.swift; sourceTree = "<group>"; };
BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderScreenCoordinator.swift; sourceTree = "<group>"; };
BFC9F57320EC80C7CE34FE4A /* VoiceMessagePreviewComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessagePreviewComposer.swift; sourceTree = "<group>"; };
BFDCAC6CAAD65A2C24EA9C4B /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = "<group>"; };
BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreen.swift; sourceTree = "<group>"; };
C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXAttributeScope.swift; sourceTree = "<group>"; };
@ -1535,6 +1551,7 @@
C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelProtocol.swift; sourceTree = "<group>"; };
C4CD503F5E0938FE53C7C6E7 /* UserDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsEditScreenCoordinator.swift; sourceTree = "<group>"; };
C54464351F170D570110AFCA /* WelcomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreen.swift; sourceTree = "<group>"; };
C55CC239AE12339C565F6C9A /* AudioRecorderStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderStateTests.swift; sourceTree = "<group>"; };
C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = "<group>"; };
C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenUITests.swift; sourceTree = "<group>"; };
C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreen.swift; sourceTree = "<group>"; };
@ -1570,13 +1587,13 @@
CC680E0E79D818706CB28CF8 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
CC743C7A85E3171BCBF0A653 /* AvatarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarHeaderView.swift; sourceTree = "<group>"; };
CCACD75595C40EACD6AD4A74 /* AuthenticationTextFieldStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationTextFieldStyle.swift; sourceTree = "<group>"; };
CCB6F36CCE44A29A06FCAF1C /* VoiceMessageRecordingComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingComposer.swift; sourceTree = "<group>"; };
CD469F7513574341181F7EAA /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = "<group>"; };
CD6613DE16AD26B3A74DA1F5 /* LocationRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineItemContent.swift; sourceTree = "<group>"; };
CD6B0C4639E066915B5E6463 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
CD700E035C85738EE4B97129 /* PerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceTests.swift; sourceTree = "<group>"; };
CD95B3714F806AC9CF9A557B /* ComposerToolbarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModel.swift; sourceTree = "<group>"; };
CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = "<group>"; };
CE1CD5EC6265A09315772DB7 /* AudioConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverter.swift; sourceTree = "<group>"; };
CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = "<group>"; };
CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = "<group>"; };
CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = "<group>"; };
@ -1591,6 +1608,7 @@
D1BC84BA0AF11C2128D58ABD /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = "<group>"; };
D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = "<group>"; };
D26813CCE39221FE30BF22CD /* PlatformViewVersionPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformViewVersionPredicate.swift; sourceTree = "<group>"; };
D2E61DDB42C0DE429C0955D8 /* VoiceMessageRecordingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingButton.swift; sourceTree = "<group>"; };
D316BB02636AF2174F2580E6 /* SoftLogoutScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenViewModelProtocol.swift; sourceTree = "<group>"; };
D33116993D54FADC0C721C1F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
D38391154120264910D19528 /* PollMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollMock.swift; sourceTree = "<group>"; };
@ -1641,6 +1659,7 @@
E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModel.swift; sourceTree = "<group>"; };
E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorModalView.swift; sourceTree = "<group>"; };
E44928D844E16EE48A311FCA /* ComposerToolbarViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModelProtocol.swift; sourceTree = "<group>"; };
E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverter.swift; sourceTree = "<group>"; };
E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteURLParserTests.swift; sourceTree = "<group>"; };
E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineView.swift; sourceTree = "<group>"; };
E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
@ -1709,9 +1728,7 @@
F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = "<group>"; };
F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineView.swift; sourceTree = "<group>"; };
FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelProtocol.swift; sourceTree = "<group>"; };
FA88C615D8BFCCEF0D2FEAC9 /* AudioPlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerState.swift; sourceTree = "<group>"; };
FB0D6CB491777E7FC6B5BA12 /* CreateRoomScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreen.swift; sourceTree = "<group>"; };
FB4D7B3FBCA261927536FE64 /* AudioConverterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverterProtocol.swift; sourceTree = "<group>"; };
FB7BAD55A4E2B8E5828CD64C /* SoftLogoutScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreenViewModel.swift; sourceTree = "<group>"; };
FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreen.swift; sourceTree = "<group>"; };
FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = "<group>"; };
@ -1722,6 +1739,7 @@
FE87C931165F5E201CACBB87 /* MediaPlayerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProtocol.swift; sourceTree = "<group>"; };
FEC2E8E1B20BB2EA07B0B61E /* WelcomeScreenScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenViewModel.swift; sourceTree = "<group>"; };
FEFEEE93B82937B2E86F92EB /* AnalyticsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsScreen.swift; sourceTree = "<group>"; };
FF449205DF1E9817115245C4 /* VoiceMessageRecordingButtonTooltipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingButtonTooltipView.swift; sourceTree = "<group>"; };
FFECCE59967018204876D0A5 /* LocationMarkerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationMarkerView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -1850,6 +1868,16 @@
path = RoomNotificationSettingsScreen;
sourceTree = "<group>";
};
0371482D36C95ABAF9D4C651 /* Recorder */ = {
isa = PBXGroup;
children = (
907FA4DE17DEA1A3738EFB83 /* AudioRecorder.swift */,
BBEC57C204D77908E355EF42 /* AudioRecorderProtocol.swift */,
6569593FA36B22259E806A67 /* AudioRecorderState.swift */,
);
path = Recorder;
sourceTree = "<group>";
};
040A58C2A22F7195740EBF5C /* NCE */ = {
isa = PBXGroup;
children = (
@ -1896,7 +1924,7 @@
children = (
4BF8D11D9ED15CFC373D0119 /* Analytics */,
7803E03F759061C948D66B7E /* AppLock */,
984A887BA0294FE3B00CE9B1 /* AudioPlayer */,
FCE7249621F507F34A8122FB /* Audio */,
AAFDD509929A0CCF8BCE51EB /* Authentication */,
EBBEB5471737E9D116DF4738 /* Background */,
0ED3F5C21537519389C07644 /* BugReport */,
@ -2281,6 +2309,16 @@
path = Emojis;
sourceTree = "<group>";
};
3A2CAA4ABF5E66C3C8BBA3E9 /* Player */ = {
isa = PBXGroup;
children = (
7B19B2BCC779ED934E0BBC2A /* AudioPlayer.swift */,
AC9104846487244648D32C6D /* AudioPlayerProtocol.swift */,
2BDB3E65A79779EDA5D33D8A /* AudioPlayerState.swift */,
);
path = Player;
sourceTree = "<group>";
};
3A304097A59704AC9B869EC6 /* Helpers */ = {
isa = PBXGroup;
children = (
@ -2491,6 +2529,11 @@
2FD0E68C42CA7DDCD4CAD68D /* MentionSuggestionItemView.swift */,
A0A01AECFF54281CF35909A6 /* MessageComposer.swift */,
3E6A9B9DFEE964962C179DE3 /* RoomAttachmentPicker.swift */,
BFC9F57320EC80C7CE34FE4A /* VoiceMessagePreviewComposer.swift */,
D2E61DDB42C0DE429C0955D8 /* VoiceMessageRecordingButton.swift */,
FF449205DF1E9817115245C4 /* VoiceMessageRecordingButtonTooltipView.swift */,
CCB6F36CCE44A29A06FCAF1C /* VoiceMessageRecordingComposer.swift */,
0A634D8DD1E10D858CF7995D /* VoiceMessageRecordingView.swift */,
);
path = View;
sourceTree = "<group>";
@ -2820,6 +2863,7 @@
AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */,
37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */,
89233612A8632AD7E2803620 /* AudioPlayerStateTests.swift */,
C55CC239AE12339C565F6C9A /* AudioRecorderStateTests.swift */,
6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */,
EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */,
7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */,
@ -3346,18 +3390,6 @@
path = TimelineItems;
sourceTree = "<group>";
};
984A887BA0294FE3B00CE9B1 /* AudioPlayer */ = {
isa = PBXGroup;
children = (
CE1CD5EC6265A09315772DB7 /* AudioConverter.swift */,
FB4D7B3FBCA261927536FE64 /* AudioConverterProtocol.swift */,
2EDDE503C9BAC82B661E4164 /* AudioPlayer.swift */,
048A3590E8379BCED4D30D5C /* AudioPlayerProtocol.swift */,
FA88C615D8BFCCEF0D2FEAC9 /* AudioPlayerState.swift */,
);
path = AudioPlayer;
sourceTree = "<group>";
};
99B9B46F2D621380428E68F7 /* ElementX */ = {
isa = PBXGroup;
children = (
@ -4146,6 +4178,17 @@
path = Timeline;
sourceTree = "<group>";
};
FCE7249621F507F34A8122FB /* Audio */ = {
isa = PBXGroup;
children = (
E44E35AA87F49503E7B3BF6E /* AudioConverter.swift */,
2757B1BE23DF8AA239937243 /* AudioConverterProtocol.swift */,
3A2CAA4ABF5E66C3C8BBA3E9 /* Player */,
0371482D36C95ABAF9D4C651 /* Recorder */,
);
path = Audio;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -4641,6 +4684,7 @@
90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */,
5100F53E6884A15F9BA07CC3 /* AttributedStringTests.swift in Sources */,
C1429699A6A5BB09A25775C1 /* AudioPlayerStateTests.swift in Sources */,
3042527CB344A9EF1157FC26 /* AudioRecorderStateTests.swift in Sources */,
0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */,
7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */,
C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */,
@ -4788,11 +4832,14 @@
D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */,
3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */,
A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */,
5F8E96263497FFB7D3254EB2 /* AudioConverter.swift in Sources */,
8418688282763F4B9DDC42FB /* AudioConverterProtocol.swift in Sources */,
CD1C6943F42F29079E5E7511 /* AudioPlayer.swift in Sources */,
F0B196905CD23E3B4505CB7B /* AudioPlayerProtocol.swift in Sources */,
C19085A284D54A166A64A86C /* AudioPlayerState.swift in Sources */,
2DA27D78560D5F79B917E163 /* AudioConverter.swift in Sources */,
F3F38062C6CA21CF403C5C90 /* AudioConverterProtocol.swift in Sources */,
46C9F8FE3810A04A005FE16B /* AudioPlayer.swift in Sources */,
086D01E79C8E8D3F004FAF21 /* AudioPlayerProtocol.swift in Sources */,
1471A080552631358D152C18 /* AudioPlayerState.swift in Sources */,
62A7FC3A0191BC7181AA432B /* AudioRecorder.swift in Sources */,
3C31E1A65EEB61E72E1113B4 /* AudioRecorderProtocol.swift in Sources */,
5992EF10AA157EBD97D88910 /* AudioRecorderState.swift in Sources */,
F8E725D42023ECA091349245 /* AudioRoomTimelineItem.swift in Sources */,
88F348E2CB14FF71CBBB665D /* AudioRoomTimelineItemContent.swift in Sources */,
E62EC30B39354A391E32A126 /* AudioRoomTimelineView.swift in Sources */,
@ -5316,6 +5363,11 @@
4F2DF6138E87A4B8C2488CA3 /* VoiceMessageCacheProtocol.swift in Sources */,
386720B603F87D156DB01FB2 /* VoiceMessageMediaManager.swift in Sources */,
9DE801D278AC34737467F937 /* VoiceMessageMediaManagerProtocol.swift in Sources */,
33CA777C9DF263582D77A67F /* VoiceMessagePreviewComposer.swift in Sources */,
09EF4222EEBBA1A7B8F4071E /* VoiceMessageRecordingButton.swift in Sources */,
8C27BEB00B903D953F31F962 /* VoiceMessageRecordingButtonTooltipView.swift in Sources */,
CA5BFF0C2EF5A8EF40CA2D69 /* VoiceMessageRecordingComposer.swift in Sources */,
EBDB339A7C127F068B6E52E5 /* VoiceMessageRecordingView.swift in Sources */,
A9482B967FC85DA611514D35 /* VoiceMessageRoomPlaybackView.swift in Sources */,
024E70451A7CD9E4E034D8A9 /* VoiceMessageRoomTimelineItem.swift in Sources */,
87B4E59A4467F8EC75F82372 /* VoiceMessageRoomTimelineView.swift in Sources */,

View File

@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "mic-fill.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="mic_FILL1_wght400_GRAD0_opsz20 1">
<path id="Vector" d="M12.0294 14.15C11.1764 14.15 10.4458 13.8458 9.83746 13.2375C9.22913 12.6292 8.92496 11.9 8.92496 11.05V5.075C8.92496 4.22083 9.22913 3.49479 9.83746 2.89688C10.4458 2.29896 11.1764 2 12.0294 2C12.8823 2 13.6073 2.29896 14.2044 2.89688C14.8014 3.49479 15.1 4.22083 15.1 5.075V11.05C15.1 11.9 14.8014 12.6292 14.2044 13.2375C13.6073 13.8458 12.8823 14.15 12.0294 14.15ZM12.025 21C11.7416 21 11.4958 20.8958 11.2875 20.6875C11.0791 20.4792 10.975 20.2333 10.975 19.95V18.0047C9.44163 17.8016 8.14163 17.1625 7.07496 16.0875C6.0083 15.0125 5.34996 13.725 5.09996 12.225C5.04996 11.9153 5.13136 11.6421 5.34416 11.4052C5.55698 11.1684 5.84225 11.05 6.19996 11.05C6.4333 11.05 6.64163 11.1333 6.82496 11.3C7.0083 11.4667 7.12496 11.6833 7.17496 11.95C7.38833 13.0953 7.94636 14.0525 8.84906 14.8215C9.75178 15.5905 10.8104 15.975 12.025 15.975C13.2332 15.975 14.2864 15.5905 15.1845 14.8215C16.0825 14.0525 16.6377 13.0953 16.85 11.95C16.9 11.6833 17.0166 11.4667 17.2 11.3C17.3833 11.1333 17.6003 11.05 17.8509 11.05C18.191 11.05 18.4675 11.1684 18.6805 11.4052C18.8935 11.6421 18.975 11.9153 18.925 12.225C18.6949 13.7279 18.0457 15.0162 16.9774 16.0897C15.9091 17.1632 14.6083 17.8016 13.075 18.0047V19.95C13.075 20.2333 12.9708 20.4792 12.7625 20.6875C12.5541 20.8958 12.3083 21 12.025 21Z" fill="#1B1D22"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "mic.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="mic_FILL0_wght400_GRAD0_opsz24 1">
<path id="Vector" d="M12.0001 14C11.1667 14 10.4584 13.7083 9.87506 13.125C9.29173 12.5417 9.00006 11.8333 9.00006 11V5C9.00006 4.16667 9.29173 3.45833 9.87506 2.875C10.4584 2.29167 11.1667 2 12.0001 2C12.8334 2 13.5417 2.29167 14.1251 2.875C14.7084 3.45833 15.0001 4.16667 15.0001 5V11C15.0001 11.8333 14.7084 12.5417 14.1251 13.125C13.5417 13.7083 12.8334 14 12.0001 14ZM12.0001 21C11.7167 21 11.4792 20.9042 11.2876 20.7125C11.0959 20.5208 11.0001 20.2833 11.0001 20V17.925C9.45006 17.7083 8.13339 17.0583 7.05006 15.975C5.96673 14.8917 5.30839 13.5917 5.07506 12.075C5.02506 11.7917 5.10006 11.5417 5.30006 11.325C5.50006 11.1083 5.76673 11 6.10006 11C6.33339 11 6.54173 11.0875 6.72506 11.2625C6.90839 11.4375 7.02506 11.65 7.07506 11.9C7.29173 13.0667 7.85839 14.0417 8.77506 14.825C9.69173 15.6083 10.7667 16 12.0001 16C13.2334 16 14.3084 15.6083 15.2251 14.825C16.1417 14.0417 16.7084 13.0667 16.9251 11.9C16.9751 11.65 17.0959 11.4375 17.2876 11.2625C17.4792 11.0875 17.6917 11 17.9251 11C18.2417 11 18.5001 11.1083 18.7001 11.325C18.9001 11.5417 18.9751 11.7917 18.9251 12.075C18.6917 13.5917 18.0334 14.8917 16.9501 15.975C15.8667 17.0583 14.5501 17.7083 13.0001 17.925V20C13.0001 20.2833 12.9042 20.5208 12.7126 20.7125C12.5209 20.9042 12.2834 21 12.0001 21ZM12.0001 12C12.2834 12 12.5209 11.9042 12.7126 11.7125C12.9042 11.5208 13.0001 11.2833 13.0001 11V5C13.0001 4.71667 12.9042 4.47917 12.7126 4.2875C12.5209 4.09583 12.2834 4 12.0001 4C11.7167 4 11.4792 4.09583 11.2876 4.2875C11.0959 4.47917 11.0001 4.71667 11.0001 5V11C11.0001 11.2833 11.0959 11.5208 11.2876 11.7125C11.4792 11.9042 11.7167 12 12.0001 12Z" fill="#656D77"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -17,4 +17,3 @@
"soft_logout_clear_data_submit" = "Clear all data";
"soft_logout_clear_data_dialog_title" = "Clear data";
"soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages.";

View File

@ -49,6 +49,8 @@ internal enum Asset {
internal static let inlineCode = ImageAsset(name: "images/inline-code")
internal static let italic = ImageAsset(name: "images/italic")
internal static let link = ImageAsset(name: "images/link")
internal static let micFill = ImageAsset(name: "images/mic-fill")
internal static let mic = ImageAsset(name: "images/mic")
internal static let numberedList = ImageAsset(name: "images/numbered-list")
internal static let quote = ImageAsset(name: "images/quote")
internal static let sendMessage = ImageAsset(name: "images/send-message")

View File

@ -246,6 +246,74 @@ class AudioPlayerMock: AudioPlayerProtocol {
await seekToClosure?(progress)
}
}
class AudioRecorderMock: AudioRecorderProtocol {
var actions: AnyPublisher<AudioRecorderAction, Never> {
get { return underlyingActions }
set(value) { underlyingActions = value }
}
var underlyingActions: AnyPublisher<AudioRecorderAction, Never>!
var currentTime: TimeInterval {
get { return underlyingCurrentTime }
set(value) { underlyingCurrentTime = value }
}
var underlyingCurrentTime: TimeInterval!
var isRecording: Bool {
get { return underlyingIsRecording }
set(value) { underlyingIsRecording = value }
}
var underlyingIsRecording: Bool!
var url: URL?
//MARK: - recordWithOutputURL
var recordWithOutputURLCallsCount = 0
var recordWithOutputURLCalled: Bool {
return recordWithOutputURLCallsCount > 0
}
var recordWithOutputURLReceivedUrl: URL?
var recordWithOutputURLReceivedInvocations: [URL] = []
var recordWithOutputURLClosure: ((URL) -> Void)?
func recordWithOutputURL(_ url: URL) {
recordWithOutputURLCallsCount += 1
recordWithOutputURLReceivedUrl = url
recordWithOutputURLReceivedInvocations.append(url)
recordWithOutputURLClosure?(url)
}
//MARK: - stopRecording
var stopRecordingCallsCount = 0
var stopRecordingCalled: Bool {
return stopRecordingCallsCount > 0
}
var stopRecordingClosure: (() -> Void)?
func stopRecording() {
stopRecordingCallsCount += 1
stopRecordingClosure?()
}
//MARK: - averagePowerForChannelNumber
var averagePowerForChannelNumberCallsCount = 0
var averagePowerForChannelNumberCalled: Bool {
return averagePowerForChannelNumberCallsCount > 0
}
var averagePowerForChannelNumberReceivedChannelNumber: Int?
var averagePowerForChannelNumberReceivedInvocations: [Int] = []
var averagePowerForChannelNumberReturnValue: Float!
var averagePowerForChannelNumberClosure: ((Int) -> Float)?
func averagePowerForChannelNumber(_ channelNumber: Int) -> Float {
averagePowerForChannelNumberCallsCount += 1
averagePowerForChannelNumberReceivedChannelNumber = channelNumber
averagePowerForChannelNumberReceivedInvocations.append(channelNumber)
if let averagePowerForChannelNumberClosure = averagePowerForChannelNumberClosure {
return averagePowerForChannelNumberClosure(channelNumber)
} else {
return averagePowerForChannelNumberReturnValue
}
}
}
class BugReportServiceMock: BugReportServiceProtocol {
var isRunning: Bool {
get { return underlyingIsRunning }

View File

@ -30,6 +30,11 @@ enum ComposerToolbarViewModelAction {
case composerModeChanged(mode: RoomScreenComposerMode)
case composerFocusedChanged(isFocused: Bool)
case startRecordingVoiceMessage
case stopRecordingVoiceMessage
case deleteRecordedVoiceMessage
case sendVoiceMessage
}
enum ComposerToolbarViewAction {
@ -46,6 +51,9 @@ enum ComposerToolbarViewAction {
case enableTextFormatting
case composerAction(action: ComposerAction)
case selectedSuggestion(_ suggestion: SuggestionItem)
case startRecordingVoiceMessage
case stopRecordingVoiceMessage
case deleteRecordedVoiceMessage
}
struct ComposerToolbarViewState: BindableState {
@ -53,11 +61,33 @@ struct ComposerToolbarViewState: BindableState {
var composerEmpty = true
var areSuggestionsEnabled = true
var suggestions: [SuggestionItem] = []
var enableVoiceMessageComposer: Bool
var audioPlayerState: AudioPlayerState
var audioRecorderState: AudioRecorderState
var bindings: ComposerToolbarViewStateBindings
var showSendButton: Bool {
switch composerMode {
case .recordVoiceMessage:
return false
case .previewVoiceMessage:
return true
default:
if enableVoiceMessageComposer {
return !composerEmpty
} else {
return true
}
}
}
var sendButtonDisabled: Bool {
composerEmpty
if case .previewVoiceMessage = composerMode {
return false
}
return composerEmpty
}
}

View File

@ -45,7 +45,12 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
self.completionSuggestionService = completionSuggestionService
self.appSettings = appSettings
super.init(initialViewState: ComposerToolbarViewState(areSuggestionsEnabled: completionSuggestionService.areSuggestionsEnabled, bindings: .init()), imageProvider: mediaProvider)
super.init(initialViewState: ComposerToolbarViewState(areSuggestionsEnabled: completionSuggestionService.areSuggestionsEnabled,
enableVoiceMessageComposer: appSettings.voiceMessageEnabled,
audioPlayerState: .init(duration: 0),
audioRecorderState: .init(),
bindings: .init()),
imageProvider: mediaProvider)
context.$viewState
.map(\.composerMode)
@ -98,10 +103,16 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
wysiwygViewModel.setup()
case .sendMessage:
guard !state.sendButtonDisabled else { return }
let sendHTML = ServiceLocator.shared.settings.richTextEditorEnabled
actionsSubject.send(.sendMessage(plain: wysiwygViewModel.content.markdown,
html: sendHTML ? wysiwygViewModel.content.html : nil,
mode: state.composerMode))
switch state.composerMode {
case .previewVoiceMessage:
actionsSubject.send(.sendVoiceMessage)
default:
let sendHTML = ServiceLocator.shared.settings.richTextEditorEnabled
actionsSubject.send(.sendMessage(plain: wysiwygViewModel.content.markdown,
html: sendHTML ? wysiwygViewModel.content.html : nil,
mode: state.composerMode))
}
case .cancelReply:
set(mode: .default)
case .cancelEdit:
@ -130,6 +141,13 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
}
case .selectedSuggestion(let suggestion):
handleSuggestion(suggestion)
case .startRecordingVoiceMessage:
state.bindings.composerActionsEnabled = false
actionsSubject.send(.startRecordingVoiceMessage)
case .stopRecordingVoiceMessage:
actionsSubject.send(.stopRecordingVoiceMessage)
case .deleteRecordedVoiceMessage:
actionsSubject.send(.deleteRecordedVoiceMessage)
}
}
@ -197,7 +215,15 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
guard mode != state.composerMode else { return }
state.composerMode = mode
if mode != .default {
switch mode {
case .default:
break
case .recordVoiceMessage(let audioRecorderState):
state.bindings.composerFocused = false
state.audioRecorderState = audioRecorderState
case .previewVoiceMessage(let audioPlayerState):
state.audioPlayerState = audioPlayerState
case .edit, .reply:
// Focus composer when switching to reply/edit
state.bindings.composerFocused = true
}

View File

@ -22,16 +22,21 @@ struct ComposerToolbar: View {
@ObservedObject var context: ComposerToolbarViewModel.Context
let wysiwygViewModel: WysiwygComposerViewModel
let keyCommandHandler: KeyCommandHandler
@FocusState private var composerFocused: Bool
@ScaledMetric private var sendButtonIconSize = 16
@ScaledMetric private var trashButtonIconSize = 24
@ScaledMetric(relativeTo: .title) private var closeRTEButtonSize = 30
@State private var showVoiceMessageRecordingTooltip = false
@ScaledMetric private var voiceMessageTooltipPointerHeight = 6
@State private var frame: CGRect = .zero
var body: some View {
VStack(spacing: 8) {
topBar
if context.composerActionsEnabled {
bottomBar
}
@ -45,6 +50,10 @@ struct ComposerToolbar: View {
.offset(y: -frame.height)
}
}
.overlay(alignment: .bottomTrailing) {
voiceMessageRecordingButtonTooltipView
.offset(y: -frame.height - voiceMessageTooltipPointerHeight)
}
.alert(item: $context.alertInfo)
}
@ -53,42 +62,55 @@ struct ComposerToolbar: View {
context.send(viewAction: .selectedSuggestion(suggestion))
}
}
private var topBar: some View {
HStack(alignment: .bottom, spacing: 5) {
if !context.composerActionsEnabled {
RoomAttachmentPicker(context: context)
switch context.viewState.composerMode {
case .recordVoiceMessage(let state) where context.viewState.enableVoiceMessageComposer:
VoiceMessageRecordingComposer(recorderState: state)
.padding(.leading, 12)
case .previewVoiceMessage(let state) where context.viewState.enableVoiceMessageComposer:
voiceMessageTrashButton
VoiceMessagePreviewComposer(playerState: state)
default:
if !context.composerActionsEnabled {
RoomAttachmentPicker(context: context)
}
messageComposer
.environmentObject(context)
.onTapGesture {
guard !composerFocused else { return }
composerFocused = true
}
.padding(.leading, context.composerActionsEnabled ? 7 : 0)
.padding(.trailing, context.composerActionsEnabled ? 4 : 0)
}
messageComposer
.environmentObject(context)
.onTapGesture {
guard !composerFocused else { return }
composerFocused = true
}
.padding(.leading, context.composerActionsEnabled ? 7 : 0)
.padding(.trailing, context.composerActionsEnabled ? 4 : 0)
if !context.composerActionsEnabled {
sendButton
.padding(.leading, 3)
if context.viewState.showSendButton {
sendButton
.padding(.leading, 3)
} else if context.viewState.enableVoiceMessageComposer {
voiceMessageRecordingButton
.padding(.leading, 4)
}
}
}
}
private var bottomBar: some View {
HStack(alignment: .center, spacing: 9) {
closeRTEButton
FormattingToolbar(formatItems: context.formatItems) { action in
context.send(viewAction: .composerAction(action: action.composerAction))
}
sendButton
.padding(.leading, 7)
}
}
private var closeRTEButton: some View {
Button {
context.composerActionsEnabled = false
@ -103,7 +125,7 @@ struct ComposerToolbar: View {
.accessibilityLabel(L10n.actionClose)
.accessibilityIdentifier(A11yIdentifiers.roomScreen.composerToolbar.closeFormattingOptions)
}
private var sendButton: some View {
Button {
context.send(viewAction: .sendMessage)
@ -141,7 +163,7 @@ struct ComposerToolbar: View {
.focused($composerFocused)
.onChange(of: context.composerFocused) { newValue in
guard composerFocused != newValue else { return }
composerFocused = newValue
}
.onChange(of: composerFocused) { newValue in
@ -167,7 +189,7 @@ struct ComposerToolbar: View {
context.send(viewAction: .handlePasteOrDrop(provider: provider))
}
}
private var submitButtonImage: some View {
// ZStack with opacity so the button size is consistent.
ZStack {
@ -184,12 +206,44 @@ struct ComposerToolbar: View {
.accessibilityHidden(context.viewState.composerMode.isEdit)
}
}
private class ItemProviderHelper: WysiwygItemProviderHelper {
func isPasteSupported(for itemProvider: NSItemProvider) -> Bool {
itemProvider.isSupportedForPasteOrDrop
}
}
// MARK: - Voice message
private var voiceMessageRecordingButton: some View {
VoiceMessageRecordingButton(showRecordTooltip: $showVoiceMessageRecordingTooltip, startRecording: {
context.send(viewAction: .startRecordingVoiceMessage)
}, stopRecording: {
context.send(viewAction: .stopRecordingVoiceMessage)
})
.padding(4)
}
private var voiceMessageTrashButton: some View {
Button {
context.send(viewAction: .deleteRecordedVoiceMessage)
} label: {
CompoundIcon(\.delete)
.font(.compound.bodyLG)
.foregroundColor(.compound.textCriticalPrimary)
.frame(width: trashButtonIconSize, height: trashButtonIconSize)
.padding(EdgeInsets(top: 10, leading: 11, bottom: 10, trailing: 11))
.fixedSize()
.accessibilityLabel(L10n.a11yDelete)
}
}
private var voiceMessageRecordingButtonTooltipView: some View {
VoiceMessageRecordingButtonTooltipView(text: L10n.screenRoomVoiceMessageTooltip, pointerHeight: voiceMessageTooltipPointerHeight)
.allowsHitTesting(false)
.opacity(showVoiceMessageRecordingTooltip ? 1.0 : 0.0)
.animation(.elementDefault, value: showVoiceMessageRecordingTooltip)
}
}
struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
@ -203,7 +257,7 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
.user(item: MentionSuggestionItem(id: "@user_mention_2:matrix.org", displayName: "User 2", avatarURL: URL.documentsDirectory))]
static var previews: some View {
ComposerToolbar.mock()
ComposerToolbar.mock(focused: true)
// Putting them is VStack allows the completion suggestion preview to work properly in tests
VStack {
@ -213,19 +267,82 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
keyCommandHandler: { _ in false })
}
.previewDisplayName("With Suggestions")
VStack {
ComposerToolbar.textWithVoiceMessage(focused: false)
ComposerToolbar.textWithVoiceMessage(focused: true)
ComposerToolbar.voiceMessageRecordingMock(recording: true)
ComposerToolbar.voiceMessagePreviewMock(recording: false)
}
.previewDisplayName("Voice Message")
}
}
// MARK: - Mock
extension ComposerToolbar {
static func mock() -> ComposerToolbar {
static func mock(focused: Bool = true) -> ComposerToolbar {
let wysiwygViewModel = WysiwygComposerViewModel()
let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MockMediaProvider(),
appSettings: ServiceLocator.shared.settings,
mentionDisplayHelper: ComposerMentionDisplayHelper.mock)
var composerViewModel: ComposerToolbarViewModel {
let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MockMediaProvider(),
appSettings: ServiceLocator.shared.settings,
mentionDisplayHelper: ComposerMentionDisplayHelper.mock)
model.state.composerEmpty = focused
return model
}
return ComposerToolbar(context: composerViewModel.context,
wysiwygViewModel: wysiwygViewModel,
keyCommandHandler: { _ in false })
}
static func textWithVoiceMessage(focused: Bool = true) -> ComposerToolbar {
let wysiwygViewModel = WysiwygComposerViewModel()
var composerViewModel: ComposerToolbarViewModel {
let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MockMediaProvider(),
appSettings: ServiceLocator.shared.settings,
mentionDisplayHelper: ComposerMentionDisplayHelper.mock)
model.state.composerEmpty = focused
model.state.enableVoiceMessageComposer = true
return model
}
return ComposerToolbar(context: composerViewModel.context,
wysiwygViewModel: wysiwygViewModel,
keyCommandHandler: { _ in false })
}
static func voiceMessageRecordingMock(recording: Bool) -> ComposerToolbar {
let wysiwygViewModel = WysiwygComposerViewModel()
var composerViewModel: ComposerToolbarViewModel {
let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MockMediaProvider(),
appSettings: ServiceLocator.shared.settings,
mentionDisplayHelper: ComposerMentionDisplayHelper.mock)
model.state.composerMode = .recordVoiceMessage(state: AudioRecorderState())
model.state.enableVoiceMessageComposer = true
return model
}
return ComposerToolbar(context: composerViewModel.context,
wysiwygViewModel: wysiwygViewModel,
keyCommandHandler: { _ in false })
}
static func voiceMessagePreviewMock(recording: Bool) -> ComposerToolbar {
let wysiwygViewModel = WysiwygComposerViewModel()
var composerViewModel: ComposerToolbarViewModel {
let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MockMediaProvider(),
appSettings: ServiceLocator.shared.settings,
mentionDisplayHelper: ComposerMentionDisplayHelper.mock)
model.state.composerMode = .previewVoiceMessage(state: AudioPlayerState(duration: 10.0))
model.state.enableVoiceMessageComposer = true
return model
}
return ComposerToolbar(context: composerViewModel.context,
wysiwygViewModel: wysiwygViewModel,
keyCommandHandler: { _ in false })

View File

@ -90,7 +90,7 @@ struct MessageComposer: View {
MessageComposerReplyHeader(replyDetails: replyDetails, action: replyCancellationAction)
case .edit:
MessageComposerEditHeader(action: editCancellationAction)
case .default:
case .recordVoiceMessage, .previewVoiceMessage, .default:
EmptyView()
}
}

View File

@ -0,0 +1,63 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import SwiftUI
struct VoiceMessagePreviewComposer: View {
@ObservedObject var playerState: AudioPlayerState
@State private var resumePlaybackAfterScrubbing = false
var body: some View {
VoiceMessageRoomPlaybackView(playerState: playerState,
onPlayPause: onPlaybackPlayPause,
onSeek: { onPlaybackSeek($0) },
onScrubbing: { onPlaybackScrubbing($0) })
.padding(.vertical, 4.0)
.padding(.horizontal, 6.0)
.background {
let roundedRectangle = RoundedRectangle(cornerRadius: 12)
ZStack {
roundedRectangle
.fill(Color.compound.bgSubtleSecondary)
roundedRectangle
.stroke(Color.compound._borderTextFieldFocused, lineWidth: 0.5)
}
}
.frame(minHeight: 42)
.fixedSize(horizontal: false, vertical: true)
}
private func onPlaybackPlayPause() { }
private func onPlaybackSeek(_ progress: Double) { }
private func onPlaybackScrubbing(_ dragging: Bool) { }
}
struct VoiceMessagePreviewComposer_Previews: PreviewProvider, TestablePreview {
static let playerState = AudioPlayerState(duration: 10.0,
waveform: Waveform.mockWaveform,
progress: 0.4)
static var previews: some View {
VStack {
VoiceMessagePreviewComposer(playerState: playerState)
.fixedSize(horizontal: false, vertical: true)
}
}
}

View File

@ -0,0 +1,83 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Compound
import SwiftUI
struct VoiceMessageRecordingButton: View {
@ScaledMetric private var buttonIconSize = 24
@State private var longPressConfirmed = false
@State private var buttonPressed = false
@State private var longPressTask = VoiceMessageButtonTask()
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
private let delayBeforeRecording = 500
@Binding var showRecordTooltip: Bool
var startRecording: (() -> Void)?
var stopRecording: (() -> Void)?
var body: some View {
Button { } label: {
voiceMessageButtonImage
}
.onLongPressGesture(perform: { }, onPressingChanged: { pressing in
buttonPressed = pressing
if pressing {
showRecordTooltip = true
feedbackGenerator.prepare()
longPressTask.task = Task {
try? await Task.sleep(for: .milliseconds(delayBeforeRecording))
guard !Task.isCancelled else {
return
}
feedbackGenerator.impactOccurred()
showRecordTooltip = false
longPressConfirmed = true
startRecording?()
}
} else {
longPressTask.task?.cancel()
showRecordTooltip = false
guard longPressConfirmed else { return }
longPressConfirmed = false
stopRecording?()
}
})
.fixedSize()
}
@ViewBuilder
private var voiceMessageButtonImage: some View {
(buttonPressed ? Image(asset: Asset.Images.micFill) : Image(asset: Asset.Images.mic))
.resizable()
.frame(width: buttonIconSize, height: buttonIconSize)
.foregroundColor(.compound.iconSecondary)
.padding(EdgeInsets(top: 6, leading: 6, bottom: 6, trailing: 6))
.accessibilityLabel(L10n.a11yVoiceMessageRecord)
}
}
private class VoiceMessageButtonTask {
@CancellableTask var task: Task<Void, Never>?
}
struct VoiceMessageRecordingButton_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
VoiceMessageRecordingButton(showRecordTooltip: .constant(false))
.fixedSize(horizontal: true, vertical: true)
}
}

View File

@ -0,0 +1,101 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Compound
import Foundation
import SwiftUI
struct VoiceMessageRecordingButtonTooltipView: View {
var text: String
var radius: CGFloat = 4
var corners: UIRectCorner = .allCorners
@ScaledMetric var pointerHeight: CGFloat = 6
@ScaledMetric var pointerWidth: CGFloat = 10
@ScaledMetric var pointerOffset: CGFloat = 8
var body: some View {
Text(text)
.font(.compound.bodySMSemibold)
.foregroundColor(.compound.textOnSolidPrimary)
.padding(6)
.background(
TooltipShape(radius: radius,
corners: corners,
pointerHeight: pointerHeight,
pointerWidth: pointerWidth,
pointerOffset: pointerOffset)
.fill(.compound.bgActionPrimaryRest)
)
.padding(.trailing, 8)
}
}
private struct TooltipShape: Shape {
var radius: CGFloat = 4
var corners: UIRectCorner = .allCorners
var pointerHeight: CGFloat = 6
var pointerWidth: CGFloat = 10
var pointerOffset: CGFloat = 8
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.size.width
let height = rect.size.height
var topLeft: CGFloat = corners.contains(.topLeft) ? radius : 0.0
var topRight: CGFloat = corners.contains(.topRight) ? radius : 0.0
var bottomLeft: CGFloat = corners.contains(.bottomLeft) ? radius : 0.0
var bottomRight: CGFloat = corners.contains(.bottomRight) ? radius : 0.0
// Make sure we do not exceed the size of the rectangle
topRight = min(min(topRight, height / 2), width / 2)
topLeft = min(min(topLeft, height / 2), width / 2)
bottomLeft = min(min(bottomLeft, height / 2), width / 2)
bottomRight = min(min(bottomRight, height / 2), width / 2)
path.move(to: CGPoint(x: width / 2.0, y: 0))
path.addLine(to: CGPoint(x: width - topRight, y: 0))
path.addArc(center: CGPoint(x: width - topRight, y: topRight), radius: topRight,
startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false)
path.addLine(to: CGPoint(x: width, y: height - bottomRight))
path.addArc(center: CGPoint(x: width - bottomRight, y: height - bottomRight), radius: bottomRight,
startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false)
path.addLine(to: CGPoint(x: width - pointerOffset, y: height))
path.addLine(to: CGPoint(x: width - pointerOffset - (pointerWidth / 2.0), y: height + pointerHeight))
path.addLine(to: CGPoint(x: width - pointerOffset - pointerWidth, y: height))
path.addLine(to: CGPoint(x: bottomLeft, y: height))
path.addArc(center: CGPoint(x: bottomLeft, y: height - bottomLeft), radius: bottomLeft,
startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false)
path.addLine(to: CGPoint(x: 0, y: topLeft))
path.addArc(center: CGPoint(x: topLeft, y: topLeft), radius: topLeft,
startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false)
path.closeSubpath()
return path
}
}
struct VoiceMessageRecordingButtonTooltipView_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
VoiceMessageRecordingButtonTooltipView(text: "Hold to record")
.fixedSize()
}
}

View File

@ -0,0 +1,57 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Compound
import Foundation
import SwiftUI
struct VoiceMessageRecordingComposer: View {
@ObservedObject var recorderState: AudioRecorderState
var body: some View {
VoiceMessageRecordingView(recorderState: recorderState)
.padding(.vertical, 8.0)
.padding(.horizontal, 12.0)
.background {
let roundedRectangle = RoundedRectangle(cornerRadius: 12)
ZStack {
roundedRectangle
.fill(Color.compound.bgSubtleSecondary)
roundedRectangle
.stroke(Color.compound._borderTextFieldFocused, lineWidth: 0.5)
}
}
.frame(minHeight: 42)
.fixedSize(horizontal: false, vertical: true)
}
private func onPlaybackPlayPause() { }
private func onPlaybackSeek(_ progress: Double) { }
private func onPlaybackScrubbing(_ dragging: Bool) { }
}
struct VoiceMessageRecordingComposer_Previews: PreviewProvider, TestablePreview {
static let recorderState = AudioRecorderState()
static var previews: some View {
VStack {
VoiceMessageRecordingComposer(recorderState: recorderState)
.fixedSize(horizontal: false, vertical: true)
}
}
}

View File

@ -0,0 +1,67 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Compound
import Foundation
import SwiftUI
struct VoiceMessageRecordingView: View {
@ObservedObject var recorderState: AudioRecorderState
@ScaledMetric private var waveformLineWidth = 2.0
@ScaledMetric private var waveformLinePadding = 2.0
@ScaledMetric private var recordingIndicatorSize = 8
private static let elapsedTimeFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "mm:ss"
return dateFormatter
}()
var timeLabelContent: String {
Self.elapsedTimeFormatter.string(from: Date(timeIntervalSinceReferenceDate: recorderState.duration))
}
var body: some View {
HStack {
Circle()
.frame(width: recordingIndicatorSize, height: recordingIndicatorSize)
.foregroundColor(.red)
Text(timeLabelContent)
.lineLimit(1)
.font(.compound.bodySMSemibold)
.foregroundColor(.compound.textSecondary)
.monospacedDigit()
.fixedSize()
WaveformView(lineWidth: waveformLineWidth, linePadding: waveformLinePadding, waveform: recorderState.waveform, progress: 0, showCursor: false)
}
.padding(.leading, 2)
.padding(.trailing, 8)
}
}
struct VoiceMessageRecordingView_Previews: PreviewProvider, TestablePreview {
static let waveform = Waveform(data: [3, 127, 400, 266, 126, 122, 373, 251, 45, 112,
334, 205, 99, 138, 397, 354, 125, 361, 199, 51,
294, 131, 19, 2, 3, 3, 1, 2, 0, 0,
0, 0, 0, 0, 0, 3])
static let recorderState = AudioRecorderState()
static var previews: some View {
VoiceMessageRecordingView(recorderState: recorderState)
.fixedSize(horizontal: false, vertical: true)
}
}

View File

@ -40,6 +40,8 @@ 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)
var isEdit: Bool {
switch self {

View File

@ -186,6 +186,15 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
trackComposerMode(mode)
case .composerFocusedChanged(isFocused: let isFocused):
composerFocusedSubject.send(isFocused)
case .startRecordingVoiceMessage:
timelineController.pauseAudio()
startRecordingVoiceMessage()
case .stopRecordingVoiceMessage:
stopRecordingVoiceMessage()
case .deleteRecordedVoiceMessage:
deleteCurrentVoiceMessage()
case .sendVoiceMessage:
Task { await sendCurrentVoiceMessage() }
}
}
@ -466,9 +475,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
await timelineController.editMessage(message, html: html, original: originalItemId)
case .default:
await timelineController.sendMessage(message, html: html)
case .recordVoiceMessage, .previewVoiceMessage:
fatalError("invalid composer mode.")
}
}
private func trackComposerMode(_ mode: RoomScreenComposerMode) {
var isEdit = false
var isReply = false
@ -888,6 +899,34 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private func audioPlayerState(for itemID: TimelineItemIdentifier) -> AudioPlayerState {
timelineController.audioPlayerState(for: itemID)
}
// MARK: - Voice message
private func startRecordingVoiceMessage() {
// Partially implemented
let audioRecordState = AudioRecorderState()
actionsSubject.send(.composer(action: .setMode(mode: .recordVoiceMessage(state: audioRecordState))))
}
private func stopRecordingVoiceMessage() {
// Partially implemented
let audioPlayerState = AudioPlayerState(duration: 0)
actionsSubject.send(.composer(action: .setMode(mode: .previewVoiceMessage(state: audioPlayerState))))
}
private func deleteCurrentVoiceMessage() {
// Partially implemented
actionsSubject.send(.composer(action: .setMode(mode: .default)))
}
private func sendCurrentVoiceMessage() async {
// Partially implemented
actionsSubject.send(.composer(action: .setMode(mode: .default)))
}
}
private extension RoomProxyProtocol {

View File

@ -30,6 +30,7 @@ enum AudioConverterPreferredFileExtension: String {
struct AudioConverter: AudioConverterProtocol {
func convertToOpusOgg(sourceURL: URL, destinationURL: URL) throws {
do {
MXLog.debug("converting \(sourceURL) to \(destinationURL)")
try OGGConverter.convertM4aFileToOpusOGG(src: sourceURL, dest: destinationURL)
} catch {
MXLog.error("failed to convert to OpusOgg: \(error)")

View File

@ -27,7 +27,8 @@ enum AudioPlayerPlaybackState {
}
@MainActor
class AudioPlayerState: ObservableObject {
class AudioPlayerState: ObservableObject, Identifiable {
let id = UUID()
let duration: Double
let waveform: Waveform
@Published private(set) var playbackState: AudioPlayerPlaybackState
@ -150,3 +151,9 @@ class AudioPlayerState: ObservableObject {
await audioPlayer.seek(to: progress)
}
}
extension AudioPlayerState: Equatable {
nonisolated static func == (lhs: AudioPlayerState, rhs: AudioPlayerState) -> Bool {
lhs.id == rhs.id
}
}

View File

@ -0,0 +1,115 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AVFoundation
import Combine
import Foundation
enum AudioRecorderError: Error {
case genericError
}
class AudioRecorder: NSObject, AudioRecorderProtocol, AVAudioRecorderDelegate {
private let silenceThreshold: Float = -50.0
private var audioRecorder: AVAudioRecorder?
private let actionsSubject: PassthroughSubject<AudioRecorderAction, Never> = .init()
var actions: AnyPublisher<AudioRecorderAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
var url: URL? {
audioRecorder?.url
}
var currentTime: TimeInterval {
audioRecorder?.currentTime ?? 0
}
var isRecording: Bool {
audioRecorder?.isRecording ?? false
}
func recordWithOutputURL(_ url: URL) {
let settings = [AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 48000,
AVEncoderBitRateKey: 128_000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue]
do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
audioRecorder = try AVAudioRecorder(url: url, settings: settings)
audioRecorder?.delegate = self
audioRecorder?.isMeteringEnabled = true
audioRecorder?.record()
actionsSubject.send(.didStartRecording)
} catch {
MXLog.error("audio recording failed: \(error)")
actionsSubject.send(.didFailWithError(error: error))
}
}
func stopRecording() {
audioRecorder?.stop()
do {
try AVAudioSession.sharedInstance().setActive(false)
} catch {
actionsSubject.send(.didFailWithError(error: error))
}
}
func peakPowerForChannelNumber(_ channelNumber: Int) -> Float {
guard isRecording, let audioRecorder else {
return 0.0
}
audioRecorder.updateMeters()
return normalizedPowerLevelFromDecibels(audioRecorder.peakPower(forChannel: channelNumber))
}
func averagePowerForChannelNumber(_ channelNumber: Int) -> Float {
guard isRecording, let audioRecorder else {
return 0.0
}
audioRecorder.updateMeters()
return normalizedPowerLevelFromDecibels(audioRecorder.averagePower(forChannel: channelNumber))
}
// MARK: - AVAudioRecorderDelegate
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully success: Bool) {
if success {
actionsSubject.send(.didStopRecording)
} else {
actionsSubject.send(.didFailWithError(error: AudioRecorderError.genericError))
}
}
func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) {
actionsSubject.send(.didFailWithError(error: error ?? AudioRecorderError.genericError))
}
private func normalizedPowerLevelFromDecibels(_ decibels: Float) -> Float {
decibels / silenceThreshold
}
}

View File

@ -0,0 +1,38 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import Foundation
enum AudioRecorderAction {
case didStartRecording
case didStopRecording
case didFailWithError(error: Error)
}
protocol AudioRecorderProtocol: AnyObject {
var actions: AnyPublisher<AudioRecorderAction, Never> { get }
var currentTime: TimeInterval { get }
var isRecording: Bool { get }
var url: URL? { get }
func recordWithOutputURL(_ url: URL)
func stopRecording()
func averagePowerForChannelNumber(_ channelNumber: Int) -> Float
}
// sourcery: AutoMockable
extension AudioRecorderProtocol { }

View File

@ -0,0 +1,114 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import Foundation
import UIKit
enum AudioRecorderRecordingState {
case recording
case stopped
case error
}
@MainActor
class AudioRecorderState: ObservableObject, Identifiable {
let id = UUID()
@Published private(set) var recordingState: AudioRecorderRecordingState = .stopped
@Published private(set) var duration = 0.0
@Published private(set) var waveform = Waveform(data: Array(repeating: 0, count: 100))
private weak var audioRecorder: AudioRecorderProtocol?
private var cancellables: Set<AnyCancellable> = []
private var displayLink: CADisplayLink?
func attachAudioRecorder(_ audioRecorder: AudioRecorderProtocol) {
if self.audioRecorder != nil {
detachAudioRecorder()
}
recordingState = .stopped
self.audioRecorder = audioRecorder
subscribeToAudioRecorder(audioRecorder)
}
func detachAudioRecorder() {
guard audioRecorder != nil else { return }
audioRecorder?.stopRecording()
stopPublishUpdates()
cancellables = []
audioRecorder = nil
recordingState = .stopped
}
func reportError(_ error: Error) {
recordingState = .error
}
// MARK: - Private
private func subscribeToAudioRecorder(_ audioRecorder: AudioRecorderProtocol) {
audioRecorder.actions
.receive(on: DispatchQueue.main)
.sink { [weak self] action in
guard let self else {
return
}
self.handleAudioRecorderAction(action)
}
.store(in: &cancellables)
}
private func handleAudioRecorderAction(_ action: AudioRecorderAction) {
switch action {
case .didStartRecording:
startPublishUpdates()
recordingState = .recording
case .didStopRecording:
stopPublishUpdates()
recordingState = .stopped
case .didFailWithError:
stopPublishUpdates()
recordingState = .stopped
}
}
private func startPublishUpdates() {
if displayLink != nil {
stopPublishUpdates()
}
displayLink = CADisplayLink(target: self, selector: #selector(publishUpdate))
displayLink?.preferredFrameRateRange = .init(minimum: 10, maximum: 20)
displayLink?.add(to: .current, forMode: .common)
}
@objc private func publishUpdate(displayLink: CADisplayLink) {
if let currentTime = audioRecorder?.currentTime {
duration = currentTime
}
}
private func stopPublishUpdates() {
displayLink?.invalidate()
displayLink = nil
}
}
extension AudioRecorderState: Equatable {
nonisolated static func == (lhs: AudioRecorderState, rhs: AudioRecorderState) -> Bool {
lhs.id == rhs.id
}
}

View File

@ -115,7 +115,7 @@ protocol RoomProxyProtocol {
audioInfo: AudioInfo,
progressSubject: CurrentValueSubject<Double, Never>?,
requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result<Void, RoomProxyError>
func sendFile(url: URL,
fileInfo: FileInfo,
progressSubject: CurrentValueSubject<Double, Never>?,

View File

@ -67,7 +67,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
func processItemTap(_ itemID: TimelineItemIdentifier) async -> RoomTimelineControllerAction { .none }
func sendMessage(_ message: String, html: String?, inReplyTo itemID: TimelineItemIdentifier?) async { }
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async { }
func editMessage(_ newMessage: String, html: String?, original itemID: TimelineItemIdentifier) async { }
@ -90,6 +90,8 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
func playPauseAudio(for itemID: TimelineItemIdentifier) async { }
func pauseAudio() { }
func seekAudio(for itemID: TimelineItemIdentifier, progress: Double) async { }
// MARK: - UI Test signalling

View File

@ -240,6 +240,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
if let playerState = timelineAudioPlayerStates[itemID] {
return playerState
}
let playerState = AudioPlayerState(duration: voiceMessageRoomTimelineItem.content.duration,
waveform: voiceMessageRoomTimelineItem.content.waveform)
timelineAudioPlayerStates[itemID] = playerState
@ -278,6 +279,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
// Load content
do {
let url = try await voiceMessageMediaManager.loadVoiceMessageFromSource(source, body: nil)
// Make sure that the player is still attached, as it may have been detached while waiting for the voice message to be loaded.
if playerState.isAttached {
player.load(mediaSource: source, using: url)
@ -297,10 +299,16 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
func pauseAudio() {
timelineAudioPlayerStates.forEach { _, playerState in
playerState.detachAudioPlayer()
}
}
func seekAudio(for itemID: TimelineItemIdentifier, progress: Double) async {
await timelineAudioPlayerStates[itemID]?.updateState(progress: progress)
}
// MARK: - Private
@objc private func contentSizeCategoryDidChange() {

View File

@ -52,7 +52,7 @@ protocol RoomTimelineControllerProtocol {
func sendReadReceipt(for itemID: TimelineItemIdentifier) async -> Result<Void, RoomTimelineControllerError>
func sendMessage(_ message: String, html: String?, inReplyTo itemID: TimelineItemIdentifier?) async
func editMessage(_ newMessage: String, html: String?, original itemID: TimelineItemIdentifier) async
func toggleReaction(_ reaction: String, to itemID: TimelineItemIdentifier) async
@ -69,6 +69,8 @@ protocol RoomTimelineControllerProtocol {
func playPauseAudio(for itemID: TimelineItemIdentifier) async
func pauseAudio()
func seekAudio(for itemID: TimelineItemIdentifier, progress: Double) async
}

View File

@ -18,16 +18,16 @@ import SwiftUI
struct VoiceMessageRoomPlaybackView: View {
@ObservedObject var playerState: AudioPlayerState
@ScaledMetric private var waveformLineWidth = 2.0
@ScaledMetric private var waveformLinePadding = 2.0
let onPlayPause: () -> Void
let onSeek: (Double) -> Void
let onScrubbing: (Bool) -> Void
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
@State private var sendFeedback = false
@ScaledMetric private var waveformLineWidth = 2.0
@ScaledMetric private var waveformLinePadding = 2.0
private let waveformMaxWidth: CGFloat = 150
@ScaledMetric private var playPauseButtonSize = 32
@ScaledMetric private var playPauseImagePadding = 8
@ -67,6 +67,7 @@ struct VoiceMessageRoomPlaybackView: View {
HStack {
playPauseButton
Text(timeLabelContent)
.lineLimit(1)
.font(.compound.bodySMSemibold)
.foregroundColor(.compound.textSecondary)
.monospacedDigit()
@ -99,7 +100,6 @@ struct VoiceMessageRoomPlaybackView: View {
}
})
}
.frame(maxWidth: waveformMaxWidth)
}
.onChange(of: dragState) { newDragState in
switch newDragState {
@ -139,6 +139,7 @@ struct VoiceMessageRoomPlaybackView: View {
.offset(x: playerState.playbackState == .playing ? 0 : 2)
.aspectRatio(contentMode: .fit)
.foregroundColor(.compound.iconSecondary)
.accessibilityLabel(playerState.playbackState == .playing ? L10n.a11yPause : L10n.a11yPlay)
}
}
}

View File

@ -63,7 +63,7 @@ class VoiceMessageMediaManager: VoiceMessageMediaManagerProtocol {
if let fileURL = voiceMessageCache.fileURL(for: source) {
return fileURL
}
// Otherwise, load the file from source
guard case .success(let fileHandle) = await mediaProvider.loadFileFromSource(source, body: body) else {
throw MediaProviderError.failedRetrievingFile

View File

@ -0,0 +1,100 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
@testable import ElementX
import Foundation
import XCTest
@MainActor
class AudioRecorderStateTests: XCTestCase {
private var audioRecorderState: AudioRecorderState!
private var audioRecorderMock: AudioRecorderMock!
private var audioRecorderActionsSubject: PassthroughSubject<AudioRecorderAction, Never>!
private var audioRecorderActions: AnyPublisher<AudioRecorderAction, Never> {
audioRecorderActionsSubject.eraseToAnyPublisher()
}
private func buildAudioRecorderMock() -> AudioRecorderMock {
let audioRecorderMock = AudioRecorderMock()
audioRecorderMock.underlyingActions = audioRecorderActions
audioRecorderMock.currentTime = 0.0
return audioRecorderMock
}
override func setUp() async throws {
audioRecorderActionsSubject = .init()
audioRecorderState = AudioRecorderState()
audioRecorderMock = buildAudioRecorderMock()
}
func testAttach() async throws {
audioRecorderState.attachAudioRecorder(audioRecorderMock)
XCTAssertEqual(audioRecorderState.recordingState, .stopped)
}
func testDetach() async throws {
audioRecorderState.attachAudioRecorder(audioRecorderMock)
audioRecorderState.detachAudioRecorder()
XCTAssert(audioRecorderMock.stopRecordingCalled)
XCTAssertEqual(audioRecorderState.recordingState, .stopped)
}
func testReportError() async throws {
XCTAssertEqual(audioRecorderState.recordingState, .stopped)
audioRecorderState.reportError(AudioRecorderError.genericError)
XCTAssertEqual(audioRecorderState.recordingState, .error)
}
func testHandlingAudioRecorderActionDidStartRecording() async throws {
audioRecorderState.attachAudioRecorder(audioRecorderMock)
let deferred = deferFulfillment(audioRecorderState.$recordingState) { action in
switch action {
case .recording:
return true
default:
return false
}
}
audioRecorderActionsSubject.send(.didStartRecording)
try await deferred.fulfill()
XCTAssertEqual(audioRecorderState.recordingState, .recording)
}
func testHandlingAudioPlayerActionDidStopRecording() async throws {
audioRecorderState.attachAudioRecorder(audioRecorderMock)
let deferred = deferFulfillment(audioRecorderState.$recordingState) { action in
switch action {
case .stopped:
return true
default:
return false
}
}
audioRecorderActionsSubject.send(.didStopRecording)
try await deferred.fulfill()
// The state is expected to be .readyToPlay
XCTAssertEqual(audioRecorderState.recordingState, .stopped)
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.