Media loading flow changes (#483)
* Use an imageProvider directly from the view in the home screen * Add support for media request coalescing * Rename MediaProxy to MediaLoader * Add new image loading mechanism to the room details screen avatar. * Use the `SettingsScreen` prefix for all settings screen related components * Add new image loading mechanism to the room header * Add new image loading mechanism to the room member details screen * Introduce a LoadableImage SwiftUI view that will automatically handle image loading * Adopt the new LoadableImage where possible * Fix LoadableImage not using/storing loaded images properly * Simplify media loader enqueueing * Made LodableImage load content after mediaSource updates. Adopt it on the home and settings screens * Introduce a LoadableAvatarImage component and reuse it throughout the app * Small logging tweaks, made some LoadableImage properties private * Fix redacted skeletons avatar background color * Fix placeholder avatars changing when backgrounding the app * PR comments. - Trim the @ sign off of mxid placeholders. - Only expose AvatarSize on the avatar image, use CGSize elsewhere. Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
@@ -74,12 +74,15 @@
|
||||
1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; };
|
||||
1F04C63D4FA95948E3F52147 /* FileRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */; };
|
||||
1F3232BD368DF430AB433907 /* DesignKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5A56C4F47C368EBE5C5E870 /* DesignKit */; };
|
||||
1FD56B9EA4CA804120A2E743 /* ImageProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D28E3A5AE9934EE4FB4720 /* ImageProviderProtocol.swift */; };
|
||||
1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */; };
|
||||
1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEBCB9676FCD1D0F13188DD /* StringTests.swift */; };
|
||||
206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; };
|
||||
20840B4549FFF1301D0A5FF2 /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A46F50F2A46103ADF143ADB /* MediaLoader.swift */; };
|
||||
214C6B416609E58CCBF6DCEE /* SoftLogoutModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC77FC5C4F2000133047AA27 /* SoftLogoutModels.swift */; };
|
||||
214CDBF0C783155242FFE4A0 /* NotificationItemProxy+NSE.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B1FBF8CA40199B8058B1F08 /* NotificationItemProxy+NSE.swift */; };
|
||||
2276870A19F34B3FFFDA690F /* SoftLogoutCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AEA20A6B4883E60469ACF8F /* SoftLogoutCoordinator.swift */; };
|
||||
233221E32DA045018D3D3050 /* SettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF540C393C7DDEE9C902DFF /* SettingsScreenCoordinator.swift */; };
|
||||
2352C541AF857241489756FF /* MockRoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */; };
|
||||
237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; };
|
||||
23B2CD5A06B16055BDDD0994 /* ApplicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D8C8431416EB8DFEC7E235 /* ApplicationTests.swift */; };
|
||||
@@ -100,7 +103,6 @@
|
||||
2BA59D0AEFB4B82A2EC2A326 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; };
|
||||
2BAA5B222856068158D0B3C6 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */; };
|
||||
2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */; };
|
||||
2CA8AD07773A38BA4662098B /* MediaProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */; };
|
||||
2CB6787E25B11711518E9588 /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6281B199D8A8F0892490C2E /* OnboardingCoordinator.swift */; };
|
||||
2D794361CFE790C8FB3C9C0F /* message.caf in Resources */ = {isa = PBXBuildFile; fileRef = ED482057AE39D5C6D9C5F3D8 /* message.caf */; };
|
||||
2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */; };
|
||||
@@ -109,12 +111,10 @@
|
||||
2F94054F50E312AF30BE07F3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; };
|
||||
308BD9343B95657FAA583FB7 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = AD2AC190E55B2BD4D0F1D4A7 /* SwiftyBeaver */; };
|
||||
3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */; };
|
||||
323F36D880363C473B81A9EA /* MediaProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */; };
|
||||
3274219F7F26A5C6C2C55630 /* FilePreviewViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F652E88106B855A2A55ADE /* FilePreviewViewModelProtocol.swift */; };
|
||||
33CAC1226DFB8B5D8447D286 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; };
|
||||
33D630461FC4562CC767EE9F /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B0B1226DA8DB55918B34CD /* FileCache.swift */; };
|
||||
340D39DB87F3800D53A6A621 /* EmojiPickerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */; };
|
||||
34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */; };
|
||||
34C752A73717C691582DC6C7 /* UnsupportedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1B8500C152BC59445647DA8 /* UnsupportedRoomTimelineItem.swift */; };
|
||||
352C439BE0F75E101EF11FB1 /* RoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */; };
|
||||
3588F34D05B4D731A73214C6 /* BugReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DED59F9EFF273BFA2055FFDF /* BugReportScreen.swift */; };
|
||||
@@ -129,7 +129,6 @@
|
||||
39929D29B265C3F6606047DE /* AlignedScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */; };
|
||||
3A08584ECDD4A4541DBF21F8 /* EmojiLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */; };
|
||||
3A64A93A651A3CB8774ADE8E /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 21C83087604B154AA30E9A8F /* SnapshotTesting */; };
|
||||
3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */; };
|
||||
3C549A0BF39F8A854D45D9FD /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; };
|
||||
3C73442084BF8A6939F0F80B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */; };
|
||||
3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DC38E64A5ED3FDB201029A /* BugReportService.swift */; };
|
||||
@@ -142,18 +141,16 @@
|
||||
41DFDD212D1BE57CA50D783B /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = FD43A50D9B75C9D6D30F006B /* SwiftyBeaver */; };
|
||||
4219391CD2351E410554B3E8 /* AggregratedReaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */; };
|
||||
42F1C8731166633E35A6D7E6 /* RoomEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0A307A44F952CD73E63AE31 /* RoomEventStringBuilder.swift */; };
|
||||
438FB9BC535BC95948AA5F34 /* SettingsViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */; };
|
||||
43BD17BC8794BB9B04F2A26B /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179423E34EE846E048E49CBF /* MediaSourceProxy.swift */; };
|
||||
43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */; };
|
||||
440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */; };
|
||||
447E8580A0A2569E32529E17 /* MockRoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */; };
|
||||
44AE0752E001D1D10605CD88 /* Swipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FDA5344F7C4C6E4E863E13 /* Swipe.swift */; };
|
||||
46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */; };
|
||||
490E606044B18985055FF690 /* SettingsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */; };
|
||||
492274DA6691EE985C2FCCAA /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; };
|
||||
49E9B99CB6A275C7744351F0 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D58333B377888012740101 /* LoginViewModel.swift */; };
|
||||
49F2E7DD8CAACE09CEECE3E6 /* SeparatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */; };
|
||||
4A2E0DBB63919AC8309B6D40 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */; };
|
||||
4B978C09567387EF4366BD7A /* MediaLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EF1AC723C2609C7705569CA /* MediaLoaderTests.swift */; };
|
||||
4BB282209EA82015D0DF8F89 /* NavigationStackCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */; };
|
||||
4C3365818DE1CEAEDF590FD3 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; };
|
||||
4C5A638DAA8AF64565BA4866 /* EncryptedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5351EBD7A0B9610548E4B7B2 /* EncryptedRoomTimelineItem.swift */; };
|
||||
@@ -163,6 +160,7 @@
|
||||
4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */; };
|
||||
500CB65ED116B81DA52FDAEE /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874A1842477895F199567BD7 /* TimelineView.swift */; };
|
||||
501304F26B52DF7024011B6C /* EmojiMartJSONLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF9E3E6A23180EC05F06460 /* EmojiMartJSONLoaderTests.swift */; };
|
||||
50C59870BEB1F29C60252FD4 /* SettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F72B9701D847C591ABE1A8 /* SettingsScreenViewModelProtocol.swift */; };
|
||||
50C90117FE25390BFBD40173 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; };
|
||||
518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */; };
|
||||
51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */; };
|
||||
@@ -175,6 +173,7 @@
|
||||
56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; };
|
||||
588411C8FD72B2A2DFE5F7DE /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E992D7B8BE54B2AB454613AF /* XCUIElement.swift */; };
|
||||
59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; };
|
||||
5B300BACD8A1B252AC95FA34 /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A3DF4FAB9FBEF782DF08F3A /* MediaLoaderProtocol.swift */; };
|
||||
5B8B51CEC4717AF487794685 /* NotificationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B490675B8E31423AF116BDA /* NotificationServiceProxy.swift */; };
|
||||
5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; };
|
||||
5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */; };
|
||||
@@ -215,7 +214,9 @@
|
||||
6C9F6C7F2B35288C4230EF3F /* FilePreviewModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55EA4B03F92F31EAA83B3F7B /* FilePreviewModels.swift */; };
|
||||
6CA81428F0970785CDCC5E86 /* UserNotificationToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F31A4E5941ACBA4BB9FEF94C /* UserNotificationToastView.swift */; };
|
||||
6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; };
|
||||
6E47D126DD7585E8F8237CE7 /* LoadableAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */; };
|
||||
6E6E0AAF6C44C0B117EBBE5A /* SlidingSyncViewProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41F3B445BD6EF1C751806B22 /* SlidingSyncViewProxy.swift */; };
|
||||
6EC7A40A537CFB3D526A111C /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47EBB5D698CE9A25BB553A2D /* Strings.swift */; };
|
||||
6F2AB43A1EFAD8A97AF41A15 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = A7CA6F33C553805035C3B114 /* DeviceKit */; };
|
||||
6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */; };
|
||||
6FF51EB400DBA0668FC38B97 /* TimelineStartRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */; };
|
||||
@@ -224,8 +225,10 @@
|
||||
70558528EF68CAAEF09972D5 /* RoomTimelineItemFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96ED747FF90332EA1333C22 /* RoomTimelineItemFixtures.swift */; };
|
||||
706289B086B0A6B0C211763F /* UITestsSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */; };
|
||||
706F79A39BDB32F592B8C2C7 /* UIKitBackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */; };
|
||||
709B1C6026A3056662FF93EE /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A3DF4FAB9FBEF782DF08F3A /* MediaLoaderProtocol.swift */; };
|
||||
719E7AAD1F8E68F68F30FECD /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40C19719687984FD9478FBE /* Task.swift */; };
|
||||
71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; };
|
||||
7313970ED46B213AF4CFB4B3 /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A46F50F2A46103ADF143ADB /* MediaLoader.swift */; };
|
||||
7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; };
|
||||
7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */; };
|
||||
7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; };
|
||||
@@ -249,7 +252,6 @@
|
||||
7BB31E67648CF32D2AB5E502 /* RoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */; };
|
||||
7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */ = {isa = PBXBuildFile; fileRef = C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */; };
|
||||
7CD16990BA843BE9ED639129 /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */; };
|
||||
7D1DAAA364A9A29D554BD24E /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */; };
|
||||
7E3B1F8D72573ED2FCB2D94B /* NotificationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD5FEE195446A9E458DDDAF /* NotificationServiceProxyProtocol.swift */; };
|
||||
7E3C34BC10936AD4F77975F4 /* EmojiMartJSONLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39001365B76B89983FDB7AD8 /* EmojiMartJSONLoader.swift */; };
|
||||
7E7DF1867F98B0D10A6C0A63 /* FileCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3648F2FADEF2672D6A0D489 /* FileCacheTests.swift */; };
|
||||
@@ -301,6 +303,7 @@
|
||||
968A5B890004526AB58A217C /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; };
|
||||
96FE26ABD4E5B8B6EF0EF596 /* RoomMemberDetailsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CEFB5144EF8F50C77CF6E14 /* RoomMemberDetailsCoordinator.swift */; };
|
||||
97189E495F0E47805D1868DB /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 527578916BD388A09F5A8036 /* DTCoreText */; };
|
||||
973B5350B5C72E3EB1E62E67 /* ImageProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D28E3A5AE9934EE4FB4720 /* ImageProviderProtocol.swift */; };
|
||||
978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; };
|
||||
981853650217B6C8ECDD998C /* NavigationRootCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */; };
|
||||
983896D611ABF52A5C37498D /* RoomSummaryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */; };
|
||||
@@ -315,8 +318,10 @@
|
||||
9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */; };
|
||||
9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A20AE75FF4FF35B1FF6CA7 /* MockServerSelectionScreenState.swift */; };
|
||||
9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */; };
|
||||
9C5A07E7C33F3F40287D7861 /* SettingsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */; };
|
||||
9CCC77C31CB399661A034739 /* UserProperties+Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A6C4BE591FE5C38CE9C7EF3 /* UserProperties+Element.swift */; };
|
||||
9D2E03DB175A6AB14589076D /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = AA4E1BEB4E9BC2467006E12B /* AppAuth */; };
|
||||
9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; };
|
||||
9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; };
|
||||
9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */; };
|
||||
9E8AE387FD03E4F1C1B8815A /* SessionVerificationStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06FCD42EEFEFC220F14EAC5 /* SessionVerificationStateMachine.swift */; };
|
||||
@@ -339,11 +344,13 @@
|
||||
A7FD7B992E6EE6E5A8429197 /* RoomSummaryDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */; };
|
||||
A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; };
|
||||
A8771F5975A82759FA5138AE /* RoomMemberDetailsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F19DBE940499D3E3DD405D8 /* RoomMemberDetailsScreenUITests.swift */; };
|
||||
A896998A6784DB6F16E912F4 /* MockMediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */; };
|
||||
A8EC7C9D886244DAE9433E37 /* SessionVerificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */; };
|
||||
A9D23B78F42BCDD896531436 /* UserNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649759084B0C9FE1F8DF8D17 /* UserNotificationPresenter.swift */; };
|
||||
AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */; };
|
||||
AAF0BBED840DF4A53EE85E77 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C2C69B8BA5A9702E7A8BC08F /* MatrixRustSDK */; };
|
||||
AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9760103CF316DF68698BCFE6 /* LaunchScreen.storyboard */; };
|
||||
ABDC81BD1C3C8B62665F2C72 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */; };
|
||||
ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; };
|
||||
AC5CC8250CEAE57B73900C57 /* UserNotificationModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD80F22830C2360F3F39DDCE /* UserNotificationModalView.swift */; };
|
||||
AC69B6DF15FC451AB2945036 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */; };
|
||||
@@ -427,6 +434,8 @@
|
||||
D85D4FA590305180B4A41795 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3073CCD77D906B330BC1D6 /* Tests.swift */; };
|
||||
D876EC0FED3B6D46C806912A /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; };
|
||||
D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */; };
|
||||
D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352359663A0E52BA20761EE /* LoadableImage.swift */; };
|
||||
DBAA69CC2CE4D44BC8E20105 /* SettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 548E7D356609ACD33AE7643E /* SettingsScreenModels.swift */; };
|
||||
DC68E866D6E664B0D2B06E74 /* MockImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */; };
|
||||
DD9B70DE54B24E0694A35D8A /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; };
|
||||
DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */; };
|
||||
@@ -449,7 +458,6 @@
|
||||
E89536FC8C0E4B79E9842A78 /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0197EAE9D45A662B8847B6 /* RoomTimelineControllerProtocol.swift */; };
|
||||
E8AB8D16E6D8E8E501F29BD9 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B0B1226DA8DB55918B34CD /* FileCache.swift */; };
|
||||
E96005321849DBD7C72A28F2 /* UITestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */; };
|
||||
E9631F628251F77A24AA4BB4 /* MockMediaProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36CFF3E430B27B7C3AD0A28 /* MockMediaProxy.swift */; };
|
||||
EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885D8C42DD17625B5261BEFF /* MediaProvider.swift */; };
|
||||
EA31DD9043B91ECB8E45A9A6 /* ScreenshotDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */; };
|
||||
EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; };
|
||||
@@ -457,16 +465,19 @@
|
||||
EBE13FAB4E29738AC41BD3E5 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; };
|
||||
EC280623A42904341363EAAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 886A0A498FA01E8EDD451D05 /* Sentry */; };
|
||||
EC4C31963E755EEC77BD778C /* AnalyticsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */; };
|
||||
ECA636DAF071C611FDC2BB57 /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; };
|
||||
EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F40F48279322E504153AB0D /* MockClientProxy.swift */; };
|
||||
EE6933C935080B4E0348A58B /* EmojiMartCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5C3AACCAA82392D08924496 /* EmojiMartCategory.swift */; };
|
||||
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; };
|
||||
EE8A37E2A1A77DE5CF941632 /* StateRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */; };
|
||||
EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; };
|
||||
EEC499F9AC7DD6D18760F81D /* SettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E3CEDE000EFF6E988BEFDE /* SettingsScreenViewModel.swift */; };
|
||||
EF7924005216B8189898F370 /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */; };
|
||||
F040ABFEB0A2B142D948BA12 /* Untranslated.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = F75DF9500D69A3AAF8339E69 /* Untranslated.stringsdict */; };
|
||||
F06CE9132855E81EBB6DDC32 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 800631D7250B7F93195035F1 /* KeychainAccess */; };
|
||||
F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */; };
|
||||
F0F82C3C848C865C3098AA52 /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 67E7A6F388D3BF85767609D9 /* Sentry */; };
|
||||
F253AAB4C8F06208173C9C4A /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; };
|
||||
F257F964493A9CD02A6F720C /* OnboardingPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2717AB91060260E5F4781 /* OnboardingPageView.swift */; };
|
||||
F32B271F60531BE92C6E62A1 /* StickerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612EF972F2A1800682D32C5E /* StickerRoomTimelineView.swift */; };
|
||||
F425C3F85BFF28C9AC593F52 /* MockNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96561CC53F7C1E24D4C292E4 /* MockNotificationManager.swift */; };
|
||||
@@ -475,12 +486,10 @@
|
||||
F656F92A63D3DC1978D79427 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; };
|
||||
F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; };
|
||||
F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; };
|
||||
F9981191DC408AED537C1749 /* MediaProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C9E0B61A77C7F0EE7918C /* MediaProxy.swift */; };
|
||||
F99FB21EFC6D99D247FE7CBE /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DE8DC9B3FBA402117DC4C49F /* Kingfisher */; };
|
||||
F9F6D2883BBEBB9A3789A137 /* OnboardingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */; };
|
||||
FA2BBAE9FC5E2E9F960C0980 /* NavigationCoordinators.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */; };
|
||||
FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; };
|
||||
FBCD77D557AACBE9B445133A /* MediaProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C9E0B61A77C7F0EE7918C /* MediaProxy.swift */; };
|
||||
FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */; };
|
||||
FC10228E73323BDC09526F97 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 9C73F37731C9FDED1BB24C1C /* Collections */; };
|
||||
FCD3F2B82CAB29A07887A127 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2B43F2AF7456567FE37270A7 /* KeychainAccess */; };
|
||||
@@ -549,11 +558,9 @@
|
||||
086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = "<group>"; };
|
||||
08F64963396A6A23538EFCEC /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = is; path = is.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
09199C43BAB209C0BD89A836 /* OnboardingPageIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPageIndicator.swift; sourceTree = "<group>"; };
|
||||
0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = "<group>"; };
|
||||
095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProviderProtocol.swift; sourceTree = "<group>"; };
|
||||
0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToggleStyle.swift; sourceTree = "<group>"; };
|
||||
099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = "<group>"; };
|
||||
0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
||||
0AB7A0C06CB527A1095DEB33 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = da; path = da.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
0B490675B8E31423AF116BDA /* NotificationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceProxy.swift; sourceTree = "<group>"; };
|
||||
0B869438A1B52836F912A702 /* MockSoftLogoutScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSoftLogoutScreenState.swift; sourceTree = "<group>"; };
|
||||
@@ -624,6 +631,7 @@
|
||||
287FC98AF2664EAD79C0D902 /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = "<group>"; };
|
||||
28959C7DB36C7688A01D4045 /* BugReportViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
28EA8BE9EEDBD17555141C7E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
2A46F50F2A46103ADF143ADB /* MediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoader.swift; sourceTree = "<group>"; };
|
||||
2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = "<group>"; };
|
||||
2AE83A3DD63BCFBB956FE5CB /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
2AEA20A6B4883E60469ACF8F /* SoftLogoutCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutCoordinator.swift; sourceTree = "<group>"; };
|
||||
@@ -669,10 +677,10 @@
|
||||
3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = "<group>"; };
|
||||
3D4DD336905C72F95EAF34B7 /* ElementX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ElementX-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = "<group>"; };
|
||||
3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = "<group>"; };
|
||||
3DD6E7C1D8B53F47789778CD /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBackgroundTaskService.swift; sourceTree = "<group>"; };
|
||||
3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
3EF1AC723C2609C7705569CA /* MediaLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderTests.swift; sourceTree = "<group>"; };
|
||||
3F40F48279322E504153AB0D /* MockClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockClientProxy.swift; sourceTree = "<group>"; };
|
||||
3F9E67AAB66638C69626866C /* UserSessionFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||
3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineViewFactory.swift; sourceTree = "<group>"; };
|
||||
@@ -699,10 +707,10 @@
|
||||
48CE6BF18E542B32FA52CE06 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
49193CB0C248D621A96FB2AA /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
4959CECEC984B3995616F427 /* DataProtectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = "<group>"; };
|
||||
4990FDBDA96B88E214F92F48 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = "<group>"; };
|
||||
49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = "<group>"; };
|
||||
4A57A4AFA6A068668AFBD070 /* UIActivityViewControllerWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIActivityViewControllerWrapper.swift; sourceTree = "<group>"; };
|
||||
4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaLoader.swift; sourceTree = "<group>"; };
|
||||
4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettings.swift; sourceTree = "<group>"; };
|
||||
4B40B7F6FCCE2D8C242492D9 /* ga */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ga; path = ga.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = "<group>"; };
|
||||
@@ -716,6 +724,7 @@
|
||||
4F5F0662483ED69791D63B16 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = et; path = et.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
50E3CEDE000EFF6E988BEFDE /* SettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
51DF91C374901E94D93276F1 /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-MX"; path = "es-MX.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreen.swift; sourceTree = "<group>"; };
|
||||
529513218340CC8419273165 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -728,6 +737,7 @@
|
||||
541542F5AC323709D8563458 /* AnalyticsPrompt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPrompt.swift; sourceTree = "<group>"; };
|
||||
542D4F49FABA056DEEEB3400 /* RustTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RustTracing.swift; sourceTree = "<group>"; };
|
||||
5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
|
||||
548E7D356609ACD33AE7643E /* SettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenModels.swift; sourceTree = "<group>"; };
|
||||
55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemSender.swift; sourceTree = "<group>"; };
|
||||
55BC11560C8A2598964FFA4C /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
55D7187F6B0C0A651AC3DFFA /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = in; path = in.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -738,7 +748,6 @@
|
||||
5773C86AF04AEF26515AD00C /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
57CF0E6DD78FB3F6CBF5AC38 /* RoomMemberDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModel.swift; sourceTree = "<group>"; };
|
||||
57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLocator.swift; sourceTree = "<group>"; };
|
||||
5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = "<group>"; };
|
||||
5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinatorUITests.swift; sourceTree = "<group>"; };
|
||||
5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProperties.swift; sourceTree = "<group>"; };
|
||||
@@ -774,6 +783,7 @@
|
||||
6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModel.swift; sourceTree = "<group>"; };
|
||||
6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermalinkBuilderTests.swift; sourceTree = "<group>"; };
|
||||
6FC5015B9634698BDB8701AF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
6FF540C393C7DDEE9C902DFF /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
71556206CD5E8B1F53F07178 /* MockRoomTimelineControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineControllerFactory.swift; sourceTree = "<group>"; };
|
||||
71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenModels.swift; sourceTree = "<group>"; };
|
||||
71D52BAA5BADB06E5E8C295D /* Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = "<group>"; };
|
||||
@@ -814,6 +824,7 @@
|
||||
8872E9C5E91E9F2BFC4EBCCA /* AlignedScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignedScrollView.swift; sourceTree = "<group>"; };
|
||||
8888D13645C04AC9818F5778 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
892E29C98C4E8182C9037F84 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = "<group>"; };
|
||||
8A3DF4FAB9FBEF782DF08F3A /* MediaLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderProtocol.swift; sourceTree = "<group>"; };
|
||||
8A9AE4967817E9608E22EB44 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
8AC1A01C3A745BDF1D3697D3 /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = "<group>"; };
|
||||
8C0AA893D6F8A2F563E01BB9 /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = in; path = in.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -822,6 +833,7 @@
|
||||
8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = "<group>"; };
|
||||
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
|
||||
8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = "<group>"; };
|
||||
8ED2D2F6A137A95EA50413BE /* UserNotificationControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationControllerProtocol.swift; sourceTree = "<group>"; };
|
||||
8F7D42E66E939B709C1EC390 /* MockRoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomSummaryProvider.swift; sourceTree = "<group>"; };
|
||||
8FC26871038FB0E4AAE22605 /* apple_emojis_data.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = apple_emojis_data.json; sourceTree = "<group>"; };
|
||||
@@ -918,6 +930,7 @@
|
||||
B4C18FAAD59AE7F1462D817E /* SessionVerificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModel.swift; sourceTree = "<group>"; };
|
||||
B4DE1CF8F5EFD353B1A5E36F /* AnalyticsPromptCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptCoordinator.swift; sourceTree = "<group>"; };
|
||||
B516212D9FE785DDD5E490D1 /* BugReportModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportModels.swift; sourceTree = "<group>"; };
|
||||
B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = "<group>"; };
|
||||
B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
|
||||
@@ -944,6 +957,7 @@
|
||||
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = "<group>"; };
|
||||
C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = "<group>"; };
|
||||
C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = "<group>"; };
|
||||
C352359663A0E52BA20761EE /* LoadableImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableImage.swift; sourceTree = "<group>"; };
|
||||
C38AE3617D7619EF30CDD229 /* EmojiMartStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartStore.swift; sourceTree = "<group>"; };
|
||||
C3F652E88106B855A2A55ADE /* FilePreviewViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
C483956FA3D665E3842E319A /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = "<group>"; };
|
||||
@@ -951,6 +965,7 @@
|
||||
C687844F60BFF532D49A994C /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = "<group>"; };
|
||||
C6A9F49B3EE59147AF2F70BB /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportUITests.swift; sourceTree = "<group>"; };
|
||||
C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = "<group>"; };
|
||||
C75EF87651B00A176AB08E97 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
C789E7BFC066CF39B8AE0974 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; };
|
||||
C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = "<group>"; };
|
||||
@@ -959,6 +974,7 @@
|
||||
C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
C9A86C95340248A8B7BA9A43 /* AnalyticsPromptViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
C9F72B9701D847C591ABE1A8 /* SettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
CA28F29C9F93E93CC3C2C715 /* NavigationRootCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRootCoordinator.swift; sourceTree = "<group>"; };
|
||||
CA78F8D91974DFFEDC05485A /* RoomMemberDetailsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsModels.swift; sourceTree = "<group>"; };
|
||||
CA89A2DD51B6BBE1DA55E263 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||
@@ -985,6 +1001,7 @@
|
||||
D3D455BC2423D911A62ACFB2 /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = "<group>"; };
|
||||
D4DA544B2520BFA65D6DB4BB /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
|
||||
D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = "<group>"; };
|
||||
D5D28E3A5AE9934EE4FB4720 /* ImageProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProviderProtocol.swift; sourceTree = "<group>"; };
|
||||
D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
D67CBAFA48ED0B6FCE74F88F /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = "<group>"; };
|
||||
@@ -1006,15 +1023,12 @@
|
||||
DF38B69D2C331A499276F400 /* FilePreviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewViewModelTests.swift; sourceTree = "<group>"; };
|
||||
DF3D25B3EDB283B5807EADCF /* ReadMarkerRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = "<group>"; };
|
||||
E12C9E0B61A77C7F0EE7918C /* MediaProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProxy.swift; sourceTree = "<group>"; };
|
||||
E157152B11E347F735C3FD6E /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
E18CF12478983A5EB390FB26 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = "<group>"; };
|
||||
E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = "<group>"; };
|
||||
E26747B3154A5DBC3A7E24A5 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = "<group>"; };
|
||||
E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = "<group>"; };
|
||||
E36CFF3E430B27B7C3AD0A28 /* MockMediaProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProxy.swift; sourceTree = "<group>"; };
|
||||
E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = "<group>"; };
|
||||
E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUITests.swift; sourceTree = "<group>"; };
|
||||
E45C57120F28F8D619150219 /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E4BB9A17AC512A7EF4B106E5 /* SessionVerificationModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationModels.swift; sourceTree = "<group>"; };
|
||||
E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
@@ -1070,7 +1084,6 @@
|
||||
F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
FAB10E673916D2B8D21FD197 /* TemplateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModels.swift; sourceTree = "<group>"; };
|
||||
FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = "<group>"; };
|
||||
FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProxyProtocol.swift; sourceTree = "<group>"; };
|
||||
FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerTests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -1307,6 +1320,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */,
|
||||
B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */,
|
||||
C352359663A0E52BA20761EE /* LoadableImage.swift */,
|
||||
C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */,
|
||||
839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */,
|
||||
);
|
||||
path = Views;
|
||||
@@ -1657,10 +1673,10 @@
|
||||
70B74A432C241E56A7ACE610 /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3DD2D50A7EAA4FC78417730E /* SettingsCoordinator.swift */,
|
||||
4990FDBDA96B88E214F92F48 /* SettingsModels.swift */,
|
||||
0A191D3FDB995309C7E2DE7D /* SettingsViewModel.swift */,
|
||||
5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */,
|
||||
6FF540C393C7DDEE9C902DFF /* SettingsScreenCoordinator.swift */,
|
||||
548E7D356609ACD33AE7643E /* SettingsScreenModels.swift */,
|
||||
50E3CEDE000EFF6E988BEFDE /* SettingsScreenViewModel.swift */,
|
||||
C9F72B9701D847C591ABE1A8 /* SettingsScreenViewModelProtocol.swift */,
|
||||
4541090DFE1A5499BD67BD14 /* View */,
|
||||
);
|
||||
path = Settings;
|
||||
@@ -1735,11 +1751,12 @@
|
||||
7583EAC171059A86B767209F /* MediaProvider */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3EF1AC723C2609C7705569CA /* MediaLoaderTests.swift */,
|
||||
62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */,
|
||||
3948D16F021DFDB2CD26EAA8 /* MockBackgroundTaskService.swift */,
|
||||
AEC96B3DC55090BBF8876CC2 /* MockFileCache.swift */,
|
||||
AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */,
|
||||
E36CFF3E430B27B7C3AD0A28 /* MockMediaProxy.swift */,
|
||||
4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */,
|
||||
);
|
||||
path = MediaProvider;
|
||||
sourceTree = "<group>";
|
||||
@@ -1808,10 +1825,11 @@
|
||||
79E560F5113ED25D172E550C /* Media */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D5D28E3A5AE9934EE4FB4720 /* ImageProviderProtocol.swift */,
|
||||
2A46F50F2A46103ADF143ADB /* MediaLoader.swift */,
|
||||
8A3DF4FAB9FBEF782DF08F3A /* MediaLoaderProtocol.swift */,
|
||||
885D8C42DD17625B5261BEFF /* MediaProvider.swift */,
|
||||
C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */,
|
||||
E12C9E0B61A77C7F0EE7918C /* MediaProxy.swift */,
|
||||
FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */,
|
||||
179423E34EE846E048E49CBF /* MediaSourceProxy.swift */,
|
||||
6920A4869821BF72FFC58842 /* MockMediaProvider.swift */,
|
||||
);
|
||||
@@ -1991,7 +2009,7 @@
|
||||
086B997409328F091EBA43CE /* RoomScreenUITests.swift */,
|
||||
054F469E433864CC6FE6EE8E /* ServerSelectionUITests.swift */,
|
||||
6D4777F0142E330A75C46FE4 /* SessionVerificationUITests.swift */,
|
||||
E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */,
|
||||
8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */,
|
||||
55F30E764BED111C81739844 /* SoftLogoutUITests.swift */,
|
||||
F899D02CF26EA7675EEBE74C /* UserSessionScreenTests.swift */,
|
||||
);
|
||||
@@ -2183,7 +2201,6 @@
|
||||
D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */,
|
||||
B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */,
|
||||
42EEA67A6796BDC2761619C5 /* PaginationIndicatorRoomTimelineView.swift */,
|
||||
0950733DD4BA83EEE752E259 /* PlaceholderAvatarImage.swift */,
|
||||
B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */,
|
||||
C8F2A7A4E3F5060F52ACFFB0 /* RedactedRoomTimelineView.swift */,
|
||||
6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */,
|
||||
@@ -2875,6 +2892,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F253AAB4C8F06208173C9C4A /* Assets.swift in Sources */,
|
||||
968A5B890004526AB58A217C /* AvatarSize.swift in Sources */,
|
||||
EF7924005216B8189898F370 /* BackgroundTaskProtocol.swift in Sources */,
|
||||
1B4B3E847BF944DB2C1C217F /* BackgroundTaskServiceProtocol.swift in Sources */,
|
||||
@@ -2884,16 +2902,18 @@
|
||||
E8AB8D16E6D8E8E501F29BD9 /* FileCache.swift in Sources */,
|
||||
A33784831AD880A670CAA9F9 /* FileManager.swift in Sources */,
|
||||
59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */,
|
||||
1FD56B9EA4CA804120A2E743 /* ImageProviderProtocol.swift in Sources */,
|
||||
EBE13FAB4E29738AC41BD3E5 /* InfoPlistReader.swift in Sources */,
|
||||
8691186F9B99BCDDB7CACDD8 /* KeychainController.swift in Sources */,
|
||||
A440D4BC02088482EC633A88 /* KeychainControllerProtocol.swift in Sources */,
|
||||
AD2A81B65A9F6163012086F1 /* MXLog.swift in Sources */,
|
||||
8C454500B8073E1201F801A9 /* MXLogger.swift in Sources */,
|
||||
20840B4549FFF1301D0A5FF2 /* MediaLoader.swift in Sources */,
|
||||
5B300BACD8A1B252AC95FA34 /* MediaLoaderProtocol.swift in Sources */,
|
||||
0F3F2FDD4021A25A0D57F801 /* MediaProvider.swift in Sources */,
|
||||
4C3365818DE1CEAEDF590FD3 /* MediaProviderProtocol.swift in Sources */,
|
||||
F9981191DC408AED537C1749 /* MediaProxy.swift in Sources */,
|
||||
2CA8AD07773A38BA4662098B /* MediaProxyProtocol.swift in Sources */,
|
||||
5E25568E1CDAD983517E58B5 /* MediaSourceProxy.swift in Sources */,
|
||||
ABDC81BD1C3C8B62665F2C72 /* MockMediaProvider.swift in Sources */,
|
||||
5455147CAC63F71E48F7D699 /* NSELogger.swift in Sources */,
|
||||
5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */,
|
||||
214CDBF0C783155242FFE4A0 /* NotificationItemProxy+NSE.swift in Sources */,
|
||||
@@ -2903,6 +2923,8 @@
|
||||
7E3B1F8D72573ED2FCB2D94B /* NotificationServiceProxyProtocol.swift in Sources */,
|
||||
414F50CFCFEEE2611127DCFB /* RestorationToken.swift in Sources */,
|
||||
7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */,
|
||||
ECA636DAF071C611FDC2BB57 /* Strings+Untranslated.swift in Sources */,
|
||||
6EC7A40A537CFB3D526A111C /* Strings.swift in Sources */,
|
||||
719E7AAD1F8E68F68F30FECD /* Task.swift in Sources */,
|
||||
BA074E9812F96FFA3200ED1D /* TimelineItemProxy.swift in Sources */,
|
||||
B8C316C6CA24512DFE9A27FD /* TimelineItemSender.swift in Sources */,
|
||||
@@ -2934,12 +2956,13 @@
|
||||
149D1942DC005D0485FB8D93 /* LoggingTests.swift in Sources */,
|
||||
1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */,
|
||||
2E43A3D221BE9587BC19C3F1 /* MatrixEntityRegexTests.swift in Sources */,
|
||||
4B978C09567387EF4366BD7A /* MediaLoaderTests.swift in Sources */,
|
||||
167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */,
|
||||
4E8F17EBA24FBBA6ABB62ECB /* MockBackgroundTaskService.swift in Sources */,
|
||||
1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */,
|
||||
CF6319CC05F964B4D05BF614 /* MockFileCache.swift in Sources */,
|
||||
DC68E866D6E664B0D2B06E74 /* MockImageCache.swift in Sources */,
|
||||
E9631F628251F77A24AA4BB4 /* MockMediaProxy.swift in Sources */,
|
||||
A896998A6784DB6F16E912F4 /* MockMediaLoader.swift in Sources */,
|
||||
981853650217B6C8ECDD998C /* NavigationRootCoordinatorTests.swift in Sources */,
|
||||
69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */,
|
||||
4BB282209EA82015D0DF8F89 /* NavigationStackCoordinatorTests.swift in Sources */,
|
||||
@@ -3065,6 +3088,7 @@
|
||||
56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */,
|
||||
03D684A3AE85A23B3DA3B43F /* Image.swift in Sources */,
|
||||
BA31448FBD9697F8CB9A83CD /* ImageCache.swift in Sources */,
|
||||
973B5350B5C72E3EB1E62E67 /* ImageProviderProtocol.swift in Sources */,
|
||||
7CD16990BA843BE9ED639129 /* ImageRoomTimelineItem.swift in Sources */,
|
||||
D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */,
|
||||
B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */,
|
||||
@@ -3073,6 +3097,8 @@
|
||||
1FE593ECEC40A43789105D80 /* KeychainController.swift in Sources */,
|
||||
CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */,
|
||||
15D867E638BFD0E5E71DB1EF /* List.swift in Sources */,
|
||||
6E47D126DD7585E8F8237CE7 /* LoadableAvatarImage.swift in Sources */,
|
||||
D9F80CE61BF8FF627FDB0543 /* LoadableImage.swift in Sources */,
|
||||
83E5054739949181CA981193 /* LoginCoordinator.swift in Sources */,
|
||||
872A6457DF573AF8CEAE927A /* LoginHomeserver.swift in Sources */,
|
||||
CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */,
|
||||
@@ -3084,10 +3110,10 @@
|
||||
B94368839BDB69172E28E245 /* MXLog.swift in Sources */,
|
||||
B66757D0254843162595B25D /* MXLogger.swift in Sources */,
|
||||
67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */,
|
||||
7313970ED46B213AF4CFB4B3 /* MediaLoader.swift in Sources */,
|
||||
709B1C6026A3056662FF93EE /* MediaLoaderProtocol.swift in Sources */,
|
||||
EA1E7949533E19C6D862680A /* MediaProvider.swift in Sources */,
|
||||
7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */,
|
||||
FBCD77D557AACBE9B445133A /* MediaProxy.swift in Sources */,
|
||||
323F36D880363C473B81A9EA /* MediaProxyProtocol.swift in Sources */,
|
||||
43BD17BC8794BB9B04F2A26B /* MediaSourceProxy.swift in Sources */,
|
||||
24906A1E82D0046655958536 /* MessageComposer.swift in Sources */,
|
||||
072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */,
|
||||
@@ -3132,7 +3158,7 @@
|
||||
7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */,
|
||||
764AFCC225B044CF5F9B41E5 /* PaginationIndicatorRoomTimelineView.swift in Sources */,
|
||||
80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */,
|
||||
7D1DAAA364A9A29D554BD24E /* PlaceholderAvatarImage.swift in Sources */,
|
||||
9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */,
|
||||
DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */,
|
||||
743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */,
|
||||
8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */,
|
||||
@@ -3199,11 +3225,11 @@
|
||||
9E8AE387FD03E4F1C1B8815A /* SessionVerificationStateMachine.swift in Sources */,
|
||||
A8EC7C9D886244DAE9433E37 /* SessionVerificationViewModel.swift in Sources */,
|
||||
D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */,
|
||||
34966D4C1C2C6D37FE3F7F50 /* SettingsCoordinator.swift in Sources */,
|
||||
3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */,
|
||||
7FED310F6AB7A70CBFB7C8A3 /* SettingsScreen.swift in Sources */,
|
||||
4A2E0DBB63919AC8309B6D40 /* SettingsViewModel.swift in Sources */,
|
||||
438FB9BC535BC95948AA5F34 /* SettingsViewModelProtocol.swift in Sources */,
|
||||
233221E32DA045018D3D3050 /* SettingsScreenCoordinator.swift in Sources */,
|
||||
DBAA69CC2CE4D44BC8E20105 /* SettingsScreenModels.swift in Sources */,
|
||||
EEC499F9AC7DD6D18760F81D /* SettingsScreenViewModel.swift in Sources */,
|
||||
50C59870BEB1F29C60252FD4 /* SettingsScreenViewModelProtocol.swift in Sources */,
|
||||
6E6E0AAF6C44C0B117EBBE5A /* SlidingSyncViewProxy.swift in Sources */,
|
||||
2276870A19F34B3FFFDA690F /* SoftLogoutCoordinator.swift in Sources */,
|
||||
214C6B416609E58CCBF6DCEE /* SoftLogoutModels.swift in Sources */,
|
||||
@@ -3294,7 +3320,7 @@
|
||||
2F1CF90A3460C153154427F0 /* RoomScreenUITests.swift in Sources */,
|
||||
77FACC29F98FE2E65BBB6A5F /* ServerSelectionUITests.swift in Sources */,
|
||||
05EC896A4B9AF4A56670C0BB /* SessionVerificationUITests.swift in Sources */,
|
||||
490E606044B18985055FF690 /* SettingsUITests.swift in Sources */,
|
||||
9C5A07E7C33F3F40287D7861 /* SettingsScreenUITests.swift in Sources */,
|
||||
B064D42BA087649ACAE462E8 /* SoftLogoutUITests.swift in Sources */,
|
||||
DD9B70DE54B24E0694A35D8A /* Strings+Untranslated.swift in Sources */,
|
||||
B3357B00F1AA930E54F76609 /* Strings.swift in Sources */,
|
||||
|
||||
@@ -86,8 +86,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/matrix-org/matrix-rust-components-swift",
|
||||
"state" : {
|
||||
"revision" : "2d702d0d52805e4f81924507b39ad81c8a74a63f",
|
||||
"version" : "1.0.33-alpha"
|
||||
"revision" : "a119a8f16bbe3adc34cfdf2e092886d44e01705a",
|
||||
"version" : "1.0.32-alpha"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -25,11 +25,7 @@ class AppCoordinator: AppCoordinatorProtocol {
|
||||
/// Common background task to resume long-running tasks in the background.
|
||||
/// When this task expiring, we'll try to suspend the state machine by `suspend` event.
|
||||
private var backgroundTask: BackgroundTaskProtocol?
|
||||
private var isSuspended = false {
|
||||
didSet {
|
||||
MXLog.info("didSet to: \(isSuspended)")
|
||||
}
|
||||
}
|
||||
private var isSuspended = false
|
||||
|
||||
private var userSession: UserSessionProtocol! {
|
||||
didSet {
|
||||
|
||||
@@ -36,6 +36,10 @@ class ViewModelContext<ViewState: BindableState, ViewAction>: ObservableObject {
|
||||
|
||||
/// Get-able/Observable `Published` property for the `ViewState`
|
||||
@Published fileprivate(set) var viewState: ViewState
|
||||
|
||||
/// An optional image loading service so that views can manage themselves
|
||||
/// Intentionally non-generic so that it doesn't grow uncontrollably
|
||||
let imageProvider: ImageProviderProtocol?
|
||||
|
||||
/// Set-able/Bindable access to the bindable state.
|
||||
subscript<T>(dynamicMember keyPath: WritableKeyPath<ViewState.BindStateType, T>) -> T {
|
||||
@@ -43,9 +47,10 @@ class ViewModelContext<ViewState: BindableState, ViewAction>: ObservableObject {
|
||||
set { viewState.bindings[keyPath: keyPath] = newValue }
|
||||
}
|
||||
|
||||
init(initialViewState: ViewState) {
|
||||
init(initialViewState: ViewState, imageProvider: ImageProviderProtocol?) {
|
||||
self.viewActions = PassthroughSubject()
|
||||
self.viewState = initialViewState
|
||||
self.imageProvider = imageProvider
|
||||
}
|
||||
|
||||
/// Send a `ViewAction` to the `ViewModel` for processing.
|
||||
@@ -78,8 +83,8 @@ class StateStoreViewModel<State: BindableState, ViewAction> {
|
||||
set { context.viewState = newValue }
|
||||
}
|
||||
|
||||
init(initialViewState: State) {
|
||||
context = Context(initialViewState: initialViewState)
|
||||
init(initialViewState: State, imageProvider: ImageProviderProtocol? = nil) {
|
||||
context = Context(initialViewState: initialViewState, imageProvider: imageProvider)
|
||||
context.viewActions
|
||||
.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LoadableAvatarImage: View {
|
||||
private let url: URL?
|
||||
private let name: String?
|
||||
private let contentID: String?
|
||||
private let avatarSize: AvatarSize
|
||||
private let imageProvider: ImageProviderProtocol?
|
||||
|
||||
@ScaledMetric private var frameSize: CGFloat
|
||||
|
||||
init(url: URL?, name: String?, contentID: String?, avatarSize: AvatarSize, imageProvider: ImageProviderProtocol?) {
|
||||
self.url = url
|
||||
self.name = name
|
||||
self.contentID = contentID
|
||||
self.avatarSize = avatarSize
|
||||
self.imageProvider = imageProvider
|
||||
|
||||
_frameSize = ScaledMetric(wrappedValue: avatarSize.value)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
LoadableImage(url: url,
|
||||
size: avatarSize.scaledSize,
|
||||
imageProvider: imageProvider) { image in
|
||||
image
|
||||
.scaledToFill()
|
||||
} placeholder: {
|
||||
PlaceholderAvatarImage(name: name, contentID: contentID)
|
||||
}
|
||||
.frame(width: frameSize, height: frameSize)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
102
ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift
Normal file
102
ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LoadableImage<TransformerView: View, PlaceholderView: View>: View {
|
||||
private let mediaSource: MediaSourceProxy?
|
||||
private let blurhash: String?
|
||||
private let size: CGSize?
|
||||
private let imageProvider: ImageProviderProtocol?
|
||||
|
||||
private var transformer: (Image) -> TransformerView
|
||||
private let placeholder: () -> PlaceholderView
|
||||
|
||||
@State private var cachedImage: UIImage?
|
||||
|
||||
/// A SwiftUI view that automatically fetches images
|
||||
/// It will try fetching the image from in-memory cache and if that's not available
|
||||
/// it will fire a task to load it through the image provider
|
||||
/// - Parameters:
|
||||
/// - mediaSource: the source of the image
|
||||
/// - blurhash: an optional blurhash
|
||||
/// - transformer: entry point for configuring the resulting image view
|
||||
/// - placeholder: a view to show while the image or blurhash are not available
|
||||
init(mediaSource: MediaSourceProxy?,
|
||||
blurhash: String? = nil,
|
||||
size: CGSize? = nil,
|
||||
imageProvider: ImageProviderProtocol?,
|
||||
transformer: @escaping (Image) -> TransformerView = { $0 },
|
||||
placeholder: @escaping () -> PlaceholderView) {
|
||||
self.mediaSource = mediaSource
|
||||
self.blurhash = blurhash
|
||||
self.size = size
|
||||
self.imageProvider = imageProvider
|
||||
|
||||
self.transformer = transformer
|
||||
self.placeholder = placeholder
|
||||
}
|
||||
|
||||
init(url: URL?,
|
||||
blurhash: String? = nil,
|
||||
size: CGSize? = nil,
|
||||
imageProvider: ImageProviderProtocol?,
|
||||
transformer: @escaping (Image) -> TransformerView = { $0 },
|
||||
placeholder: @escaping () -> PlaceholderView) {
|
||||
let mediaSource = url.map(MediaSourceProxy.init)
|
||||
|
||||
self.init(mediaSource: mediaSource,
|
||||
blurhash: blurhash,
|
||||
size: size,
|
||||
imageProvider: imageProvider,
|
||||
transformer: transformer,
|
||||
placeholder: placeholder)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let _ = Task {
|
||||
// Future improvement: Does guarding against a nil image prevent the image being updated when the URL changes?
|
||||
guard image == nil, let mediaSource else { return }
|
||||
|
||||
if case let .success(image) = await imageProvider?.loadImageFromSource(mediaSource, size: size) {
|
||||
self.cachedImage = image
|
||||
}
|
||||
}
|
||||
|
||||
ZStack {
|
||||
if let image {
|
||||
transformer(
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
)
|
||||
} else if let blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
transformer(
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
)
|
||||
} else {
|
||||
placeholder()
|
||||
}
|
||||
}
|
||||
.animation(.elementDefault, value: image)
|
||||
}
|
||||
|
||||
private var image: UIImage? {
|
||||
cachedImage ?? imageProvider?.imageFromSource(mediaSource, size: size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PlaceholderAvatarImage: View {
|
||||
@Environment(\.redactionReasons) var redactionReasons
|
||||
|
||||
private let textForImage: String
|
||||
private let contentID: String?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
bgColor
|
||||
|
||||
// This text's frame doesn't look right when redacted
|
||||
if redactionReasons != .placeholder {
|
||||
Text(textForImage)
|
||||
.padding(4)
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 200).weight(.semibold))
|
||||
.minimumScaleFactor(0.001)
|
||||
}
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
}
|
||||
|
||||
init(name: String?, contentID: String?) {
|
||||
let baseName = name ?? contentID?.trimmingCharacters(in: .punctuationCharacters)
|
||||
textForImage = baseName?.first?.uppercased() ?? ""
|
||||
self.contentID = contentID
|
||||
}
|
||||
|
||||
private var bgColor: Color {
|
||||
if redactionReasons == .placeholder {
|
||||
return .element.systemGray4
|
||||
}
|
||||
|
||||
guard let contentID else {
|
||||
return .element.accent
|
||||
}
|
||||
|
||||
return .element.avatarBackground(for: contentID)
|
||||
}
|
||||
}
|
||||
|
||||
struct PlaceholderAvatarImage_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 75) {
|
||||
PlaceholderAvatarImage(name: "Xavier", contentID: "@userid1:matrix.org")
|
||||
.clipShape(Circle())
|
||||
.frame(width: 150, height: 100)
|
||||
|
||||
PlaceholderAvatarImage(name: "@*~AmazingName~*@", contentID: "@userid2:matrix.org")
|
||||
.clipShape(Circle())
|
||||
.frame(width: 150, height: 100)
|
||||
|
||||
PlaceholderAvatarImage(name: nil, contentID: "@userid3:matrix.org")
|
||||
.clipShape(Circle())
|
||||
.frame(width: 150, height: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,6 @@ enum HomeScreenViewUserMenuAction {
|
||||
}
|
||||
|
||||
enum HomeScreenViewAction {
|
||||
case loadRoomData(roomIdentifier: String)
|
||||
case selectRoom(roomIdentifier: String)
|
||||
case userMenu(action: HomeScreenViewUserMenuAction)
|
||||
case verifySession
|
||||
@@ -59,7 +58,7 @@ enum HomeScreenRoomListMode: CustomStringConvertible {
|
||||
struct HomeScreenViewState: BindableState {
|
||||
var userID: String
|
||||
var userDisplayName: String?
|
||||
var userAvatar: UIImage?
|
||||
var userAvatarURL: URL?
|
||||
|
||||
var showSessionVerificationBanner = false
|
||||
|
||||
@@ -96,7 +95,6 @@ struct HomeScreenViewStateBindings {
|
||||
|
||||
struct HomeScreenRoom: Identifiable, Equatable {
|
||||
private static let placeholderLastMessage = AttributedString("Last message")
|
||||
private static let placeholderAvatar = UIImage(systemName: "photo")
|
||||
|
||||
/// The list item identifier can be a real room identifier, a custom one for invalidated entries
|
||||
/// or a completely unique one for empty items and skeletons
|
||||
@@ -115,8 +113,6 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
|
||||
var avatarURL: URL?
|
||||
|
||||
var avatar: UIImage?
|
||||
|
||||
var isPlaceholder = false
|
||||
|
||||
static func placeholder() -> HomeScreenRoom {
|
||||
@@ -126,7 +122,6 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
hasUnreads: false,
|
||||
timestamp: "Now",
|
||||
lastMessage: Self.placeholderLastMessage,
|
||||
avatar: Self.placeholderAvatar,
|
||||
isPlaceholder: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
// swiftlint:disable:next function_body_length cyclomatic_complexity
|
||||
// swiftlint:disable:next function_body_length
|
||||
init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) {
|
||||
self.userSession = userSession
|
||||
self.attributedStringBuilder = attributedStringBuilder
|
||||
@@ -45,7 +45,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
visibleRoomsSummaryProvider = userSession.clientProxy.visibleRoomsSummaryProvider
|
||||
allRoomsSummaryProvider = userSession.clientProxy.allRoomsSummaryProvider
|
||||
|
||||
super.init(initialViewState: HomeScreenViewState(userID: userSession.userID))
|
||||
super.init(initialViewState: HomeScreenViewState(userID: userSession.userID),
|
||||
imageProvider: userSession.mediaProvider)
|
||||
|
||||
userSession.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
@@ -71,9 +72,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
Task {
|
||||
if case let .success(url) = await userSession.clientProxy.loadUserAvatarURL() {
|
||||
if case let .success(avatar) = await userSession.mediaProvider.loadImageFromURL(url, avatarSize: .user(on: .home)) {
|
||||
state.userAvatar = avatar
|
||||
}
|
||||
state.userAvatarURL = url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,13 +138,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
override func process(viewAction: HomeScreenViewAction) async {
|
||||
switch viewAction {
|
||||
case .loadRoomData(let roomIdentifier):
|
||||
if state.roomListMode != .skeletons {
|
||||
loadDataForRoomIdentifier(roomIdentifier)
|
||||
}
|
||||
case .selectRoom(let roomIdentifier):
|
||||
callback?(.presentRoom(roomIdentifier: roomIdentifier))
|
||||
case .userMenu(let action):
|
||||
@@ -178,27 +172,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func loadDataForRoomIdentifier(_ identifier: String) {
|
||||
guard let room = roomsForIdentifiers[identifier],
|
||||
room.avatar == nil,
|
||||
let avatarURL = room.avatarURL else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
if case let .success(image) = await userSession.mediaProvider.loadImageFromURL(avatarURL, avatarSize: .room(on: .home)) {
|
||||
guard let roomIndex = state.rooms.firstIndex(where: { $0.roomId == identifier }) else {
|
||||
return
|
||||
}
|
||||
|
||||
var room = state.rooms[roomIndex]
|
||||
room.avatar = image
|
||||
state.rooms[roomIndex] = room
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This method will update all view state rooms by merging the data from both summary providers
|
||||
/// If a room is empty in the visible room summary provider it will try to get it from the allRooms one
|
||||
/// This ensures that we show as many room details as possible without loading up timelines
|
||||
@@ -256,10 +230,6 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
room.hasUnreads = details.unreadNotificationCount > 0
|
||||
room.lastMessage = details.lastMessage
|
||||
|
||||
if let avatarURL = details.avatarURL {
|
||||
room.avatar = userSession.mediaProvider.imageFromURL(avatarURL, avatarSize: .room(on: .home))
|
||||
}
|
||||
|
||||
if let lastMessageTimestamp = details.lastMessageTimestamp {
|
||||
room.timestamp = lastMessageTimestamp.formattedMinimal()
|
||||
}
|
||||
|
||||
@@ -102,9 +102,12 @@ struct HomeScreen: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
userAvatarImageView
|
||||
.animation(.elementDefault, value: context.viewState.userAvatar)
|
||||
.transition(.opacity)
|
||||
LoadableAvatarImage(url: context.viewState.userAvatarURL,
|
||||
name: context.viewState.userDisplayName,
|
||||
contentID: context.viewState.userID,
|
||||
avatarSize: .user(on: .home),
|
||||
imageProvider: context.imageProvider)
|
||||
.accessibilityIdentifier("userAvatarImage")
|
||||
}
|
||||
.alert(ElementL10n.actionSignOut,
|
||||
isPresented: $showingLogoutConfirmation) {
|
||||
@@ -116,26 +119,6 @@ struct HomeScreen: View {
|
||||
}
|
||||
.accessibilityLabel(ElementL10n.a11yAllChatsUserAvatarMenu)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var userAvatarImageView: some View {
|
||||
userAvatarImage
|
||||
.frame(width: AvatarSize.user(on: .home).value, height: AvatarSize.user(on: .home).value, alignment: .center)
|
||||
.clipShape(Circle())
|
||||
.accessibilityIdentifier("userAvatarImage")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var userAvatarImage: some View {
|
||||
if let avatar = context.viewState.userAvatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: context.viewState.userDisplayName ?? context.viewState.userID,
|
||||
contentId: context.viewState.userID)
|
||||
}
|
||||
}
|
||||
|
||||
private var sessionVerificationBanner: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct HomeScreenRoomCell: View {
|
||||
@ScaledMetric private var avatarSize = AvatarSize.room(on: .home).value
|
||||
|
||||
let room: HomeScreenRoom
|
||||
let context: HomeScreenViewModel.Context
|
||||
|
||||
@@ -38,11 +36,6 @@ struct HomeScreenRoomCell: View {
|
||||
}
|
||||
.frame(minHeight: 64.0)
|
||||
.accessibilityElement(children: .combine)
|
||||
.task {
|
||||
if let roomId = room.roomId {
|
||||
context.send(viewAction: .loadRoomData(roomIdentifier: roomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(HomeScreenRoomCellButtonStyle())
|
||||
.accessibilityIdentifier("roomName:\(room.name)")
|
||||
@@ -50,19 +43,12 @@ struct HomeScreenRoomCell: View {
|
||||
|
||||
@ViewBuilder
|
||||
var avatar: some View {
|
||||
if let avatar = room.avatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
.accessibilityHidden(true)
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: room.name, contentId: room.roomId)
|
||||
.clipShape(Circle())
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
LoadableAvatarImage(url: room.avatarURL,
|
||||
name: room.name,
|
||||
contentID: room.roomId,
|
||||
avatarSize: .room(on: .home),
|
||||
imageProvider: context.imageProvider)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
@@ -32,9 +32,9 @@ struct RoomDetailsViewState: BindableState {
|
||||
let roomId: String
|
||||
let isEncrypted: Bool
|
||||
let isDirect: Bool
|
||||
var roomTitle = ""
|
||||
var roomTopic: String?
|
||||
var roomAvatar: UIImage?
|
||||
var title = ""
|
||||
var topic: String?
|
||||
var avatarURL: URL?
|
||||
var members: [RoomDetailsMember]
|
||||
|
||||
var isLoadingMembers: Bool {
|
||||
@@ -62,8 +62,6 @@ struct RoomDetailsMember: Identifiable, Equatable {
|
||||
let id: String
|
||||
let name: String?
|
||||
let avatarURL: URL?
|
||||
// cached
|
||||
var avatar: UIImage?
|
||||
|
||||
init(withProxy proxy: RoomMemberProxy) {
|
||||
id = proxy.userId
|
||||
|
||||
@@ -19,29 +19,25 @@ import SwiftUI
|
||||
typealias RoomDetailsViewModelType = StateStoreViewModel<RoomDetailsViewState, RoomDetailsViewAction>
|
||||
|
||||
class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtocol {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
|
||||
private var members: [RoomMemberProxy] = [] {
|
||||
didSet {
|
||||
state.members = members.map { RoomDetailsMember(withProxy: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var callback: ((RoomDetailsViewModelAction) -> Void)?
|
||||
|
||||
init(roomProxy: RoomProxyProtocol,
|
||||
mediaProvider: MediaProviderProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
self.mediaProvider = mediaProvider
|
||||
|
||||
init(roomProxy: RoomProxyProtocol, mediaProvider: MediaProviderProtocol) {
|
||||
super.init(initialViewState: .init(roomId: roomProxy.id,
|
||||
isEncrypted: roomProxy.isEncrypted,
|
||||
isDirect: roomProxy.isDirect,
|
||||
roomTitle: roomProxy.displayName ?? roomProxy.name ?? "Unknown Room",
|
||||
roomTopic: roomProxy.topic,
|
||||
title: roomProxy.displayName ?? roomProxy.name ?? "Unknown Room",
|
||||
topic: roomProxy.topic,
|
||||
avatarURL: roomProxy.avatarURL,
|
||||
members: [],
|
||||
bindings: .init()))
|
||||
|
||||
bindings: .init()),
|
||||
imageProvider: mediaProvider)
|
||||
|
||||
Task {
|
||||
switch await roomProxy.members() {
|
||||
case .success(let members):
|
||||
@@ -51,14 +47,6 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
|
||||
state.bindings.alertInfo = AlertInfo(id: .alert(ElementL10n.unknownError))
|
||||
}
|
||||
}
|
||||
|
||||
if let avatarURL = roomProxy.avatarURL {
|
||||
Task {
|
||||
if case let .success(avatar) = await mediaProvider.loadImageFromURL(avatarURL, avatarSize: .room(on: .details)) {
|
||||
state.roomAvatar = avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
@@ -17,24 +17,16 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RoomDetailsScreen: View {
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@ScaledMetric private var avatarSize = AvatarSize.room(on: .details).value
|
||||
@ScaledMetric private var menuIconSize = 30.0
|
||||
private let listRowInsets = EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var context: RoomDetailsViewModel.Context
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
headerSection
|
||||
|
||||
if let topic = context.viewState.roomTopic {
|
||||
if let topic = context.viewState.topic {
|
||||
topicSection(with: topic)
|
||||
}
|
||||
|
||||
@@ -47,11 +39,19 @@ struct RoomDetailsScreen: View {
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.navigationTitle(ElementL10n.roomDetailsTitle)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var headerSection: some View {
|
||||
VStack(spacing: 16.0) {
|
||||
roomAvatarImage
|
||||
Text(context.viewState.roomTitle)
|
||||
LoadableAvatarImage(url: context.viewState.avatarURL,
|
||||
name: context.viewState.title,
|
||||
contentID: context.viewState.roomId,
|
||||
avatarSize: .room(on: .details),
|
||||
imageProvider: context.imageProvider)
|
||||
.accessibilityIdentifier("roomAvatarImage")
|
||||
|
||||
Text(context.viewState.title)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.font(.element.headline)
|
||||
.multilineTextAlignment(.center)
|
||||
@@ -128,23 +128,6 @@ struct RoomDetailsScreen: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private var roomAvatarImage: some View {
|
||||
if let avatar = context.viewState.roomAvatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
.accessibilityIdentifier("roomAvatarImage")
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: context.viewState.roomTitle,
|
||||
contentId: context.viewState.roomId)
|
||||
.clipShape(Circle())
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.accessibilityIdentifier("roomAvatarPlaceholderImage")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@@ -46,5 +46,4 @@ struct RoomMemberDetailsViewStateBindings {
|
||||
|
||||
enum RoomMemberDetailsViewAction {
|
||||
case selectMember(id: String)
|
||||
case loadMemberData(id: String)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDeta
|
||||
members: [RoomMemberProxy]) {
|
||||
self.mediaProvider = mediaProvider
|
||||
super.init(initialViewState: .init(members: members.map { RoomDetailsMember(withProxy: $0) },
|
||||
bindings: .init()))
|
||||
bindings: .init()),
|
||||
imageProvider: mediaProvider)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -36,30 +37,6 @@ class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDeta
|
||||
switch viewAction {
|
||||
case .selectMember(let id):
|
||||
MXLog.debug("Member selected: \(id)")
|
||||
case .loadMemberData(let id):
|
||||
await loadAvatar(forMember: id)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAvatar(forMember memberId: String) async {
|
||||
guard let member = state.members.first(where: { $0.id == memberId }) else {
|
||||
return
|
||||
}
|
||||
if member.avatar != nil {
|
||||
// Avatar already loaded.
|
||||
return
|
||||
}
|
||||
guard let avatarURL = member.avatarURL else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromURL(avatarURL, avatarSize: .user(on: .roomDetails)) {
|
||||
case .success(let image):
|
||||
if let index = state.members.firstIndex(where: { $0.id == memberId }) {
|
||||
state.members[index].avatar = image
|
||||
}
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed to retrieve room member avatar: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,19 +27,12 @@ struct RoomMemberDetailsMemberCell: View {
|
||||
context.send(viewAction: .selectMember(id: member.id))
|
||||
} label: {
|
||||
HStack {
|
||||
if let avatar = member.avatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
.accessibilityHidden(true)
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: member.name ?? "", contentId: member.id)
|
||||
.clipShape(Circle())
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
LoadableAvatarImage(url: member.avatarURL,
|
||||
name: member.name ?? "",
|
||||
contentID: member.id,
|
||||
avatarSize: .user(on: .roomDetails),
|
||||
imageProvider: context.imageProvider)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(member.name ?? "")
|
||||
.font(.element.callout.bold())
|
||||
@@ -49,9 +42,6 @@ struct RoomMemberDetailsMemberCell: View {
|
||||
Spacer()
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.task {
|
||||
context.send(viewAction: .loadMemberData(id: member.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ enum RoomScreenViewAction {
|
||||
struct RoomScreenViewState: BindableState {
|
||||
var roomId: String
|
||||
var roomTitle = ""
|
||||
var roomAvatar: UIImage?
|
||||
var roomAvatarURL: URL?
|
||||
var items: [RoomTimelineViewProvider] = []
|
||||
var canBackPaginate = true
|
||||
var isBackPaginating = false
|
||||
|
||||
@@ -28,7 +28,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
private let timelineController: RoomTimelineControllerProtocol
|
||||
private let timelineViewFactory: RoomTimelineViewFactoryProtocol
|
||||
private let mediaProvider: MediaProviderProtocol
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
@@ -39,12 +38,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
roomAvatarUrl: URL? = nil) {
|
||||
self.timelineController = timelineController
|
||||
self.timelineViewFactory = timelineViewFactory
|
||||
self.mediaProvider = mediaProvider
|
||||
|
||||
super.init(initialViewState: RoomScreenViewState(roomId: timelineController.roomID,
|
||||
roomTitle: roomName ?? "Unknown room 💥",
|
||||
roomAvatar: nil,
|
||||
bindings: .init(composerText: "", composerFocused: false)))
|
||||
roomAvatarURL: roomAvatarUrl,
|
||||
bindings: .init(composerText: "", composerFocused: false)),
|
||||
imageProvider: mediaProvider)
|
||||
|
||||
timelineController.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
@@ -76,15 +75,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
state.contextMenuBuilder = buildContextMenuForItemId(_:)
|
||||
|
||||
buildTimelineViews()
|
||||
|
||||
if let roomAvatarUrl {
|
||||
Task {
|
||||
if case let .success(avatar) = await mediaProvider.loadImageFromURL(roomAvatarUrl,
|
||||
avatarSize: .room(on: .timeline)) {
|
||||
state.roomAvatar = avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
@@ -38,24 +38,12 @@ struct RoomHeaderView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private var roomAvatar: some View {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
roomAvatarImage
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.frame(width: AvatarSize.room(on: .timeline).value, height: AvatarSize.room(on: .timeline).value)
|
||||
}
|
||||
|
||||
@ViewBuilder private var roomAvatarImage: some View {
|
||||
if let avatar = context.viewState.roomAvatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.accessibilityIdentifier("roomAvatarImage")
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: context.viewState.roomTitle,
|
||||
contentId: context.viewState.roomId)
|
||||
.accessibilityIdentifier("roomAvatarPlaceholderImage")
|
||||
}
|
||||
LoadableAvatarImage(url: context.viewState.roomAvatarURL,
|
||||
name: context.viewState.roomTitle,
|
||||
contentID: context.viewState.roomId,
|
||||
avatarSize: .room(on: .timeline),
|
||||
imageProvider: context.imageProvider)
|
||||
.accessibilityIdentifier("roomAvatarImage")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,33 +18,29 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct ImageRoomTimelineView: View {
|
||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||
let timelineItem: ImageRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
if let image = timelineItem.image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
} else if let blurhash = timelineItem.blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
} else {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.foregroundColor(.element.systemGray6)
|
||||
.opacity(0.3)
|
||||
|
||||
ProgressView(ElementL10n.loading)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
LoadableImage(mediaSource: timelineItem.source,
|
||||
blurhash: timelineItem.blurhash,
|
||||
imageProvider: context.imageProvider) {
|
||||
placeholder
|
||||
}
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
}
|
||||
|
||||
var placeholder: some View {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.foregroundColor(.element.systemGray6)
|
||||
.opacity(0.3)
|
||||
|
||||
ProgressView(ElementL10n.loading)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.animation(.elementDefault, value: timelineItem.image)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +59,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
source: nil,
|
||||
image: UIImage(systemName: "photo")))
|
||||
source: nil))
|
||||
|
||||
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some other image",
|
||||
@@ -73,8 +68,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
source: nil,
|
||||
image: nil))
|
||||
source: nil))
|
||||
|
||||
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Blurhashed image",
|
||||
@@ -84,7 +78,6 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
source: nil,
|
||||
image: nil,
|
||||
aspectRatio: 0.7,
|
||||
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PlaceholderAvatarImage: View {
|
||||
private let textForImage: String
|
||||
private let contentId: String?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
bgColor
|
||||
Text(textForImage)
|
||||
.padding(4)
|
||||
.foregroundColor(.white)
|
||||
.font(.system(size: 200).weight(.semibold))
|
||||
.minimumScaleFactor(0.001)
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
}
|
||||
|
||||
init(text: String, contentId: String? = nil) {
|
||||
textForImage = text.first?.uppercased() ?? ""
|
||||
self.contentId = contentId
|
||||
}
|
||||
|
||||
private var bgColor: Color {
|
||||
guard let contentId else {
|
||||
return .element.accent
|
||||
}
|
||||
|
||||
return .element.avatarBackground(for: contentId)
|
||||
}
|
||||
}
|
||||
|
||||
struct PlaceholderAvatarImage_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PlaceholderAvatarImage(text: "X", contentId: "@userid:matrix.org")
|
||||
.clipShape(Circle())
|
||||
.frame(width: 150, height: 100)
|
||||
}
|
||||
}
|
||||
@@ -18,35 +18,31 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct StickerRoomTimelineView: View {
|
||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||
let timelineItem: StickerRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
if let image = timelineItem.image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
} else if let blurhash = timelineItem.blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
} else {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.foregroundColor(.element.systemGray6)
|
||||
.opacity(0.3)
|
||||
|
||||
ProgressView(ElementL10n.loading)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
LoadableImage(url: timelineItem.imageURL,
|
||||
blurhash: timelineItem.blurhash,
|
||||
imageProvider: context.imageProvider) {
|
||||
placeholder
|
||||
}
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
.animation(.elementDefault, value: timelineItem.image)
|
||||
.accessibilityLabel(timelineItem.text)
|
||||
}
|
||||
|
||||
private var placeholder: some View {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.foregroundColor(.element.systemGray6)
|
||||
.opacity(0.3)
|
||||
|
||||
ProgressView(ElementL10n.loading)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StickerRoomTimelineView_Previews: PreviewProvider {
|
||||
@@ -64,8 +60,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
imageURL: nil,
|
||||
image: UIImage(systemName: "photo")))
|
||||
imageURL: URL.picturesDirectory))
|
||||
|
||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some other image",
|
||||
@@ -74,8 +69,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
imageURL: nil,
|
||||
image: nil))
|
||||
imageURL: URL.picturesDirectory))
|
||||
|
||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Blurhashed image",
|
||||
@@ -84,8 +78,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
sender: .init(id: "Bob"),
|
||||
imageURL: nil,
|
||||
image: nil,
|
||||
imageURL: URL.picturesDirectory,
|
||||
aspectRatio: 0.7,
|
||||
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))
|
||||
}
|
||||
|
||||
@@ -18,42 +18,39 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct VideoRoomTimelineView: View {
|
||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||
let timelineItem: VideoRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
if let image = timelineItem.image {
|
||||
thumbnail(with: image)
|
||||
} else if let blurhash = timelineItem.blurhash,
|
||||
// Build a small blurhash image so that it's fast
|
||||
let image = UIImage(blurHash: blurhash, size: .init(width: 10.0, height: 10.0)) {
|
||||
thumbnail(with: image)
|
||||
} else {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.foregroundColor(.element.systemGray6)
|
||||
.opacity(0.3)
|
||||
|
||||
ProgressView(ElementL10n.loading)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
LoadableImage(mediaSource: timelineItem.thumbnailSource,
|
||||
blurhash: timelineItem.blurhash,
|
||||
imageProvider: context.imageProvider) { imageView in
|
||||
imageView
|
||||
.overlay { playIcon }
|
||||
} placeholder: {
|
||||
placeholder
|
||||
}
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
}
|
||||
.animation(.elementDefault, value: timelineItem.image)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func thumbnail(with image: UIImage) -> some View {
|
||||
|
||||
var playIcon: some View {
|
||||
Image(systemName: "play.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.background(.ultraThinMaterial, in: Circle())
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
var placeholder: some View {
|
||||
ZStack {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||
Image(systemName: "play.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.background(.ultraThinMaterial, in: Circle())
|
||||
.foregroundColor(.white)
|
||||
Rectangle()
|
||||
.foregroundColor(.element.systemGray6)
|
||||
.opacity(0.3)
|
||||
|
||||
ProgressView(ElementL10n.loading)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,8 +72,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
||||
sender: .init(id: "Bob"),
|
||||
duration: 21,
|
||||
source: nil,
|
||||
thumbnailSource: nil,
|
||||
image: UIImage(systemName: "photo")))
|
||||
thumbnailSource: nil))
|
||||
|
||||
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Some other video",
|
||||
@@ -87,8 +83,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
||||
sender: .init(id: "Bob"),
|
||||
duration: 22,
|
||||
source: nil,
|
||||
thumbnailSource: nil,
|
||||
image: nil))
|
||||
thumbnailSource: nil))
|
||||
|
||||
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "Blurhashed video",
|
||||
@@ -100,7 +95,6 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
||||
duration: 23,
|
||||
source: nil,
|
||||
thumbnailSource: nil,
|
||||
image: nil,
|
||||
aspectRatio: 0.7,
|
||||
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))
|
||||
}
|
||||
|
||||
@@ -18,29 +18,19 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineSenderAvatarView: View {
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
|
||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||
@ScaledMetric private var avatarSize = AvatarSize.user(on: .timeline).value
|
||||
|
||||
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .center) {
|
||||
if let avatar = timelineItem.sender.avatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.overlay(Circle().stroke(Color.element.accent))
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: timelineItem.sender.displayName ?? timelineItem.sender.id,
|
||||
contentId: timelineItem.sender.id)
|
||||
}
|
||||
}
|
||||
.clipShape(Circle())
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.element.background, lineWidth: 3)
|
||||
)
|
||||
|
||||
.animation(.elementDefault, value: timelineItem.sender.avatar)
|
||||
LoadableAvatarImage(url: timelineItem.sender.avatarURL,
|
||||
name: timelineItem.sender.displayName,
|
||||
contentID: timelineItem.sender.id,
|
||||
avatarSize: .user(on: .timeline),
|
||||
imageProvider: context.imageProvider)
|
||||
.overlay(
|
||||
Circle().stroke(Color.element.background, lineWidth: 3)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,30 +16,30 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsCoordinatorParameters {
|
||||
struct SettingsScreenCoordinatorParameters {
|
||||
let navigationStackCoordinator: NavigationStackCoordinator
|
||||
let userNotificationController: UserNotificationControllerProtocol
|
||||
let userSession: UserSessionProtocol
|
||||
let bugReportService: BugReportServiceProtocol
|
||||
}
|
||||
|
||||
enum SettingsCoordinatorAction {
|
||||
enum SettingsScreenCoordinatorAction {
|
||||
case dismiss
|
||||
case logout
|
||||
}
|
||||
|
||||
final class SettingsCoordinator: CoordinatorProtocol {
|
||||
private let parameters: SettingsCoordinatorParameters
|
||||
private var viewModel: SettingsViewModelProtocol
|
||||
final class SettingsScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: SettingsScreenCoordinatorParameters
|
||||
private var viewModel: SettingsScreenViewModelProtocol
|
||||
|
||||
var callback: ((SettingsCoordinatorAction) -> Void)?
|
||||
var callback: ((SettingsScreenCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: SettingsCoordinatorParameters) {
|
||||
init(parameters: SettingsScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = SettingsViewModel(withUserSession: parameters.userSession)
|
||||
viewModel = SettingsScreenViewModel(withUserSession: parameters.userSession)
|
||||
viewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
@@ -17,26 +17,26 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum SettingsViewModelAction {
|
||||
enum SettingsScreenViewModelAction {
|
||||
case close
|
||||
case toggleAnalytics
|
||||
case reportBug
|
||||
case logout
|
||||
}
|
||||
|
||||
struct SettingsViewState: BindableState {
|
||||
var bindings: SettingsViewStateBindings
|
||||
struct SettingsScreenViewState: BindableState {
|
||||
var bindings: SettingsScreenViewStateBindings
|
||||
var deviceID: String?
|
||||
var userID: String
|
||||
var userAvatar: UIImage?
|
||||
var userAvatarURL: URL?
|
||||
var userDisplayName: String?
|
||||
}
|
||||
|
||||
struct SettingsViewStateBindings {
|
||||
struct SettingsScreenViewStateBindings {
|
||||
var enableAnalytics = ServiceLocator.shared.settings.enableAnalytics
|
||||
}
|
||||
|
||||
enum SettingsViewAction {
|
||||
enum SettingsScreenViewAction {
|
||||
case close
|
||||
case toggleAnalytics
|
||||
case reportBug
|
||||
@@ -16,23 +16,22 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
typealias SettingsViewModelType = StateStoreViewModel<SettingsViewState, SettingsViewAction>
|
||||
typealias SettingsScreenViewModelType = StateStoreViewModel<SettingsScreenViewState, SettingsScreenViewAction>
|
||||
|
||||
class SettingsViewModel: SettingsViewModelType, SettingsViewModelProtocol {
|
||||
class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewModelProtocol {
|
||||
private let userSession: UserSessionProtocol
|
||||
|
||||
var callback: ((SettingsViewModelAction) -> Void)?
|
||||
var callback: ((SettingsScreenViewModelAction) -> Void)?
|
||||
|
||||
init(withUserSession userSession: UserSessionProtocol) {
|
||||
self.userSession = userSession
|
||||
let bindings = SettingsViewStateBindings()
|
||||
super.init(initialViewState: .init(bindings: bindings, deviceID: userSession.deviceId, userID: userSession.userID))
|
||||
let bindings = SettingsScreenViewStateBindings()
|
||||
super.init(initialViewState: .init(bindings: bindings, deviceID: userSession.deviceId, userID: userSession.userID),
|
||||
imageProvider: userSession.mediaProvider)
|
||||
|
||||
Task {
|
||||
if case let .success(userAvatarURL) = await userSession.clientProxy.loadUserAvatarURL() {
|
||||
if case let .success(avatar) = await userSession.mediaProvider.loadImageFromURL(userAvatarURL, avatarSize: .user(on: .settings)) {
|
||||
state.userAvatar = avatar
|
||||
}
|
||||
state.userAvatarURL = userAvatarURL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +42,7 @@ class SettingsViewModel: SettingsViewModelType, SettingsViewModelProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
override func process(viewAction: SettingsViewAction) async {
|
||||
override func process(viewAction: SettingsScreenViewAction) async {
|
||||
switch viewAction {
|
||||
case .close:
|
||||
callback?(.close)
|
||||
@@ -17,7 +17,7 @@
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol SettingsViewModelProtocol {
|
||||
var callback: ((SettingsViewModelAction) -> Void)? { get set }
|
||||
var context: SettingsViewModelType.Context { get }
|
||||
protocol SettingsScreenViewModelProtocol {
|
||||
var callback: ((SettingsScreenViewModelAction) -> Void)? { get set }
|
||||
var context: SettingsScreenViewModelType.Context { get }
|
||||
}
|
||||
@@ -25,7 +25,7 @@ struct SettingsScreen: View {
|
||||
@ScaledMetric private var menuIconSize = 30.0
|
||||
private let listRowInsets = EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
|
||||
|
||||
@ObservedObject var context: SettingsViewModel.Context
|
||||
@ObservedObject var context: SettingsScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
@@ -69,7 +69,12 @@ struct SettingsScreen: View {
|
||||
private var userSection: some View {
|
||||
Section {
|
||||
HStack(spacing: 13) {
|
||||
userAvatar
|
||||
LoadableAvatarImage(url: context.viewState.userAvatarURL,
|
||||
name: context.viewState.userDisplayName,
|
||||
contentID: context.viewState.userID,
|
||||
avatarSize: .user(on: .settings),
|
||||
imageProvider: context.imageProvider)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(context.viewState.userDisplayName ?? "")
|
||||
.font(.title3)
|
||||
@@ -80,21 +85,6 @@ struct SettingsScreen: View {
|
||||
.listRowInsets(listRowInsets)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var userAvatar: some View {
|
||||
if let avatar = context.viewState.userAvatar {
|
||||
Image(uiImage: avatar)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
} else {
|
||||
PlaceholderAvatarImage(text: context.viewState.userDisplayName ?? context.viewState.userID, contentId: context.viewState.userID)
|
||||
.clipShape(Circle())
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
}
|
||||
}
|
||||
|
||||
private var analyticsSection: some View {
|
||||
Section {
|
||||
@@ -198,11 +188,11 @@ extension TimelineStyle: CustomStringConvertible {
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct Settings_Previews: PreviewProvider {
|
||||
struct SettingsScreen_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"),
|
||||
mediaProvider: MockMediaProvider())
|
||||
let viewModel = SettingsViewModel(withUserSession: userSession)
|
||||
let viewModel = SettingsScreenViewModel(withUserSession: userSession)
|
||||
|
||||
NavigationView {
|
||||
SettingsScreen(context: viewModel.context)
|
||||
|
||||
@@ -58,7 +58,7 @@ class UIKitBackgroundTask: BackgroundTaskProtocol {
|
||||
}
|
||||
|
||||
if identifier == .invalid {
|
||||
MXLog.info("Do not start background task: \(name), as OS declined")
|
||||
MXLog.error("Do not start background task: \(name), as OS declined")
|
||||
// call expiration handler immediately
|
||||
expirationHandler?(self)
|
||||
return nil
|
||||
@@ -69,7 +69,7 @@ class UIKitBackgroundTask: BackgroundTaskProtocol {
|
||||
reuse()
|
||||
}
|
||||
|
||||
MXLog.info("Start background task #\(identifier.rawValue) - \(name)")
|
||||
MXLog.verbose("Start background task #\(identifier.rawValue) - \(name)")
|
||||
}
|
||||
|
||||
func reuse() {
|
||||
@@ -92,7 +92,7 @@ class UIKitBackgroundTask: BackgroundTaskProtocol {
|
||||
|
||||
private func endTask() {
|
||||
if identifier != .invalid {
|
||||
MXLog.info("End background task #\(identifier.rawValue) - \(name) after \(readableElapsedTime)")
|
||||
MXLog.verbose("End background task #\(identifier.rawValue) - \(name) after \(readableElapsedTime)")
|
||||
|
||||
application.endBackgroundTask(identifier)
|
||||
identifier = .invalid
|
||||
|
||||
@@ -41,7 +41,7 @@ class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
|
||||
}
|
||||
|
||||
if avoidStartingNewTasks(for: application) {
|
||||
MXLog.info("Do not start background task: \(name), as not enough time exists")
|
||||
MXLog.error("Do not start background task: \(name), as not enough time exists")
|
||||
// call expiration handler immediately
|
||||
expirationHandler?()
|
||||
return nil
|
||||
@@ -82,7 +82,7 @@ class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
|
||||
let appState = application.applicationState
|
||||
let remainingTime = readableBackgroundTimeRemaining(application.backgroundTimeRemaining)
|
||||
|
||||
MXLog.info("Background task \(name) \(created ? "started" : "reused") with app state: \(appState) and estimated background time remaining: \(remainingTime)")
|
||||
MXLog.verbose("Background task \(name) \(created ? "started" : "reused") with app state: \(appState) and estimated background time remaining: \(remainingTime)")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
private let client: ClientProtocol
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||
private var sessionVerificationControllerProxy: SessionVerificationControllerProxy?
|
||||
private let mediaProxy: MediaProxyProtocol
|
||||
private let mediaLoader: MediaLoaderProtocol
|
||||
private let clientQueue: DispatchQueue
|
||||
|
||||
private var slidingSyncObserverToken: StoppableSpawn?
|
||||
@@ -86,7 +86,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
clientQueue = .init(label: "ClientProxyQueue", attributes: .concurrent)
|
||||
|
||||
mediaProxy = MediaProxy(client: client, clientQueue: clientQueue)
|
||||
mediaLoader = MediaLoader(client: client, clientQueue: clientQueue)
|
||||
|
||||
client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self))
|
||||
|
||||
@@ -401,16 +401,16 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
extension ClientProxy: MediaProxyProtocol {
|
||||
func mediaSourceForURL(_ url: URL) -> MediaSourceProxy {
|
||||
mediaProxy.mediaSourceForURL(url)
|
||||
extension ClientProxy: MediaLoaderProtocol {
|
||||
func mediaSourceForURL(_ url: URL) async -> MediaSourceProxy {
|
||||
await mediaLoader.mediaSourceForURL(url)
|
||||
}
|
||||
|
||||
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
|
||||
try await mediaProxy.loadMediaContentForSource(source)
|
||||
try await mediaLoader.loadMediaContentForSource(source)
|
||||
}
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
|
||||
try await mediaProxy.loadMediaThumbnailForSource(source, width: width, height: height)
|
||||
try await mediaLoader.loadMediaThumbnailForSource(source, width: width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ enum PushFormat {
|
||||
// }
|
||||
}
|
||||
|
||||
protocol ClientProxyProtocol: AnyObject, MediaProxyProtocol {
|
||||
protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
|
||||
var callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
|
||||
|
||||
var userID: String { get }
|
||||
|
||||
45
ElementX/Sources/Services/Media/ImageProviderProtocol.swift
Normal file
45
ElementX/Sources/Services/Media/ImageProviderProtocol.swift
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol ImageProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage?
|
||||
|
||||
@discardableResult func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result<UIImage, MediaProviderError>
|
||||
|
||||
func imageFromURL(_ url: URL?, size: CGSize?) -> UIImage?
|
||||
|
||||
@discardableResult func loadImageFromURL(_ url: URL, size: CGSize?) async -> Result<UIImage, MediaProviderError>
|
||||
}
|
||||
|
||||
extension ImageProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSourceProxy?) -> UIImage? {
|
||||
imageFromSource(source, size: nil)
|
||||
}
|
||||
|
||||
@discardableResult func loadImageFromSource(_ source: MediaSourceProxy) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(source, size: nil)
|
||||
}
|
||||
|
||||
func imageFromURL(_ url: URL?) -> UIImage? {
|
||||
imageFromURL(url, size: nil)
|
||||
}
|
||||
|
||||
@discardableResult func loadImageFromURL(_ url: URL) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromURL(url, size: nil)
|
||||
}
|
||||
}
|
||||
84
ElementX/Sources/Services/Media/MediaLoader.swift
Normal file
84
ElementX/Sources/Services/Media/MediaLoader.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
private final class MediaRequest {
|
||||
var continuations: [CheckedContinuation<Data, Error>] = []
|
||||
}
|
||||
|
||||
actor MediaLoader: MediaLoaderProtocol {
|
||||
private let client: ClientProtocol
|
||||
private let clientQueue: DispatchQueue
|
||||
private var ongoingRequests = [MediaSourceProxy: MediaRequest]()
|
||||
|
||||
init(client: ClientProtocol,
|
||||
clientQueue: DispatchQueue = .global()) {
|
||||
self.client = client
|
||||
self.clientQueue = clientQueue
|
||||
}
|
||||
|
||||
func mediaSourceForURL(_ url: URL) -> MediaSourceProxy {
|
||||
.init(url: url)
|
||||
}
|
||||
|
||||
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
|
||||
try await enqueueLoadMediaRequest(forSource: source) {
|
||||
try self.client.getMediaContent(source: source.underlyingSource)
|
||||
}
|
||||
}
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
|
||||
try await enqueueLoadMediaRequest(forSource: source) {
|
||||
try self.client.getMediaThumbnail(source: source.underlyingSource, width: UInt64(width), height: UInt64(height))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func enqueueLoadMediaRequest(forSource source: MediaSourceProxy, operation: @escaping () throws -> [UInt8]) async throws -> Data {
|
||||
if let ongoingRequest = ongoingRequests[source] {
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
ongoingRequest.continuations.append(continuation)
|
||||
}
|
||||
}
|
||||
|
||||
let ongoingRequest = MediaRequest()
|
||||
ongoingRequests[source] = ongoingRequest
|
||||
|
||||
defer {
|
||||
ongoingRequests[source] = nil
|
||||
}
|
||||
|
||||
do {
|
||||
let result = try await Task.dispatch(on: clientQueue) {
|
||||
let bytes = try operation()
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
}
|
||||
|
||||
ongoingRequest.continuations.forEach { $0.resume(returning: result) }
|
||||
|
||||
return result
|
||||
|
||||
} catch {
|
||||
ongoingRequest.continuations.forEach { $0.resume(throwing: error) }
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol MediaProxyProtocol {
|
||||
func mediaSourceForURL(_ url: URL) -> MediaSourceProxy
|
||||
protocol MediaLoaderProtocol {
|
||||
func mediaSourceForURL(_ url: URL) async -> MediaSourceProxy
|
||||
|
||||
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data
|
||||
|
||||
@@ -18,53 +18,54 @@ import Kingfisher
|
||||
import UIKit
|
||||
|
||||
struct MediaProvider: MediaProviderProtocol {
|
||||
private let mediaProxy: MediaProxyProtocol
|
||||
private let mediaLoader: MediaLoaderProtocol
|
||||
private let imageCache: Kingfisher.ImageCache
|
||||
private let fileCache: FileCacheProtocol
|
||||
private let backgroundTaskService: BackgroundTaskServiceProtocol?
|
||||
|
||||
init(mediaProxy: MediaProxyProtocol,
|
||||
init(mediaLoader: MediaLoaderProtocol,
|
||||
imageCache: Kingfisher.ImageCache,
|
||||
fileCache: FileCacheProtocol,
|
||||
backgroundTaskService: BackgroundTaskServiceProtocol?) {
|
||||
self.mediaProxy = mediaProxy
|
||||
self.mediaLoader = mediaLoader
|
||||
self.imageCache = imageCache
|
||||
self.fileCache = fileCache
|
||||
self.backgroundTaskService = backgroundTaskService
|
||||
}
|
||||
|
||||
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage? {
|
||||
// MARK: Images
|
||||
|
||||
func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage? {
|
||||
guard let url = source?.url else {
|
||||
return nil
|
||||
}
|
||||
let cacheKey = cacheKeyForURL(url, avatarSize: avatarSize)
|
||||
let cacheKey = cacheKeyForURL(url, size: size)
|
||||
return imageCache.retrieveImageInMemoryCache(forKey: cacheKey, options: nil)
|
||||
}
|
||||
|
||||
func imageFromURL(_ url: URL?, avatarSize: AvatarSize?) -> UIImage? {
|
||||
func imageFromURL(_ url: URL?, size: CGSize?) -> UIImage? {
|
||||
guard let url else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return imageFromSource(.init(url: url), avatarSize: avatarSize)
|
||||
return imageFromSource(.init(url: url), size: size)
|
||||
}
|
||||
|
||||
func loadImageFromURL(_ url: URL, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(.init(url: url), avatarSize: avatarSize)
|
||||
func loadImageFromURL(_ url: URL, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(.init(url: url), size: size)
|
||||
}
|
||||
|
||||
func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
if let image = imageFromSource(source, avatarSize: avatarSize) {
|
||||
func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
if let image = imageFromSource(source, size: size) {
|
||||
return .success(image)
|
||||
}
|
||||
|
||||
#warning("Media loading should check for existing in flight operations and de-dupe requests.")
|
||||
let loadImageBgTask = await backgroundTaskService?.startBackgroundTask(withName: "LoadImage: \(source.url.hashValue)")
|
||||
defer {
|
||||
loadImageBgTask?.stop()
|
||||
}
|
||||
|
||||
let cacheKey = cacheKeyForURL(source.url, avatarSize: avatarSize)
|
||||
let cacheKey = cacheKeyForURL(source.url, size: size)
|
||||
|
||||
if case let .success(cacheResult) = await imageCache.retrieveImage(forKey: cacheKey),
|
||||
let image = cacheResult.image {
|
||||
@@ -73,10 +74,10 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
|
||||
do {
|
||||
let imageData: Data
|
||||
if let avatarSize {
|
||||
imageData = try await mediaProxy.loadMediaThumbnailForSource(source, width: UInt(avatarSize.scaledValue), height: UInt(avatarSize.scaledValue))
|
||||
if let size {
|
||||
imageData = try await mediaLoader.loadMediaThumbnailForSource(source, width: UInt(size.width), height: UInt(size.height))
|
||||
} else {
|
||||
imageData = try await mediaProxy.loadMediaContentForSource(source)
|
||||
imageData = try await mediaLoader.loadMediaContentForSource(source)
|
||||
}
|
||||
|
||||
guard let image = UIImage(data: imageData) else {
|
||||
@@ -92,6 +93,8 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
return .failure(.failedRetrievingImage)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Files
|
||||
|
||||
func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL? {
|
||||
guard let source else {
|
||||
@@ -100,6 +103,18 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
let cacheKey = fileCacheKeyForURL(source.url)
|
||||
return fileCache.file(forKey: cacheKey, fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
func fileFromURL(_ url: URL?, fileExtension: String) -> URL? {
|
||||
guard let url else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fileFromSource(MediaSourceProxy(url: url), fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
func loadFileFromURL(_ url: URL, fileExtension: String) async -> Result<URL, MediaProviderError> {
|
||||
await loadFileFromSource(MediaSourceProxy(url: url), fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
@discardableResult func loadFileFromSource(_ source: MediaSourceProxy, fileExtension: String) async -> Result<URL, MediaProviderError> {
|
||||
if let url = fileFromSource(source, fileExtension: fileExtension) {
|
||||
@@ -114,7 +129,7 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
let cacheKey = fileCacheKeyForURL(source.url)
|
||||
|
||||
do {
|
||||
let data = try await mediaProxy.loadMediaContentForSource(source)
|
||||
let data = try await mediaLoader.loadMediaContentForSource(source)
|
||||
|
||||
let url = try fileCache.store(data, with: fileExtension, forKey: cacheKey)
|
||||
return .success(url)
|
||||
@@ -124,23 +139,11 @@ struct MediaProvider: MediaProviderProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func fileFromURL(_ url: URL?, fileExtension: String) -> URL? {
|
||||
guard let url else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fileFromSource(MediaSourceProxy(url: url), fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
func loadFileFromURL(_ url: URL, fileExtension: String) async -> Result<URL, MediaProviderError> {
|
||||
await loadFileFromSource(MediaSourceProxy(url: url), fileExtension: fileExtension)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func cacheKeyForURL(_ url: URL, avatarSize: AvatarSize?) -> String {
|
||||
if let avatarSize {
|
||||
return "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}"
|
||||
private func cacheKeyForURL(_ url: URL, size: CGSize?) -> String {
|
||||
if let size {
|
||||
return "\(url.absoluteString){\(size.width),\(size.height)}"
|
||||
} else {
|
||||
return url.absoluteString
|
||||
}
|
||||
|
||||
@@ -23,15 +23,7 @@ enum MediaProviderError: Error {
|
||||
case invalidImageData
|
||||
}
|
||||
|
||||
protocol MediaProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage?
|
||||
|
||||
@discardableResult func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
|
||||
|
||||
func imageFromURL(_ url: URL?, avatarSize: AvatarSize?) -> UIImage?
|
||||
|
||||
@discardableResult func loadImageFromURL(_ url: URL, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError>
|
||||
|
||||
protocol MediaProviderProtocol: ImageProviderProtocol {
|
||||
func fileFromSource(_ source: MediaSourceProxy?, fileExtension: String) -> URL?
|
||||
|
||||
@discardableResult func loadFileFromSource(_ source: MediaSourceProxy, fileExtension: String) async -> Result<URL, MediaProviderError>
|
||||
@@ -40,21 +32,3 @@ protocol MediaProviderProtocol {
|
||||
|
||||
@discardableResult func loadFileFromURL(_ url: URL, fileExtension: String) async -> Result<URL, MediaProviderError>
|
||||
}
|
||||
|
||||
extension MediaProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSourceProxy?) -> UIImage? {
|
||||
imageFromSource(source, avatarSize: nil)
|
||||
}
|
||||
|
||||
@discardableResult func loadImageFromSource(_ source: MediaSourceProxy) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromSource(source, avatarSize: nil)
|
||||
}
|
||||
|
||||
func imageFromURL(_ url: URL?) -> UIImage? {
|
||||
imageFromURL(url, avatarSize: nil)
|
||||
}
|
||||
|
||||
@discardableResult func loadImageFromURL(_ url: URL) async -> Result<UIImage, MediaProviderError> {
|
||||
await loadImageFromURL(url, avatarSize: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
import UIKit
|
||||
|
||||
class MediaProxy: MediaProxyProtocol {
|
||||
private let client: ClientProtocol
|
||||
private let clientQueue: DispatchQueue
|
||||
|
||||
init(client: ClientProtocol,
|
||||
clientQueue: DispatchQueue = .global()) {
|
||||
self.client = client
|
||||
self.clientQueue = clientQueue
|
||||
}
|
||||
|
||||
func mediaSourceForURL(_ url: URL) -> MediaSourceProxy {
|
||||
.init(url: url)
|
||||
}
|
||||
|
||||
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data {
|
||||
try await Task.dispatch(on: clientQueue) {
|
||||
let bytes = try self.client.getMediaContent(source: source.underlyingSource)
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
}
|
||||
}
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data {
|
||||
try await Task.dispatch(on: clientQueue) {
|
||||
let bytes = try self.client.getMediaThumbnail(source: source.underlyingSource, width: UInt64(width), height: UInt64(height))
|
||||
return Data(bytes: bytes, count: bytes.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,31 +18,31 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
struct MockMediaProvider: MediaProviderProtocol {
|
||||
func imageFromSource(_ source: MediaSourceProxy?, avatarSize: AvatarSize?) -> UIImage? {
|
||||
func imageFromSource(_ source: MediaSourceProxy?, size: CGSize?) -> UIImage? {
|
||||
nil
|
||||
}
|
||||
|
||||
func loadImageFromSource(_ source: MediaSourceProxy, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
func loadImageFromSource(_ source: MediaSourceProxy, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
.failure(.failedRetrievingImage)
|
||||
}
|
||||
|
||||
func imageFromURL(_ url: URL?, avatarSize: AvatarSize?) -> UIImage? {
|
||||
func imageFromURL(_ url: URL?, size: CGSize?) -> UIImage? {
|
||||
guard url != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let avatarSize {
|
||||
switch avatarSize {
|
||||
case .room:
|
||||
#warning("Fix me. this is stupid!")
|
||||
if let size {
|
||||
if size == AvatarSize.room(on: .details).scaledSize
|
||||
|| size == AvatarSize.room(on: .home).scaledSize
|
||||
|| size == AvatarSize.room(on: .timeline).scaledSize {
|
||||
return Asset.Images.appLogo.image
|
||||
default:
|
||||
return UIImage(systemName: "photo")
|
||||
}
|
||||
}
|
||||
return UIImage(systemName: "photo")
|
||||
}
|
||||
|
||||
func loadImageFromURL(_ url: URL, avatarSize: AvatarSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
func loadImageFromURL(_ url: URL, size: CGSize?) async -> Result<UIImage, MediaProviderError> {
|
||||
guard let image = UIImage(systemName: "photo") else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
@@ -103,20 +103,8 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
}
|
||||
|
||||
if let item = timelineItem as? EventBasedTimelineItemProtocol {
|
||||
await loadUserAvatarForTimelineItem(item)
|
||||
await loadUserDisplayNameForTimelineItem(item)
|
||||
}
|
||||
|
||||
switch timelineItem {
|
||||
case let item as ImageRoomTimelineItem:
|
||||
await loadThumbnailForImageTimelineItem(item)
|
||||
case let item as VideoRoomTimelineItem:
|
||||
await loadThumbnailForVideoTimelineItem(item)
|
||||
case let item as StickerRoomTimelineItem:
|
||||
await loadImageForStickerTimelineItem(item)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func processItemDisappearance(_ itemID: String) { }
|
||||
@@ -339,78 +327,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return .middle
|
||||
}
|
||||
|
||||
private func loadThumbnailForImageTimelineItem(_ timelineItem: ImageRoomTimelineItem) async {
|
||||
if timelineItem.image != nil {
|
||||
return
|
||||
}
|
||||
|
||||
guard let source = timelineItem.source else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromSource(source) {
|
||||
case .success(let image):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? ImageRoomTimelineItem else {
|
||||
return
|
||||
}
|
||||
|
||||
item.image = image
|
||||
timelineItems[index] = item
|
||||
callbacks.send(.updatedTimelineItem(timelineItem.id))
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadThumbnailForVideoTimelineItem(_ timelineItem: VideoRoomTimelineItem) async {
|
||||
if timelineItem.image != nil {
|
||||
return
|
||||
}
|
||||
|
||||
guard let source = timelineItem.thumbnailSource else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromSource(source) {
|
||||
case .success(let image):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? VideoRoomTimelineItem else {
|
||||
return
|
||||
}
|
||||
|
||||
item.image = image
|
||||
timelineItems[index] = item
|
||||
callbacks.send(.updatedTimelineItem(timelineItem.id))
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadImageForStickerTimelineItem(_ timelineItem: StickerRoomTimelineItem) async {
|
||||
if timelineItem.image != nil {
|
||||
return
|
||||
}
|
||||
|
||||
guard let url = timelineItem.imageURL else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromURL(url) {
|
||||
case .success(let image):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? StickerRoomTimelineItem else {
|
||||
return
|
||||
}
|
||||
|
||||
item.image = image
|
||||
timelineItems[index] = item
|
||||
callbacks.send(.updatedTimelineItem(timelineItem.id))
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadVideoForTimelineItem(_ timelineItem: VideoRoomTimelineItem) async {
|
||||
if timelineItem.cachedVideoURL != nil {
|
||||
// already cached
|
||||
@@ -511,29 +427,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadUserAvatarForTimelineItem(_ timelineItem: EventBasedTimelineItemProtocol) async {
|
||||
guard timelineItem.shouldShowSenderDetails,
|
||||
let avatarURL = timelineItem.sender.avatarURL,
|
||||
timelineItem.sender.avatar == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await mediaProvider.loadImageFromURL(avatarURL, avatarSize: .user(on: .timeline)) {
|
||||
case .success(let avatar):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? EventBasedTimelineItemProtocol else {
|
||||
return
|
||||
}
|
||||
|
||||
item.sender.avatar = avatar
|
||||
timelineItems[index] = item
|
||||
callbacks.send(.updatedTimelineItem(timelineItem.id))
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
#warning("This is here because sender profiles aren't working properly. Remove it entirely later")
|
||||
private func loadUserDisplayNameForTimelineItem(_ timelineItem: EventBasedTimelineItemProtocol) async {
|
||||
if timelineItem.shouldShowSenderDetails == false || timelineItem.sender.displayName != nil {
|
||||
|
||||
@@ -23,5 +23,4 @@ struct TimelineItemSender: Identifiable, Hashable {
|
||||
// Lazy loaded properties, displayName and avatarURL will be come lets.
|
||||
var displayName: String?
|
||||
var avatarURL: URL?
|
||||
var avatar: UIImage?
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
|
||||
var sender: TimelineItemSender
|
||||
|
||||
let source: MediaSourceProxy?
|
||||
var image: UIImage?
|
||||
var cachedFileURL: URL?
|
||||
|
||||
var width: CGFloat?
|
||||
|
||||
@@ -29,7 +29,6 @@ struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
|
||||
let duration: UInt64
|
||||
let source: MediaSourceProxy?
|
||||
let thumbnailSource: MediaSourceProxy?
|
||||
var image: UIImage?
|
||||
var cachedVideoURL: URL?
|
||||
|
||||
var width: CGFloat?
|
||||
|
||||
@@ -26,8 +26,7 @@ struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Ha
|
||||
|
||||
var sender: TimelineItemSender
|
||||
|
||||
let imageURL: URL?
|
||||
var image: UIImage?
|
||||
let imageURL: URL
|
||||
|
||||
var width: CGFloat?
|
||||
var height: CGFloat?
|
||||
|
||||
@@ -38,64 +38,61 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
||||
groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol? {
|
||||
var sender = eventItemProxy.sender
|
||||
if let senderAvatarURL = eventItemProxy.sender.avatarURL,
|
||||
let image = mediaProvider.imageFromURL(senderAvatarURL, avatarSize: .user(on: .timeline)) {
|
||||
sender.avatar = image
|
||||
}
|
||||
|
||||
let isOutgoing = eventItemProxy.isOwn
|
||||
|
||||
switch eventItemProxy.content.kind() {
|
||||
case .unableToDecrypt(let encryptedMessage):
|
||||
return buildEncryptedTimelineItem(eventItemProxy, encryptedMessage, sender, isOutgoing, groupState)
|
||||
return buildEncryptedTimelineItem(eventItemProxy, encryptedMessage, isOutgoing, groupState)
|
||||
case .redactedMessage:
|
||||
return buildRedactedTimelineItem(eventItemProxy, sender, isOutgoing, groupState)
|
||||
case .sticker(let body, let imageInfo, let url):
|
||||
return buildStickerTimelineItem(eventItemProxy, body, imageInfo, url, sender, isOutgoing, groupState)
|
||||
return buildRedactedTimelineItem(eventItemProxy, isOutgoing, groupState)
|
||||
case .sticker(let body, let imageInfo, let urlString):
|
||||
guard let url = URL(string: urlString) else {
|
||||
MXLog.error("Invalid sticker url string: \(urlString)")
|
||||
return buildUnsupportedTimelineItem(eventItemProxy, "m.sticker", "Invalid Sticker URL", isOutgoing, groupState)
|
||||
}
|
||||
|
||||
return buildStickerTimelineItem(eventItemProxy, body, imageInfo, url, isOutgoing, groupState)
|
||||
case .failedToParseMessageLike(let eventType, let error):
|
||||
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, sender, isOutgoing, groupState)
|
||||
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing, groupState)
|
||||
case .failedToParseState(let eventType, _, let error):
|
||||
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, sender, isOutgoing, groupState)
|
||||
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing, groupState)
|
||||
case .message:
|
||||
guard let messageContent = eventItemProxy.content.asMessage() else { fatalError("Invalid message timeline item: \(eventItemProxy)") }
|
||||
|
||||
switch messageContent.msgtype() {
|
||||
case .text(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildTextTimelineItemFromMessage(eventItemProxy, message, sender, isOutgoing, groupState)
|
||||
return buildTextTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
||||
case .image(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildImageTimelineItemFromMessage(eventItemProxy, message, sender, isOutgoing, groupState)
|
||||
return buildImageTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
||||
case .video(let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildVideoTimelineItemFromMessage(eventItemProxy, message, sender, isOutgoing, groupState)
|
||||
return buildVideoTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
||||
case .file(let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildFileTimelineItemFromMessage(eventItemProxy, message, sender, isOutgoing, groupState)
|
||||
return buildFileTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
||||
case .notice(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildNoticeTimelineItemFromMessage(eventItemProxy, message, sender, isOutgoing, groupState)
|
||||
return buildNoticeTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
||||
case .emote(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildEmoteTimelineItemFromMessage(eventItemProxy, message, sender, isOutgoing, groupState)
|
||||
return buildEmoteTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
||||
case .none:
|
||||
return buildFallbackTimelineItem(eventItemProxy, sender, isOutgoing, groupState)
|
||||
return buildFallbackTimelineItem(eventItemProxy, isOutgoing, groupState)
|
||||
}
|
||||
case .state(let stateKey, let content):
|
||||
return buildStateTimelineItemFor(eventItemProxy: eventItemProxy, state: content, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing)
|
||||
return buildStateTimelineItemFor(eventItemProxy: eventItemProxy, state: content, stateKey: stateKey, isOutgoing: isOutgoing)
|
||||
case .roomMembership(userId: let userID, change: let change):
|
||||
return buildStateMembershipChangeTimelineItemFor(eventItemProxy: eventItemProxy, member: userID, membershipChange: change, sender: sender, isOutgoing: isOutgoing)
|
||||
return buildStateMembershipChangeTimelineItemFor(eventItemProxy: eventItemProxy, member: userID, membershipChange: change, isOutgoing: isOutgoing)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Message Events
|
||||
|
||||
// swiftlint:disable:next function_parameter_count
|
||||
private func buildUnsupportedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ eventType: String,
|
||||
_ error: String,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
UnsupportedRoomTimelineItem(id: eventItemProxy.id,
|
||||
@@ -106,7 +103,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties())
|
||||
}
|
||||
|
||||
@@ -114,8 +111,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
private func buildStickerTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ body: String,
|
||||
_ imageInfo: ImageInfo,
|
||||
_ imageURLString: String,
|
||||
_ sender: TimelineItemSender,
|
||||
_ imageURL: URL,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
var aspectRatio: CGFloat?
|
||||
@@ -125,20 +121,14 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
aspectRatio = width / height
|
||||
}
|
||||
|
||||
var image: UIImage?
|
||||
if let url = URL(string: imageURLString) {
|
||||
image = mediaProvider.imageFromURL(url)
|
||||
}
|
||||
|
||||
return StickerRoomTimelineItem(id: eventItemProxy.id,
|
||||
text: body,
|
||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
imageURL: URL(string: imageURLString),
|
||||
image: image,
|
||||
sender: eventItemProxy.sender,
|
||||
imageURL: imageURL,
|
||||
width: width,
|
||||
height: height,
|
||||
aspectRatio: aspectRatio,
|
||||
@@ -149,7 +139,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ encryptedMessage: EncryptedMessage,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
var encryptionType = EncryptedRoomTimelineItem.EncryptionType.unknown
|
||||
@@ -169,12 +158,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties())
|
||||
}
|
||||
|
||||
private func buildRedactedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
RedactedRoomTimelineItem(id: eventItemProxy.id,
|
||||
@@ -183,12 +171,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties())
|
||||
}
|
||||
|
||||
private func buildFallbackTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
let attributedText = attributedStringBuilder.fromPlain(eventItemProxy.body)
|
||||
@@ -201,14 +188,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties(isEdited: eventItemProxy.content.asMessage()?.isEdited() ?? false,
|
||||
reactions: aggregateReactions(eventItemProxy.reactions)))
|
||||
}
|
||||
|
||||
private func buildTextTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ message: MessageTimelineItem<TextMessageContent>,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
@@ -221,7 +207,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus))
|
||||
@@ -229,7 +215,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
private func buildImageTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ message: MessageTimelineItem<ImageMessageContent>,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
var aspectRatio: CGFloat?
|
||||
@@ -244,9 +229,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
source: message.source,
|
||||
image: mediaProvider.imageFromSource(message.source),
|
||||
width: message.width,
|
||||
height: message.height,
|
||||
aspectRatio: aspectRatio,
|
||||
@@ -258,7 +242,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
private func buildVideoTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ message: MessageTimelineItem<VideoMessageContent>,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
var aspectRatio: CGFloat?
|
||||
@@ -273,11 +256,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
duration: message.duration,
|
||||
source: message.source,
|
||||
thumbnailSource: message.thumbnailSource,
|
||||
image: mediaProvider.imageFromSource(message.thumbnailSource),
|
||||
width: message.width,
|
||||
height: message.height,
|
||||
aspectRatio: aspectRatio,
|
||||
@@ -289,7 +271,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
private func buildFileTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ message: MessageTimelineItem<FileMessageContent>,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
FileRoomTimelineItem(id: message.id,
|
||||
@@ -298,7 +279,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
source: message.source,
|
||||
thumbnailSource: message.thumbnailSource,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
@@ -308,7 +289,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
private func buildNoticeTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ message: MessageTimelineItem<NoticeMessageContent>,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
@@ -321,7 +301,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus))
|
||||
@@ -329,10 +309,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
|
||||
private func buildEmoteTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||
_ message: MessageTimelineItem<EmoteMessageContent>,
|
||||
_ sender: TimelineItemSender,
|
||||
_ isOutgoing: Bool,
|
||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
||||
let name = sender.displayName ?? sender.id
|
||||
let name = eventItemProxy.sender.displayName ?? eventItemProxy.sender.id
|
||||
|
||||
var attributedText: AttributedString?
|
||||
if let htmlBody = message.htmlBody {
|
||||
@@ -350,7 +329,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
groupState: groupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: eventItemProxy.isEditable,
|
||||
sender: sender,
|
||||
sender: eventItemProxy.sender,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(eventItemProxy.reactions),
|
||||
deliveryStatus: eventItemProxy.deliveryStatus))
|
||||
@@ -368,28 +347,26 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
private func buildStateTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
||||
state: OtherState,
|
||||
stateKey: String,
|
||||
sender: TimelineItemSender,
|
||||
isOutgoing: Bool) -> RoomTimelineItemProtocol? {
|
||||
guard let text = stateEventStringBuilder.buildString(for: state, stateKey: stateKey, sender: sender, isOutgoing: isOutgoing) else { return nil }
|
||||
return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing)
|
||||
guard let text = stateEventStringBuilder.buildString(for: state, stateKey: stateKey, sender: eventItemProxy.sender, isOutgoing: isOutgoing) else { return nil }
|
||||
return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, isOutgoing: isOutgoing)
|
||||
}
|
||||
|
||||
private func buildStateMembershipChangeTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
||||
member: String,
|
||||
membershipChange: MembershipChange,
|
||||
sender: TimelineItemSender,
|
||||
isOutgoing: Bool) -> RoomTimelineItemProtocol? {
|
||||
guard let text = stateEventStringBuilder.buildString(for: membershipChange, member: member, sender: eventItemProxy.sender, isOutgoing: isOutgoing) else { return nil }
|
||||
return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, sender: sender, isOutgoing: isOutgoing)
|
||||
return buildStateTimelineItem(eventItemProxy: eventItemProxy, text: text, isOutgoing: isOutgoing)
|
||||
}
|
||||
|
||||
private func buildStateTimelineItem(eventItemProxy: EventTimelineItemProxy, text: String, sender: TimelineItemSender, isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||
private func buildStateTimelineItem(eventItemProxy: EventTimelineItemProxy, text: String, isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||
StateRoomTimelineItem(id: eventItemProxy.id,
|
||||
text: text,
|
||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||
groupState: .single,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: false,
|
||||
sender: sender)
|
||||
sender: eventItemProxy.sender)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,12 +190,12 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
|
||||
let userNotificationController = UserNotificationController(rootCoordinator: settingsNavigationStackCoordinator)
|
||||
|
||||
let parameters = SettingsCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator,
|
||||
userNotificationController: userNotificationController,
|
||||
userSession: userSession,
|
||||
bugReportService: bugReportService)
|
||||
let settingsCoordinator = SettingsCoordinator(parameters: parameters)
|
||||
settingsCoordinator.callback = { [weak self] action in
|
||||
let parameters = SettingsScreenCoordinatorParameters(navigationStackCoordinator: settingsNavigationStackCoordinator,
|
||||
userNotificationController: userNotificationController,
|
||||
userSession: userSession,
|
||||
bugReportService: bugReportService)
|
||||
let settingsScreenCoordinator = SettingsScreenCoordinator(parameters: parameters)
|
||||
settingsScreenCoordinator.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
@@ -206,7 +206,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
settingsNavigationStackCoordinator.setRootCoordinator(settingsCoordinator)
|
||||
settingsNavigationStackCoordinator.setRootCoordinator(settingsScreenCoordinator)
|
||||
|
||||
navigationSplitCoordinator.setSheetCoordinator(userNotificationController) { [weak self] in
|
||||
self?.stateMachine.processEvent(.dismissedSettingsScreen)
|
||||
|
||||
@@ -96,7 +96,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
imageCache.memoryStorage.config.keepWhenEnteringBackground = true
|
||||
|
||||
return UserSession(clientProxy: clientProxy,
|
||||
mediaProvider: MediaProvider(mediaProxy: clientProxy,
|
||||
mediaProvider: MediaProvider(mediaLoader: clientProxy,
|
||||
imageCache: imageCache,
|
||||
fileCache: FileCache.default,
|
||||
backgroundTaskService: backgroundTaskService))
|
||||
|
||||
@@ -118,11 +118,11 @@ class MockScreen: Identifiable {
|
||||
return navigationStackCoordinator
|
||||
case .settings:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let coordinator = SettingsCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||
userNotificationController: MockUserNotificationController(),
|
||||
userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"),
|
||||
mediaProvider: MockMediaProvider()),
|
||||
bugReportService: MockBugReportService()))
|
||||
let coordinator = SettingsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||
userNotificationController: MockUserNotificationController(),
|
||||
userSession: MockUserSession(clientProxy: MockClientProxy(userID: "@mock:client.com"),
|
||||
mediaProvider: MockMediaProvider()),
|
||||
bugReportService: MockBugReportService()))
|
||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
return navigationStackCoordinator
|
||||
case .bugReport:
|
||||
|
||||
@@ -125,15 +125,15 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
|
||||
|
||||
let client = try builder.build()
|
||||
try client.restoreSession(session: credentials.restorationToken.session)
|
||||
|
||||
|
||||
MXLog.info("\(tag) creating media provider")
|
||||
|
||||
return MediaProvider(mediaProxy: MediaProxy(client: client),
|
||||
|
||||
return MediaProvider(mediaLoader: MediaLoader(client: client),
|
||||
imageCache: .onlyOnDisk,
|
||||
fileCache: FileCache.default,
|
||||
backgroundTaskService: nil)
|
||||
}
|
||||
|
||||
|
||||
private func notify() {
|
||||
MXLog.info("\(tag) notify")
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ targets:
|
||||
sources:
|
||||
- path: ../Sources
|
||||
- path: ../SupportingFiles
|
||||
- path: ../../ElementX/Sources/Generated
|
||||
- path: ../../ElementX/Sources/Services/Timeline/TimelineItemProxy.swift
|
||||
- path: ../../ElementX/Sources/Services/Timeline/TimelineItemSender.swift
|
||||
- path: ../../ElementX/Sources/Services/Keychain/KeychainControllerProtocol.swift
|
||||
@@ -75,11 +76,7 @@ targets:
|
||||
- path: ../../ElementX/Sources/Services/Notification/Proxy/NotificationServiceProxy.swift
|
||||
- path: ../../ElementX/Sources/Services/Notification/Proxy/NotificationItemProxy.swift
|
||||
- path: ../../ElementX/Sources/Services/Notification/NotificationConstants.swift
|
||||
- path: ../../ElementX/Sources/Services/Media/MediaProxyProtocol.swift
|
||||
- path: ../../ElementX/Sources/Services/Media/MediaProxy.swift
|
||||
- path: ../../ElementX/Sources/Services/Media/MediaProviderProtocol.swift
|
||||
- path: ../../ElementX/Sources/Services/Media/MediaProvider.swift
|
||||
- path: ../../ElementX/Sources/Services/Media/MediaSourceProxy.swift
|
||||
- path: ../../ElementX/Sources/Services/Media
|
||||
- path: ../../ElementX/Sources/Services/Background/BackgroundTaskServiceProtocol.swift
|
||||
- path: ../../ElementX/Sources/Services/Background/BackgroundTaskProtocol.swift
|
||||
- path: ../../ElementX/Sources/Services/Cache/FileCache.swift
|
||||
|
||||
@@ -24,7 +24,7 @@ class RoomScreenUITests: XCTestCase {
|
||||
app.goToScreenWithIdentifier(.roomPlainNoAvatar)
|
||||
|
||||
XCTAssert(app.staticTexts["roomNameLabel"].exists)
|
||||
XCTAssert(app.staticTexts["roomAvatarPlaceholderImage"].exists)
|
||||
XCTAssert(app.staticTexts["roomAvatarImage"].exists)
|
||||
|
||||
app.assertScreenshot(.roomPlainNoAvatar)
|
||||
}
|
||||
|
||||
115
UnitTests/Sources/MediaProvider/MediaLoaderTests.swift
Normal file
115
UnitTests/Sources/MediaProvider/MediaLoaderTests.swift
Normal 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.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import XCTest
|
||||
|
||||
final class MediaLoaderTests: XCTestCase {
|
||||
func testMediaRequestCoalescing() async {
|
||||
let mediaLoadingClient = MockMediaLoadingClient()
|
||||
let mediaLoader = MediaLoader(client: mediaLoadingClient)
|
||||
|
||||
let mediaSource = MediaSourceProxy(url: URL.documentsDirectory)
|
||||
|
||||
do {
|
||||
for _ in 1...10 {
|
||||
_ = try await mediaLoader.loadMediaContentForSource(mediaSource)
|
||||
}
|
||||
|
||||
XCTAssertEqual(mediaLoadingClient.numberOfInvocations, 10)
|
||||
} catch {
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
func testMediaThumbnailRequestCoalescing() async {
|
||||
let mediaLoadingClient = MockMediaLoadingClient()
|
||||
let mediaLoader = MediaLoader(client: mediaLoadingClient)
|
||||
|
||||
let mediaSource = MediaSourceProxy(url: URL.documentsDirectory)
|
||||
|
||||
do {
|
||||
for _ in 1...10 {
|
||||
_ = try await mediaLoader.loadMediaThumbnailForSource(mediaSource, width: 100, height: 100)
|
||||
}
|
||||
|
||||
XCTAssertEqual(mediaLoadingClient.numberOfInvocations, 10)
|
||||
} catch {
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MockMediaLoadingClient: ClientProtocol {
|
||||
private(set) var numberOfInvocations = 0
|
||||
|
||||
func getMediaContent(source: MatrixRustSDK.MediaSource) throws -> [UInt8] {
|
||||
numberOfInvocations += 1
|
||||
return []
|
||||
}
|
||||
|
||||
func getMediaThumbnail(source: MatrixRustSDK.MediaSource, width: UInt64, height: UInt64) throws -> [UInt8] {
|
||||
numberOfInvocations += 1
|
||||
return []
|
||||
}
|
||||
|
||||
// MARK: - Not implemented
|
||||
|
||||
func setDelegate(delegate: MatrixRustSDK.ClientDelegate?) { }
|
||||
|
||||
func login(username: String, password: String, initialDeviceName: String?, deviceId: String?) throws { }
|
||||
|
||||
func restoreSession(session: MatrixRustSDK.Session) throws { }
|
||||
|
||||
func session() throws -> MatrixRustSDK.Session { fatalError() }
|
||||
|
||||
func userId() throws -> String { fatalError() }
|
||||
|
||||
func displayName() throws -> String { fatalError() }
|
||||
|
||||
func setDisplayName(name: String) throws { }
|
||||
|
||||
func avatarUrl() throws -> String { fatalError() }
|
||||
|
||||
func deviceId() throws -> String { fatalError() }
|
||||
|
||||
func accountData(eventType: String) throws -> String? { fatalError() }
|
||||
|
||||
func setAccountData(eventType: String, content: String) throws { fatalError() }
|
||||
|
||||
func uploadMedia(mimeType: String, content: [UInt8]) throws -> String { fatalError() }
|
||||
|
||||
func getSessionVerificationController() throws -> MatrixRustSDK.SessionVerificationController { fatalError() }
|
||||
|
||||
func fullSlidingSync() throws -> MatrixRustSDK.SlidingSync { fatalError() }
|
||||
|
||||
func logout() throws { }
|
||||
|
||||
func hasFirstSynced() -> Bool { fatalError() }
|
||||
|
||||
func homeserver() -> String { fatalError() }
|
||||
|
||||
func isSoftLogout() -> Bool { fatalError() }
|
||||
|
||||
func isSyncing() -> Bool { fatalError() }
|
||||
|
||||
func rooms() -> [MatrixRustSDK.Room] { fatalError() }
|
||||
|
||||
func slidingSync() -> MatrixRustSDK.SlidingSyncBuilder { fatalError() }
|
||||
|
||||
func startSync(timelineLimit: UInt16?) { }
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import XCTest
|
||||
|
||||
@MainActor
|
||||
final class MediaProviderTests: XCTestCase {
|
||||
private let mediaProxy = MockMediaProxy()
|
||||
private let mediaLoader = MockMediaLoader()
|
||||
private let fileCache = MockFileCache()
|
||||
private var imageCache: MockImageCache!
|
||||
private var backgroundTaskService = MockBackgroundTaskService()
|
||||
@@ -29,14 +29,14 @@ final class MediaProviderTests: XCTestCase {
|
||||
|
||||
override func setUp() {
|
||||
imageCache = MockImageCache(name: "Test")
|
||||
mediaProvider = MediaProvider(mediaProxy: mediaProxy,
|
||||
mediaProvider = MediaProvider(mediaLoader: mediaLoader,
|
||||
imageCache: imageCache,
|
||||
fileCache: fileCache,
|
||||
backgroundTaskService: backgroundTaskService)
|
||||
}
|
||||
|
||||
func test_whenImageFromSourceWithSourceNil_nilReturned() throws {
|
||||
let image = mediaProvider.imageFromSource(nil, avatarSize: .room(on: .timeline))
|
||||
let image = mediaProvider.imageFromSource(nil, size: AvatarSize.room(on: .timeline).scaledSize)
|
||||
XCTAssertNil(image)
|
||||
}
|
||||
|
||||
@@ -46,17 +46,18 @@ final class MediaProviderTests: XCTestCase {
|
||||
let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}"
|
||||
let imageForKey = UIImage()
|
||||
imageCache.retrievedImagesInMemory[key] = imageForKey
|
||||
let image = mediaProvider.imageFromSource(MediaSourceProxy(url: url), avatarSize: avatarSize)
|
||||
let image = mediaProvider.imageFromSource(MediaSourceProxy(url: url), size: avatarSize.scaledSize)
|
||||
XCTAssertEqual(image, imageForKey)
|
||||
}
|
||||
|
||||
func test_whenImageFromSourceWithSourceNotNilAndImageNotCached_nilReturned() throws {
|
||||
let image = mediaProvider.imageFromSource(MediaSourceProxy(url: URL.picturesDirectory), avatarSize: .room(on: .timeline))
|
||||
let image = mediaProvider.imageFromSource(MediaSourceProxy(url: URL.picturesDirectory),
|
||||
size: AvatarSize.room(on: .timeline).scaledSize)
|
||||
XCTAssertNil(image)
|
||||
}
|
||||
|
||||
func test_whenImageFromURLStringWithURLStringNil_nilReturned() throws {
|
||||
let image = mediaProvider.imageFromURL(nil, avatarSize: .room(on: .timeline))
|
||||
let image = mediaProvider.imageFromURL(nil, size: AvatarSize.room(on: .timeline).scaledSize)
|
||||
XCTAssertNil(image)
|
||||
}
|
||||
|
||||
@@ -66,12 +67,12 @@ final class MediaProviderTests: XCTestCase {
|
||||
let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}"
|
||||
let imageForKey = UIImage()
|
||||
imageCache.retrievedImagesInMemory[key] = imageForKey
|
||||
let image = mediaProvider.imageFromURL(url, avatarSize: avatarSize)
|
||||
let image = mediaProvider.imageFromURL(url, size: avatarSize.scaledSize)
|
||||
XCTAssertEqual(image, imageForKey)
|
||||
}
|
||||
|
||||
func test_whenImageFromURLStringWithURLStringNotNilAndImageNotCached_nilReturned() throws {
|
||||
let image = mediaProvider.imageFromURL(URL.picturesDirectory, avatarSize: .room(on: .timeline))
|
||||
let image = mediaProvider.imageFromURL(URL.picturesDirectory, size: AvatarSize.room(on: .timeline).scaledSize)
|
||||
XCTAssertNil(image)
|
||||
}
|
||||
|
||||
@@ -81,7 +82,7 @@ final class MediaProviderTests: XCTestCase {
|
||||
let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}"
|
||||
let imageForKey = UIImage()
|
||||
imageCache.retrievedImagesInMemory[key] = imageForKey
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url), avatarSize: avatarSize)
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url), size: avatarSize.scaledSize)
|
||||
XCTAssertEqual(Result.success(imageForKey), result)
|
||||
}
|
||||
|
||||
@@ -91,15 +92,15 @@ final class MediaProviderTests: XCTestCase {
|
||||
let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}"
|
||||
let imageForKey = UIImage()
|
||||
imageCache.retrievedImages[key] = imageForKey
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url), avatarSize: avatarSize)
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url), size: avatarSize.scaledSize)
|
||||
XCTAssertEqual(Result.success(imageForKey), result)
|
||||
}
|
||||
|
||||
func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFails_imageThumbnailIsLoaded() async throws {
|
||||
let avatarSize = AvatarSize.room(on: .timeline)
|
||||
let expectedImage = try loadTestImage()
|
||||
mediaProxy.mediaThumbnailData = expectedImage.pngData()
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory), avatarSize: avatarSize)
|
||||
mediaLoader.mediaThumbnailData = expectedImage.pngData()
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory), size: avatarSize.scaledSize)
|
||||
switch result {
|
||||
case .success(let image):
|
||||
XCTAssertEqual(image.pngData(), expectedImage.pngData())
|
||||
@@ -113,16 +114,16 @@ final class MediaProviderTests: XCTestCase {
|
||||
let url = URL.picturesDirectory
|
||||
let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}"
|
||||
let expectedImage = try loadTestImage()
|
||||
mediaProxy.mediaThumbnailData = expectedImage.pngData()
|
||||
_ = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url), avatarSize: avatarSize)
|
||||
mediaLoader.mediaThumbnailData = expectedImage.pngData()
|
||||
_ = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url), size: avatarSize.scaledSize)
|
||||
let storedImage = try XCTUnwrap(imageCache.storedImages[key])
|
||||
XCTAssertEqual(expectedImage.pngData(), storedImage.pngData())
|
||||
}
|
||||
|
||||
func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndNoAvatarSize_imageContentIsLoaded() async throws {
|
||||
let expectedImage = try loadTestImage()
|
||||
mediaProxy.mediaContentData = expectedImage.pngData()
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory), avatarSize: nil)
|
||||
mediaLoader.mediaContentData = expectedImage.pngData()
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory), size: nil)
|
||||
switch result {
|
||||
case .success(let image):
|
||||
XCTAssertEqual(image.pngData(), expectedImage.pngData())
|
||||
@@ -132,7 +133,8 @@ final class MediaProviderTests: XCTestCase {
|
||||
}
|
||||
|
||||
func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndLoadImageThumbnailFails_errorIsThrown() async throws {
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory), avatarSize: AvatarSize.room(on: .timeline))
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory),
|
||||
size: AvatarSize.room(on: .timeline).scaledSize)
|
||||
switch result {
|
||||
case .success:
|
||||
XCTFail("Should fail")
|
||||
@@ -142,7 +144,7 @@ final class MediaProviderTests: XCTestCase {
|
||||
}
|
||||
|
||||
func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndNoAvatarSizeAndLoadImageContentFails_errorIsThrown() async throws {
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory), avatarSize: nil)
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory), size: nil)
|
||||
switch result {
|
||||
case .success:
|
||||
XCTFail("Should fail")
|
||||
@@ -152,8 +154,9 @@ final class MediaProviderTests: XCTestCase {
|
||||
}
|
||||
|
||||
func test_whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndImageThumbnailIsLoadedWithCorruptedData_errorIsThrown() async throws {
|
||||
mediaProxy.mediaThumbnailData = Data()
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory), avatarSize: AvatarSize.room(on: .timeline))
|
||||
mediaLoader.mediaThumbnailData = Data()
|
||||
let result = await mediaProvider.loadImageFromSource(MediaSourceProxy(url: URL.picturesDirectory),
|
||||
size: AvatarSize.room(on: .timeline).scaledSize)
|
||||
switch result {
|
||||
case .success:
|
||||
XCTFail("Should fail")
|
||||
@@ -187,31 +190,31 @@ final class MediaProviderTests: XCTestCase {
|
||||
func test_whenLoadFileFromSourceAndNoFileFromSourceExists_mediaLoadedFromSource() async throws {
|
||||
let expectedURL = URL(filePath: "/some/file/path")
|
||||
let expectedResult: Result<URL, MediaProviderError> = .success(expectedURL)
|
||||
mediaProxy.mediaContentData = try loadTestImage().pngData()
|
||||
mediaLoader.mediaContentData = try loadTestImage().pngData()
|
||||
fileCache.storeURLToReturn = expectedURL
|
||||
let result = await mediaProvider.loadFileFromSource(MediaSourceProxy(url: URL(staticString: "test/test1")), fileExtension: "png")
|
||||
XCTAssertEqual(result, expectedResult)
|
||||
XCTAssertEqual(mediaProxy.mediaContentData, fileCache.storedData)
|
||||
XCTAssertEqual(mediaLoader.mediaContentData, fileCache.storedData)
|
||||
XCTAssertEqual("test1", fileCache.storedFileKey)
|
||||
XCTAssertEqual("png", fileCache.storedFileExtension)
|
||||
}
|
||||
|
||||
func test_whenLoadFileFromSourceAndNoFileFromSourceExistsAndLoadContentSourceFails_failureIsReturned() async throws {
|
||||
let expectedResult: Result<URL, MediaProviderError> = .failure(.failedRetrievingImage)
|
||||
mediaProxy.mediaContentData = nil
|
||||
mediaLoader.mediaContentData = nil
|
||||
let result = await mediaProvider.loadFileFromSource(MediaSourceProxy(url: URL(staticString: "test/test1")), fileExtension: "png")
|
||||
XCTAssertEqual(result, expectedResult)
|
||||
}
|
||||
|
||||
func test_whenLoadFileFromSourceAndNoFileFromSourceExistsAndStoreDataFails_failureIsReturned() async throws {
|
||||
let expectedResult: Result<URL, MediaProviderError> = .failure(.failedRetrievingImage)
|
||||
mediaProxy.mediaContentData = try loadTestImage().pngData()
|
||||
mediaLoader.mediaContentData = try loadTestImage().pngData()
|
||||
let result = await mediaProvider.loadFileFromSource(MediaSourceProxy(url: URL(staticString: "test/test1")), fileExtension: "png")
|
||||
XCTAssertEqual(result, expectedResult)
|
||||
}
|
||||
|
||||
func test_whenFileFromURLStringAndURLIsNil_nilIsReturned() async throws {
|
||||
mediaProxy.mediaContentData = try loadTestImage().pngData()
|
||||
mediaLoader.mediaContentData = try loadTestImage().pngData()
|
||||
let url = mediaProvider.fileFromURL(nil, fileExtension: "png")
|
||||
XCTAssertNil(url)
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
@testable import ElementX
|
||||
import Foundation
|
||||
|
||||
enum MockMediaProxyError: Error {
|
||||
enum MockMediaLoaderError: Error {
|
||||
case someError
|
||||
}
|
||||
|
||||
class MockMediaProxy: MediaProxyProtocol {
|
||||
class MockMediaLoader: MediaLoaderProtocol {
|
||||
var mediaContentData: Data?
|
||||
var mediaThumbnailData: Data?
|
||||
|
||||
@@ -32,7 +32,7 @@ class MockMediaProxy: MediaProxyProtocol {
|
||||
if let mediaContentData {
|
||||
return mediaContentData
|
||||
} else {
|
||||
throw MockMediaProxyError.someError
|
||||
throw MockMediaLoaderError.someError
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class MockMediaProxy: MediaProxyProtocol {
|
||||
if let mediaThumbnailData {
|
||||
return mediaThumbnailData
|
||||
} else {
|
||||
throw MockMediaProxyError.someError
|
||||
throw MockMediaLoaderError.someError
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,14 @@ import XCTest
|
||||
@testable import ElementX
|
||||
|
||||
@MainActor
|
||||
class SettingsViewModelTests: XCTestCase {
|
||||
var viewModel: SettingsViewModelProtocol!
|
||||
var context: SettingsViewModelType.Context!
|
||||
class SettingsScreenViewModelTests: XCTestCase {
|
||||
var viewModel: SettingsScreenViewModelProtocol!
|
||||
var context: SettingsScreenViewModelType.Context!
|
||||
|
||||
@MainActor override func setUpWithError() throws {
|
||||
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: ""),
|
||||
mediaProvider: MockMediaProvider())
|
||||
viewModel = SettingsViewModel(withUserSession: userSession)
|
||||
viewModel = SettingsScreenViewModel(withUserSession: userSession)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user