mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-06 12:28:29 +00:00
Content Scanning View (#5831)
* implement a MediaView to delegate at the view level the usage of the content scanner service, to determine how to render the media. * fix for a small design mismatch * fix for greedy error state * updated naming * just making some screenshot that have been affected by a string change on Localazy * pr suggestions + updated strings
This commit is contained in:
@@ -99,6 +99,10 @@ extension AccessibilityTests {
|
|||||||
try await performAccessibilityAudit(named: "ComposerToolbar_Previews")
|
try await performAccessibilityAudit(named: "ComposerToolbar_Previews")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testContentScanningFailureView() async throws {
|
||||||
|
try await performAccessibilityAudit(named: "ContentScanningFailureView_Previews")
|
||||||
|
}
|
||||||
|
|
||||||
func testCopyTextButton() async throws {
|
func testCopyTextButton() async throws {
|
||||||
try await performAccessibilityAudit(named: "CopyTextButton_Previews")
|
try await performAccessibilityAudit(named: "CopyTextButton_Previews")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,7 @@
|
|||||||
15913A5B07118C1268A840E4 /* RoomSummaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */; };
|
15913A5B07118C1268A840E4 /* RoomSummaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */; };
|
||||||
1621BF6316FFFEF5AE067C77 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; };
|
1621BF6316FFFEF5AE067C77 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; };
|
||||||
1653275750CE11F5CE94DDFD /* ReadReceiptsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8063E65441E771200108C558 /* ReadReceiptsSummaryView.swift */; };
|
1653275750CE11F5CE94DDFD /* ReadReceiptsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8063E65441E771200108C558 /* ReadReceiptsSummaryView.swift */; };
|
||||||
|
166D52EC67BD7586050BFB8D /* ContentScannerServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC446234C8E5031F36F1BBE2 /* ContentScannerServiceMock.swift */; };
|
||||||
167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */; };
|
167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */; };
|
||||||
167D5024DB9D44197AEA0507 /* HomeScreenNewSoundBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD5480F03306234FC086E93B /* HomeScreenNewSoundBanner.swift */; };
|
167D5024DB9D44197AEA0507 /* HomeScreenNewSoundBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD5480F03306234FC086E93B /* HomeScreenNewSoundBanner.swift */; };
|
||||||
16A1F6C703305FCAF4E14EC6 /* TimelineProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A8AA0DFA06012A9DAB951E /* TimelineProxyMock.swift */; };
|
16A1F6C703305FCAF4E14EC6 /* TimelineProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A8AA0DFA06012A9DAB951E /* TimelineProxyMock.swift */; };
|
||||||
@@ -630,6 +631,7 @@
|
|||||||
67FEA7157FA1B39DFC533C78 /* LeaveSpaceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E5A65B5A000E7237AC61C67 /* LeaveSpaceViewModel.swift */; };
|
67FEA7157FA1B39DFC533C78 /* LeaveSpaceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E5A65B5A000E7237AC61C67 /* LeaveSpaceViewModel.swift */; };
|
||||||
6817EAD73DC1FFD8B943B5B9 /* HomeScreenRoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73587C2E3CF5998361AE516 /* HomeScreenRoomTests.swift */; };
|
6817EAD73DC1FFD8B943B5B9 /* HomeScreenRoomTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B73587C2E3CF5998361AE516 /* HomeScreenRoomTests.swift */; };
|
||||||
68184EF36396424FE19A727D /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; };
|
68184EF36396424FE19A727D /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; };
|
||||||
|
681FA9A37E82A14EDFB22D56 /* ContentScanningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8794B3D86D7D444B9B6FF7B /* ContentScanningView.swift */; };
|
||||||
6832733838C57A7D3FE8FEB5 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 50009897F60FAE7D63EF5E5B /* Kingfisher */; };
|
6832733838C57A7D3FE8FEB5 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 50009897F60FAE7D63EF5E5B /* Kingfisher */; };
|
||||||
6851B077B4C913CC12DB6E77 /* AppLockFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */; };
|
6851B077B4C913CC12DB6E77 /* AppLockFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE93F0CBF0D96B77111C413 /* AppLockFlowCoordinator.swift */; };
|
||||||
6885C3D26FC1026E07408D3C /* RoomListActivityVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5DA892E8643240C7BC41900 /* RoomListActivityVisibility.swift */; };
|
6885C3D26FC1026E07408D3C /* RoomListActivityVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5DA892E8643240C7BC41900 /* RoomListActivityVisibility.swift */; };
|
||||||
@@ -969,6 +971,7 @@
|
|||||||
9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; };
|
9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; };
|
||||||
9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; };
|
9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; };
|
||||||
9DBF6524DFD8143A4D6A17F0 /* CreateRoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D432771B389EC486E7F90C6 /* CreateRoomScreenModels.swift */; };
|
9DBF6524DFD8143A4D6A17F0 /* CreateRoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D432771B389EC486E7F90C6 /* CreateRoomScreenModels.swift */; };
|
||||||
|
9DD0DFFEFCA51AE241B637D1 /* ContentScanningFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8442B74248E2839BA6FFF0 /* ContentScanningFailureView.swift */; };
|
||||||
9DD5AA10E85137140FEA86A3 /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; };
|
9DD5AA10E85137140FEA86A3 /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; };
|
||||||
9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */; };
|
9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */; };
|
||||||
9DE801D278AC34737467F937 /* VoiceMessageMediaManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 889DEDD63C68ABDA8AD29812 /* VoiceMessageMediaManagerProtocol.swift */; };
|
9DE801D278AC34737467F937 /* VoiceMessageMediaManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 889DEDD63C68ABDA8AD29812 /* VoiceMessageMediaManagerProtocol.swift */; };
|
||||||
@@ -2174,6 +2177,7 @@
|
|||||||
5A37E2FACFD041CE466223CD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
5A37E2FACFD041CE466223CD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = "<group>"; };
|
5A43964330459965AF048A8C /* LoginScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
5A70B03471F6027C90EE868C /* RoomMembersFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersFlowCoordinator.swift; sourceTree = "<group>"; };
|
5A70B03471F6027C90EE868C /* RoomMembersFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
5A8442B74248E2839BA6FFF0 /* ContentScanningFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentScanningFailureView.swift; sourceTree = "<group>"; };
|
||||||
5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = "<group>"; };
|
5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
5B2A2B168C8DC14A1A9D1C64 /* NotificationToneManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationToneManagerProtocol.swift; sourceTree = "<group>"; };
|
5B2A2B168C8DC14A1A9D1C64 /* NotificationToneManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationToneManagerProtocol.swift; sourceTree = "<group>"; };
|
||||||
5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = "<group>"; };
|
5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
@@ -2799,6 +2803,7 @@
|
|||||||
C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomList.swift; sourceTree = "<group>"; };
|
C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomList.swift; sourceTree = "<group>"; };
|
||||||
C79C2C6E7F66646D1D254927 /* RoomLiveLocationServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomLiveLocationServiceMock.swift; sourceTree = "<group>"; };
|
C79C2C6E7F66646D1D254927 /* RoomLiveLocationServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomLiveLocationServiceMock.swift; sourceTree = "<group>"; };
|
||||||
C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = "<group>"; };
|
C830A64609CBD152F06E0457 /* NotificationConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationConstants.swift; sourceTree = "<group>"; };
|
||||||
|
C8794B3D86D7D444B9B6FF7B /* ContentScanningView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentScanningView.swift; sourceTree = "<group>"; };
|
||||||
C90514BE9B8ACCBCF0AD2489 /* ComposerToolbarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModel.swift; sourceTree = "<group>"; };
|
C90514BE9B8ACCBCF0AD2489 /* ComposerToolbarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModel.swift; sourceTree = "<group>"; };
|
||||||
C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
C95ADE8D9527523572532219 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hu; path = hu.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenRoomCell.swift; sourceTree = "<group>"; };
|
C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenRoomCell.swift; sourceTree = "<group>"; };
|
||||||
@@ -2993,6 +2998,7 @@
|
|||||||
EBD21AF0131AA38FF9534FAD /* EditRoomAddressScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreenModels.swift; sourceTree = "<group>"; };
|
EBD21AF0131AA38FF9534FAD /* EditRoomAddressScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreenModels.swift; sourceTree = "<group>"; };
|
||||||
EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
EBE5502760CF6CA2D7201883 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
EBEB8D9F4940E161B18FE4BC /* UITestsNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsNotificationCenter.swift; sourceTree = "<group>"; };
|
EBEB8D9F4940E161B18FE4BC /* UITestsNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsNotificationCenter.swift; sourceTree = "<group>"; };
|
||||||
|
EC446234C8E5031F36F1BBE2 /* ContentScannerServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentScannerServiceMock.swift; sourceTree = "<group>"; };
|
||||||
EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModelTests.swift; sourceTree = "<group>"; };
|
EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
ECB836DD8BE31931F51B8AC9 /* EncryptionSettingsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionSettingsFlowCoordinator.swift; sourceTree = "<group>"; };
|
ECB836DD8BE31931F51B8AC9 /* EncryptionSettingsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionSettingsFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||||
ECBBCD7444C2B515D02EC0E8 /* IdentityConfirmationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenViewModelTests.swift; sourceTree = "<group>"; };
|
ECBBCD7444C2B515D02EC0E8 /* IdentityConfirmationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -3839,6 +3845,7 @@
|
|||||||
E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */,
|
E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */,
|
||||||
C723327DC4A3093CD9675B27 /* CLLocationManagerMock.swift */,
|
C723327DC4A3093CD9675B27 /* CLLocationManagerMock.swift */,
|
||||||
4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */,
|
4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */,
|
||||||
|
EC446234C8E5031F36F1BBE2 /* ContentScannerServiceMock.swift */,
|
||||||
86E1BAA7232081635662A83F /* CXProviderMock.swift */,
|
86E1BAA7232081635662A83F /* CXProviderMock.swift */,
|
||||||
E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */,
|
E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */,
|
||||||
3A21027F05874B1BCC3E452B /* InvitedRoomProxyMock.swift */,
|
3A21027F05874B1BCC3E452B /* InvitedRoomProxyMock.swift */,
|
||||||
@@ -3901,6 +3908,7 @@
|
|||||||
C1FA515B3B0D61EF1E907D2D /* BadgeView.swift */,
|
C1FA515B3B0D61EF1E907D2D /* BadgeView.swift */,
|
||||||
07934EF08BB39353E4A94272 /* BlurEffectView.swift */,
|
07934EF08BB39353E4A94272 /* BlurEffectView.swift */,
|
||||||
FEC4B431B0117BDEE697DB4A /* ComposerDisabledView.swift */,
|
FEC4B431B0117BDEE697DB4A /* ComposerDisabledView.swift */,
|
||||||
|
C8794B3D86D7D444B9B6FF7B /* ContentScanningView.swift */,
|
||||||
B682FE2C44C5E163E7023B05 /* CopyTextButton.swift */,
|
B682FE2C44C5E163E7023B05 /* CopyTextButton.swift */,
|
||||||
89D7846847BA9D2C5346ED9C /* EditAvatarButtonStyle.swift */,
|
89D7846847BA9D2C5346ED9C /* EditAvatarButtonStyle.swift */,
|
||||||
E2776E63E02719B20758EB78 /* EditRoomAddressListRow.swift */,
|
E2776E63E02719B20758EB78 /* EditRoomAddressListRow.swift */,
|
||||||
@@ -6516,6 +6524,7 @@
|
|||||||
664ABD745A746C45CB842158 /* CallInviteRoomTimelineView.swift */,
|
664ABD745A746C45CB842158 /* CallInviteRoomTimelineView.swift */,
|
||||||
AD6E082B0507FB28F966516A /* CallNotificationRoomTimelineView.swift */,
|
AD6E082B0507FB28F966516A /* CallNotificationRoomTimelineView.swift */,
|
||||||
BFBF273BC2BFB9F3EEFA988B /* CollapsibleRoomTimelineView.swift */,
|
BFBF273BC2BFB9F3EEFA988B /* CollapsibleRoomTimelineView.swift */,
|
||||||
|
5A8442B74248E2839BA6FFF0 /* ContentScanningFailureView.swift */,
|
||||||
44ABA63DBE7F76C58260B43B /* EmoteRoomTimelineView.swift */,
|
44ABA63DBE7F76C58260B43B /* EmoteRoomTimelineView.swift */,
|
||||||
2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */,
|
2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */,
|
||||||
4F75EF13F49DD2204E760910 /* FileRoomTimelineView.swift */,
|
4F75EF13F49DD2204E760910 /* FileRoomTimelineView.swift */,
|
||||||
@@ -8382,7 +8391,10 @@
|
|||||||
9ED768A43A2A302F9812C5DF /* ContentScannerProxy.swift in Sources */,
|
9ED768A43A2A302F9812C5DF /* ContentScannerProxy.swift in Sources */,
|
||||||
32BFB9B4D2EC46196537B72F /* ContentScannerProxyProtocol.swift in Sources */,
|
32BFB9B4D2EC46196537B72F /* ContentScannerProxyProtocol.swift in Sources */,
|
||||||
E52F1E00FBAABA703DA510F9 /* ContentScannerService.swift in Sources */,
|
E52F1E00FBAABA703DA510F9 /* ContentScannerService.swift in Sources */,
|
||||||
|
166D52EC67BD7586050BFB8D /* ContentScannerServiceMock.swift in Sources */,
|
||||||
E8F88136C8E69D9E69621E19 /* ContentScannerServiceProtocol.swift in Sources */,
|
E8F88136C8E69D9E69621E19 /* ContentScannerServiceProtocol.swift in Sources */,
|
||||||
|
9DD0DFFEFCA51AE241B637D1 /* ContentScanningFailureView.swift in Sources */,
|
||||||
|
681FA9A37E82A14EDFB22D56 /* ContentScanningView.swift in Sources */,
|
||||||
73DBE886625AF56FF08D7F76 /* CoordinateAnimator.swift in Sources */,
|
73DBE886625AF56FF08D7F76 /* CoordinateAnimator.swift in Sources */,
|
||||||
C3522917C0C367C403429EEC /* CoordinatorProtocol.swift in Sources */,
|
C3522917C0C367C403429EEC /* CoordinatorProtocol.swift in Sources */,
|
||||||
633501761094E09DFBEBFFAD /* CopyTextButton.swift in Sources */,
|
633501761094E09DFBEBFFAD /* CopyTextButton.swift in Sources */,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"a11y_add_reaction" = "Add reaction: %1$@";
|
"a11y_add_reaction" = "Add reaction: %1$@";
|
||||||
"a11y_address" = "Address";
|
"a11y_address" = "Address";
|
||||||
"a11y_avatar" = "Avatar";
|
"a11y_avatar" = "Avatar";
|
||||||
|
"a11y_clear_search_field" = "Clear search field";
|
||||||
"a11y_collapse_message_text_field" = "Minimize message text field";
|
"a11y_collapse_message_text_field" = "Minimize message text field";
|
||||||
"a11y_create_poll_votes_allowed_decrease" = "Decrease votes allowed per person";
|
"a11y_create_poll_votes_allowed_decrease" = "Decrease votes allowed per person";
|
||||||
"a11y_create_poll_votes_allowed_increase" = "Increase votes allowed per person";
|
"a11y_create_poll_votes_allowed_increase" = "Increase votes allowed per person";
|
||||||
@@ -36,9 +37,9 @@
|
|||||||
"a11y_qr_code" = "QR Code";
|
"a11y_qr_code" = "QR Code";
|
||||||
"a11y_react_with" = "React with %1$@";
|
"a11y_react_with" = "React with %1$@";
|
||||||
"a11y_react_with_other_emojis" = "React with other emojis";
|
"a11y_react_with_other_emojis" = "React with other emojis";
|
||||||
"a11y_read_receipts_multiple" = "Read by %1$@ and %2$@";
|
"a11y_read_receipts_multiple" = "Previous message read by %1$@ and %2$@";
|
||||||
"a11y_read_receipts_single" = "Read by %1$@";
|
"a11y_read_receipts_single" = "Previous message read by %1$@";
|
||||||
"a11y_read_receipts_tap_to_show_all" = "Tap to show all";
|
"a11y_read_receipts_tap_to_show_all_ios" = "Tap to show all";
|
||||||
"a11y_remove_reaction" = "Remove reaction: %1$@";
|
"a11y_remove_reaction" = "Remove reaction: %1$@";
|
||||||
"a11y_remove_reaction_with" = "Remove reaction with %1$@";
|
"a11y_remove_reaction_with" = "Remove reaction with %1$@";
|
||||||
"a11y_send_files" = "Send files";
|
"a11y_send_files" = "Send files";
|
||||||
@@ -408,6 +409,10 @@
|
|||||||
"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key";
|
"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key";
|
||||||
"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?";
|
"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?";
|
||||||
"confirm_recovery_key_banner_title" = "Your key storage is out of sync";
|
"confirm_recovery_key_banner_title" = "Your key storage is out of sync";
|
||||||
|
"content_scanner_not_found" = "Download not available.";
|
||||||
|
"content_scanner_not_found_title" = "File not found";
|
||||||
|
"content_scanner_unsafe_message" = "Preview and download have been disabled.";
|
||||||
|
"content_scanner_unsafe_title" = "The file is not safe";
|
||||||
"crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?";
|
"crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?";
|
||||||
"crypto_event_authenticity_mismatched_sender" = "The sender of the event does not match the owner of the device that sent it.";
|
"crypto_event_authenticity_mismatched_sender" = "The sender of the event does not match the owner of the device that sent it.";
|
||||||
"crypto_event_authenticity_not_guaranteed" = "The authenticity of this encrypted message can't be guaranteed on this device.";
|
"crypto_event_authenticity_not_guaranteed" = "The authenticity of this encrypted message can't be guaranteed on this device.";
|
||||||
@@ -1030,7 +1035,7 @@
|
|||||||
"screen_notification_settings_edit_custom_settings_section_title" = "Custom setting per chat";
|
"screen_notification_settings_edit_custom_settings_section_title" = "Custom setting per chat";
|
||||||
"screen_notification_settings_edit_failed_updating_default_mode" = "An error occurred while updating the notification setting.";
|
"screen_notification_settings_edit_failed_updating_default_mode" = "An error occurred while updating the notification setting.";
|
||||||
"screen_notification_settings_edit_mode_all_messages" = "All messages";
|
"screen_notification_settings_edit_mode_all_messages" = "All messages";
|
||||||
"screen_notification_settings_edit_mode_mentions_and_keywords" = "Mentions and Keywords only";
|
"screen_notification_settings_edit_mode_mentions_and_keywords" = "Mentions and replies only";
|
||||||
"screen_notification_settings_edit_screen_direct_section_header" = "On direct chats, notify me for";
|
"screen_notification_settings_edit_screen_direct_section_header" = "On direct chats, notify me for";
|
||||||
"screen_notification_settings_edit_screen_group_section_header" = "On group chats, notify me for";
|
"screen_notification_settings_edit_screen_group_section_header" = "On group chats, notify me for";
|
||||||
"screen_notification_settings_enable_notifications" = "Enable notifications on this device";
|
"screen_notification_settings_enable_notifications" = "Enable notifications on this device";
|
||||||
@@ -1336,7 +1341,7 @@
|
|||||||
"screen_room_notification_settings_error_setting_mode" = "Failed setting the mode, please try again.";
|
"screen_room_notification_settings_error_setting_mode" = "Failed setting the mode, please try again.";
|
||||||
"screen_room_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you won't get notified in this room.";
|
"screen_room_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you won't get notified in this room.";
|
||||||
"screen_room_notification_settings_mode_all_messages" = "All messages";
|
"screen_room_notification_settings_mode_all_messages" = "All messages";
|
||||||
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only";
|
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and replies only";
|
||||||
"screen_room_notification_settings_room_custom_settings_title" = "In this room, notify me for";
|
"screen_room_notification_settings_room_custom_settings_title" = "In this room, notify me for";
|
||||||
"screen_room_pinned_banner_indicator" = "%1$@ of %2$@";
|
"screen_room_pinned_banner_indicator" = "%1$@ of %2$@";
|
||||||
"screen_room_pinned_banner_indicator_description" = "%1$@ Pinned messages";
|
"screen_room_pinned_banner_indicator_description" = "%1$@ Pinned messages";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"a11y_add_reaction" = "Add reaction: %1$@";
|
"a11y_add_reaction" = "Add reaction: %1$@";
|
||||||
"a11y_address" = "Address";
|
"a11y_address" = "Address";
|
||||||
"a11y_avatar" = "Avatar";
|
"a11y_avatar" = "Avatar";
|
||||||
|
"a11y_clear_search_field" = "Clear search field";
|
||||||
"a11y_collapse_message_text_field" = "Minimise message text field";
|
"a11y_collapse_message_text_field" = "Minimise message text field";
|
||||||
"a11y_create_poll_votes_allowed_decrease" = "Decrease votes allowed per person";
|
"a11y_create_poll_votes_allowed_decrease" = "Decrease votes allowed per person";
|
||||||
"a11y_create_poll_votes_allowed_increase" = "Increase votes allowed per person";
|
"a11y_create_poll_votes_allowed_increase" = "Increase votes allowed per person";
|
||||||
@@ -36,9 +37,9 @@
|
|||||||
"a11y_qr_code" = "QR Code";
|
"a11y_qr_code" = "QR Code";
|
||||||
"a11y_react_with" = "React with %1$@";
|
"a11y_react_with" = "React with %1$@";
|
||||||
"a11y_react_with_other_emojis" = "React with other emojis";
|
"a11y_react_with_other_emojis" = "React with other emojis";
|
||||||
"a11y_read_receipts_multiple" = "Read by %1$@ and %2$@";
|
"a11y_read_receipts_multiple" = "Previous message read by %1$@ and %2$@";
|
||||||
"a11y_read_receipts_single" = "Read by %1$@";
|
"a11y_read_receipts_single" = "Previous message read by %1$@";
|
||||||
"a11y_read_receipts_tap_to_show_all" = "Tap to show all";
|
"a11y_read_receipts_tap_to_show_all_ios" = "Tap to show all";
|
||||||
"a11y_remove_reaction" = "Remove reaction: %1$@";
|
"a11y_remove_reaction" = "Remove reaction: %1$@";
|
||||||
"a11y_remove_reaction_with" = "Remove reaction with %1$@";
|
"a11y_remove_reaction_with" = "Remove reaction with %1$@";
|
||||||
"a11y_send_files" = "Send files";
|
"a11y_send_files" = "Send files";
|
||||||
@@ -408,6 +409,10 @@
|
|||||||
"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key";
|
"confirm_recovery_key_banner_primary_button_title" = "Enter your recovery key";
|
||||||
"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?";
|
"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?";
|
||||||
"confirm_recovery_key_banner_title" = "Your key storage is out of sync";
|
"confirm_recovery_key_banner_title" = "Your key storage is out of sync";
|
||||||
|
"content_scanner_not_found" = "Download not available.";
|
||||||
|
"content_scanner_not_found_title" = "File not found";
|
||||||
|
"content_scanner_unsafe_message" = "Preview and download have been disabled.";
|
||||||
|
"content_scanner_unsafe_title" = "The file is not safe";
|
||||||
"crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?";
|
"crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?";
|
||||||
"crypto_event_authenticity_mismatched_sender" = "The sender of the event does not match the owner of the device that sent it.";
|
"crypto_event_authenticity_mismatched_sender" = "The sender of the event does not match the owner of the device that sent it.";
|
||||||
"crypto_event_authenticity_not_guaranteed" = "The authenticity of this encrypted message can't be guaranteed on this device.";
|
"crypto_event_authenticity_not_guaranteed" = "The authenticity of this encrypted message can't be guaranteed on this device.";
|
||||||
@@ -1030,7 +1035,7 @@
|
|||||||
"screen_notification_settings_edit_custom_settings_section_title" = "Custom setting per chat";
|
"screen_notification_settings_edit_custom_settings_section_title" = "Custom setting per chat";
|
||||||
"screen_notification_settings_edit_failed_updating_default_mode" = "An error occurred while updating the notification setting.";
|
"screen_notification_settings_edit_failed_updating_default_mode" = "An error occurred while updating the notification setting.";
|
||||||
"screen_notification_settings_edit_mode_all_messages" = "All messages";
|
"screen_notification_settings_edit_mode_all_messages" = "All messages";
|
||||||
"screen_notification_settings_edit_mode_mentions_and_keywords" = "Mentions and Keywords only";
|
"screen_notification_settings_edit_mode_mentions_and_keywords" = "Mentions and replies only";
|
||||||
"screen_notification_settings_edit_screen_direct_section_header" = "On direct chats, notify me for";
|
"screen_notification_settings_edit_screen_direct_section_header" = "On direct chats, notify me for";
|
||||||
"screen_notification_settings_edit_screen_group_section_header" = "On group chats, notify me for";
|
"screen_notification_settings_edit_screen_group_section_header" = "On group chats, notify me for";
|
||||||
"screen_notification_settings_enable_notifications" = "Enable notifications on this device";
|
"screen_notification_settings_enable_notifications" = "Enable notifications on this device";
|
||||||
@@ -1336,7 +1341,7 @@
|
|||||||
"screen_room_notification_settings_error_setting_mode" = "Failed setting the mode, please try again.";
|
"screen_room_notification_settings_error_setting_mode" = "Failed setting the mode, please try again.";
|
||||||
"screen_room_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you won't get notified in this room.";
|
"screen_room_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you won't get notified in this room.";
|
||||||
"screen_room_notification_settings_mode_all_messages" = "All messages";
|
"screen_room_notification_settings_mode_all_messages" = "All messages";
|
||||||
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only";
|
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and replies only";
|
||||||
"screen_room_notification_settings_room_custom_settings_title" = "In this room, notify me for";
|
"screen_room_notification_settings_room_custom_settings_title" = "In this room, notify me for";
|
||||||
"screen_room_pinned_banner_indicator" = "%1$@ of %2$@";
|
"screen_room_pinned_banner_indicator" = "%1$@ of %2$@";
|
||||||
"screen_room_pinned_banner_indicator_description" = "%1$@ Pinned messages";
|
"screen_room_pinned_banner_indicator_description" = "%1$@ Pinned messages";
|
||||||
|
|||||||
@@ -43,9 +43,9 @@
|
|||||||
<key>NSStringFormatValueTypeKey</key>
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
<string>d</string>
|
<string>d</string>
|
||||||
<key>one</key>
|
<key>one</key>
|
||||||
<string>Read by %1$@ and %2$d other</string>
|
<string>Previous message read by %1$@ and %2$d other</string>
|
||||||
<key>other</key>
|
<key>other</key>
|
||||||
<string>Read by %1$@ and %2$d others</string>
|
<string>Previous message read by %1$@ and %2$d others</string>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
<string>%#@COUNT@</string>
|
<string>%#@COUNT@</string>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"screen_home_tab_search" = "Search";
|
"screen_home_tab_search" = "Search";
|
||||||
"screen_search_empty_state_message" = "Search for chats and messages";
|
"screen_search_empty_state_message" = "Search for chats and messages";
|
||||||
"screen_search_tab_messages" = "Messages";
|
|
||||||
"screen_search_tab_rooms" = "Rooms";
|
|
||||||
"screen_search_empty_state_title" = "Start searching...";
|
"screen_search_empty_state_title" = "Start searching...";
|
||||||
"screen_search_no_results_message" = "There are no results for “%1$@.” Try a new search term.";
|
"screen_search_no_results_message" = "There are no results for “%1$@.” Try a new search term.";
|
||||||
|
"screen_search_tab_messages" = "Messages";
|
||||||
|
"screen_search_tab_rooms" = "Rooms";
|
||||||
"soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages.";
|
"soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages.";
|
||||||
"soft_logout_clear_data_dialog_title" = "Clear data";
|
"soft_logout_clear_data_dialog_title" = "Clear data";
|
||||||
"soft_logout_clear_data_notice" = "Warning: Your personal data (including encryption keys) is still stored on this device.\n\nClear it if you’re finished using this device, or want to sign in to another account.";
|
"soft_logout_clear_data_notice" = "Warning: Your personal data (including encryption keys) is still stored on this device.\n\nClear it if you’re finished using this device, or want to sign in to another account.";
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ internal nonisolated enum L10n {
|
|||||||
internal static var a11yAddress: String { return L10n.tr("Localizable", "a11y_address") }
|
internal static var a11yAddress: String { return L10n.tr("Localizable", "a11y_address") }
|
||||||
/// Avatar
|
/// Avatar
|
||||||
internal static var a11yAvatar: String { return L10n.tr("Localizable", "a11y_avatar") }
|
internal static var a11yAvatar: String { return L10n.tr("Localizable", "a11y_avatar") }
|
||||||
|
/// Clear search field
|
||||||
|
internal static var a11yClearSearchField: String { return L10n.tr("Localizable", "a11y_clear_search_field") }
|
||||||
/// Minimise message text field
|
/// Minimise message text field
|
||||||
internal static var a11yCollapseMessageTextField: String { return L10n.tr("Localizable", "a11y_collapse_message_text_field") }
|
internal static var a11yCollapseMessageTextField: String { return L10n.tr("Localizable", "a11y_collapse_message_text_field") }
|
||||||
/// Decrease votes allowed per person
|
/// Decrease votes allowed per person
|
||||||
@@ -108,7 +110,7 @@ internal nonisolated enum L10n {
|
|||||||
}
|
}
|
||||||
/// React with other emojis
|
/// React with other emojis
|
||||||
internal static var a11yReactWithOtherEmojis: String { return L10n.tr("Localizable", "a11y_react_with_other_emojis") }
|
internal static var a11yReactWithOtherEmojis: String { return L10n.tr("Localizable", "a11y_react_with_other_emojis") }
|
||||||
/// Read by %1$@ and %2$@
|
/// Previous message read by %1$@ and %2$@
|
||||||
internal static func a11yReadReceiptsMultiple(_ p1: Any, _ p2: Any) -> String {
|
internal static func a11yReadReceiptsMultiple(_ p1: Any, _ p2: Any) -> String {
|
||||||
return L10n.tr("Localizable", "a11y_read_receipts_multiple", String(describing: p1), String(describing: p2))
|
return L10n.tr("Localizable", "a11y_read_receipts_multiple", String(describing: p1), String(describing: p2))
|
||||||
}
|
}
|
||||||
@@ -116,12 +118,12 @@ internal nonisolated enum L10n {
|
|||||||
internal static func a11yReadReceiptsMultipleWithOthers(_ p1: Int) -> String {
|
internal static func a11yReadReceiptsMultipleWithOthers(_ p1: Int) -> String {
|
||||||
return L10n.tr("Localizable", "a11y_read_receipts_multiple_with_others", p1)
|
return L10n.tr("Localizable", "a11y_read_receipts_multiple_with_others", p1)
|
||||||
}
|
}
|
||||||
/// Read by %1$@
|
/// Previous message read by %1$@
|
||||||
internal static func a11yReadReceiptsSingle(_ p1: Any) -> String {
|
internal static func a11yReadReceiptsSingle(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "a11y_read_receipts_single", String(describing: p1))
|
return L10n.tr("Localizable", "a11y_read_receipts_single", String(describing: p1))
|
||||||
}
|
}
|
||||||
/// Tap to show all
|
/// Tap to show all
|
||||||
internal static var a11yReadReceiptsTapToShowAll: String { return L10n.tr("Localizable", "a11y_read_receipts_tap_to_show_all") }
|
internal static var a11yReadReceiptsTapToShowAllIos: String { return L10n.tr("Localizable", "a11y_read_receipts_tap_to_show_all_ios") }
|
||||||
/// Remove reaction: %1$@
|
/// Remove reaction: %1$@
|
||||||
internal static func a11yRemoveReaction(_ p1: Any) -> String {
|
internal static func a11yRemoveReaction(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "a11y_remove_reaction", String(describing: p1))
|
return L10n.tr("Localizable", "a11y_remove_reaction", String(describing: p1))
|
||||||
@@ -900,6 +902,14 @@ internal nonisolated enum L10n {
|
|||||||
internal static var confirmRecoveryKeyBannerSecondaryButtonTitle: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_secondary_button_title") }
|
internal static var confirmRecoveryKeyBannerSecondaryButtonTitle: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_secondary_button_title") }
|
||||||
/// Your key storage is out of sync
|
/// Your key storage is out of sync
|
||||||
internal static var confirmRecoveryKeyBannerTitle: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_title") }
|
internal static var confirmRecoveryKeyBannerTitle: String { return L10n.tr("Localizable", "confirm_recovery_key_banner_title") }
|
||||||
|
/// Download not available.
|
||||||
|
internal static var contentScannerNotFound: String { return L10n.tr("Localizable", "content_scanner_not_found") }
|
||||||
|
/// File not found
|
||||||
|
internal static var contentScannerNotFoundTitle: String { return L10n.tr("Localizable", "content_scanner_not_found_title") }
|
||||||
|
/// Preview and download have been disabled.
|
||||||
|
internal static var contentScannerUnsafeMessage: String { return L10n.tr("Localizable", "content_scanner_unsafe_message") }
|
||||||
|
/// The file is not safe
|
||||||
|
internal static var contentScannerUnsafeTitle: String { return L10n.tr("Localizable", "content_scanner_unsafe_title") }
|
||||||
/// %1$@ crashed the last time it was used. Would you like to share a crash report with us?
|
/// %1$@ crashed the last time it was used. Would you like to share a crash report with us?
|
||||||
internal static func crashDetectionDialogContent(_ p1: Any) -> String {
|
internal static func crashDetectionDialogContent(_ p1: Any) -> String {
|
||||||
return L10n.tr("Localizable", "crash_detection_dialog_content", String(describing: p1))
|
return L10n.tr("Localizable", "crash_detection_dialog_content", String(describing: p1))
|
||||||
@@ -2396,7 +2406,7 @@ internal nonisolated enum L10n {
|
|||||||
internal static var screenNotificationSettingsEditFailedUpdatingDefaultMode: String { return L10n.tr("Localizable", "screen_notification_settings_edit_failed_updating_default_mode") }
|
internal static var screenNotificationSettingsEditFailedUpdatingDefaultMode: String { return L10n.tr("Localizable", "screen_notification_settings_edit_failed_updating_default_mode") }
|
||||||
/// All messages
|
/// All messages
|
||||||
internal static var screenNotificationSettingsEditModeAllMessages: String { return L10n.tr("Localizable", "screen_notification_settings_edit_mode_all_messages") }
|
internal static var screenNotificationSettingsEditModeAllMessages: String { return L10n.tr("Localizable", "screen_notification_settings_edit_mode_all_messages") }
|
||||||
/// Mentions and Keywords only
|
/// Mentions and replies only
|
||||||
internal static var screenNotificationSettingsEditModeMentionsAndKeywords: String { return L10n.tr("Localizable", "screen_notification_settings_edit_mode_mentions_and_keywords") }
|
internal static var screenNotificationSettingsEditModeMentionsAndKeywords: String { return L10n.tr("Localizable", "screen_notification_settings_edit_mode_mentions_and_keywords") }
|
||||||
/// On direct chats, notify me for
|
/// On direct chats, notify me for
|
||||||
internal static var screenNotificationSettingsEditScreenDirectSectionHeader: String { return L10n.tr("Localizable", "screen_notification_settings_edit_screen_direct_section_header") }
|
internal static var screenNotificationSettingsEditScreenDirectSectionHeader: String { return L10n.tr("Localizable", "screen_notification_settings_edit_screen_direct_section_header") }
|
||||||
@@ -3080,7 +3090,7 @@ internal nonisolated enum L10n {
|
|||||||
internal static var screenRoomNotificationSettingsMentionsOnlyDisclaimer: String { return L10n.tr("Localizable", "screen_room_notification_settings_mentions_only_disclaimer") }
|
internal static var screenRoomNotificationSettingsMentionsOnlyDisclaimer: String { return L10n.tr("Localizable", "screen_room_notification_settings_mentions_only_disclaimer") }
|
||||||
/// All messages
|
/// All messages
|
||||||
internal static var screenRoomNotificationSettingsModeAllMessages: String { return L10n.tr("Localizable", "screen_room_notification_settings_mode_all_messages") }
|
internal static var screenRoomNotificationSettingsModeAllMessages: String { return L10n.tr("Localizable", "screen_room_notification_settings_mode_all_messages") }
|
||||||
/// Mentions and Keywords only
|
/// Mentions and replies only
|
||||||
internal static var screenRoomNotificationSettingsModeMentionsAndKeywords: String { return L10n.tr("Localizable", "screen_room_notification_settings_mode_mentions_and_keywords") }
|
internal static var screenRoomNotificationSettingsModeMentionsAndKeywords: String { return L10n.tr("Localizable", "screen_room_notification_settings_mode_mentions_and_keywords") }
|
||||||
/// In this room, notify me for
|
/// In this room, notify me for
|
||||||
internal static var screenRoomNotificationSettingsRoomCustomSettingsTitle: String { return L10n.tr("Localizable", "screen_room_notification_settings_room_custom_settings_title") }
|
internal static var screenRoomNotificationSettingsRoomCustomSettingsTitle: String { return L10n.tr("Localizable", "screen_room_notification_settings_room_custom_settings_title") }
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2026 Element Creations Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ContentScannerServiceMockConfiguration {
|
||||||
|
/// The verdict reported for every source: `true` when safe, `false` when unsafe, or `nil` to
|
||||||
|
/// never resolve the scan so that the scanning state remains visible.
|
||||||
|
var scanResult: Bool?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ContentScannerServiceMock {
|
||||||
|
convenience init(_ configuration: ContentScannerServiceMockConfiguration) {
|
||||||
|
self.init()
|
||||||
|
|
||||||
|
scanResultFromSourceReturnValue = configuration.scanResult
|
||||||
|
loadScanResultFromSourceClosure = { _ in
|
||||||
|
guard let scanResult = configuration.scanResult else {
|
||||||
|
try? await Task.sleep(for: .seconds(3600))
|
||||||
|
return .failure(.failedScanning)
|
||||||
|
}
|
||||||
|
return .success(scanResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import Foundation
|
|||||||
|
|
||||||
struct UserSessionMockConfiguration {
|
struct UserSessionMockConfiguration {
|
||||||
var clientProxy: ClientProxyProtocol = ClientProxyMock(.init())
|
var clientProxy: ClientProxyProtocol = ClientProxyMock(.init())
|
||||||
|
var contentScannerService: ContentScannerServiceProtocol?
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor extension UserSessionMock {
|
@MainActor extension UserSessionMock {
|
||||||
@@ -20,6 +21,7 @@ struct UserSessionMockConfiguration {
|
|||||||
clientProxy = configuration.clientProxy
|
clientProxy = configuration.clientProxy
|
||||||
mediaProvider = MediaProviderMock(.init())
|
mediaProvider = MediaProviderMock(.init())
|
||||||
voiceMessageMediaManager = VoiceMessageMediaManagerMock()
|
voiceMessageMediaManager = VoiceMessageMediaManagerMock()
|
||||||
|
contentScannerService = configuration.contentScannerService
|
||||||
|
|
||||||
sessionSecurityStatePublisher = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .enabled)).asCurrentValuePublisher()
|
sessionSecurityStatePublisher = CurrentValueSubject<SessionSecurityState, Never>(.init(verificationState: .verified, recoveryState: .enabled)).asCurrentValuePublisher()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2026 Element Creations Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// The reason a media item can't be presented.
|
||||||
|
nonisolated enum ContentScanningFailure {
|
||||||
|
/// The content scanner flagged the media as unsafe.
|
||||||
|
case notSafe
|
||||||
|
/// The media couldn't be scanned, e.g. it wasn't found or a network error occurred.
|
||||||
|
case notFound
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reports a ``ContentScanningFailure`` up the view hierarchy so that ancestors (such as the
|
||||||
|
/// message bubble) can adopt the critical styling without having to track the scan state themselves.
|
||||||
|
nonisolated struct ContentScanningFailurePreferenceKey: PreferenceKey {
|
||||||
|
static let defaultValue: ContentScanningFailure? = nil
|
||||||
|
|
||||||
|
static func reduce(value: inout ContentScanningFailure?, nextValue: () -> ContentScanningFailure?) {
|
||||||
|
value = value ?? nextValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper that gates the presentation of a media item on its content scan state.
|
||||||
|
///
|
||||||
|
/// The safe content is rendered when the media is known to be safe or when there's no content
|
||||||
|
/// scanner configured, the scanning content whilst a scan is in flight and the unsafe content
|
||||||
|
/// when the media is unsafe or couldn't be scanned. Any failure is also reported up the view
|
||||||
|
/// hierarchy through ``ContentScanningFailurePreferenceKey``.
|
||||||
|
///
|
||||||
|
/// - Important: Any interactions with the media (taps, playback etc.) must live inside the safe
|
||||||
|
/// content so that they're unavailable whilst the media is being scanned or is unsafe.
|
||||||
|
struct ContentScanningView<SafeContent: View, ScanningContent: View, UnsafeContent: View>: View {
|
||||||
|
private enum ScanState {
|
||||||
|
case safe
|
||||||
|
case scanning
|
||||||
|
case unsafe(ContentScanningFailure)
|
||||||
|
}
|
||||||
|
|
||||||
|
private let contentScannerService: ContentScannerServiceProtocol?
|
||||||
|
private let mediaSource: MediaSourceProxy?
|
||||||
|
private let safeContent: () -> SafeContent
|
||||||
|
private let scanningContent: () -> ScanningContent
|
||||||
|
private let unsafeContent: (ContentScanningFailure) -> UnsafeContent
|
||||||
|
|
||||||
|
@State private var scanState: ScanState?
|
||||||
|
|
||||||
|
/// - Parameters:
|
||||||
|
/// - contentScannerService: The service used to scan the media. Passing `nil` disables
|
||||||
|
/// scanning and the content is always rendered.
|
||||||
|
/// - mediaSource: The media source to scan. Passing `nil` renders the content, this
|
||||||
|
/// makes it convenient to wrap items whose media source is optional.
|
||||||
|
/// - safeContent: The regular presentation of the media, including any interactions with it.
|
||||||
|
/// - scanningContent: The presentation of the media whilst it's being scanned.
|
||||||
|
/// - unsafeContent: The presentation of the media when it can't be shown.
|
||||||
|
init(contentScannerService: ContentScannerServiceProtocol?,
|
||||||
|
mediaSource: MediaSourceProxy?,
|
||||||
|
@ViewBuilder safeContent: @escaping () -> SafeContent,
|
||||||
|
@ViewBuilder scanningContent: @escaping () -> ScanningContent,
|
||||||
|
@ViewBuilder unsafeContent: @escaping (ContentScanningFailure) -> UnsafeContent) {
|
||||||
|
self.contentScannerService = contentScannerService
|
||||||
|
self.mediaSource = mediaSource
|
||||||
|
self.safeContent = safeContent
|
||||||
|
self.scanningContent = scanningContent
|
||||||
|
self.unsafeContent = unsafeContent
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
content
|
||||||
|
.preference(key: ContentScanningFailurePreferenceKey.self, value: scanFailure)
|
||||||
|
.task(id: mediaSource?.url.absoluteString) {
|
||||||
|
await scan()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var content: some View {
|
||||||
|
switch resolvedScanState {
|
||||||
|
case .safe:
|
||||||
|
safeContent()
|
||||||
|
case .scanning:
|
||||||
|
scanningContent()
|
||||||
|
case .unsafe(let failure):
|
||||||
|
unsafeContent(failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var scanFailure: ContentScanningFailure? {
|
||||||
|
guard case .unsafe(let failure) = resolvedScanState else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return failure
|
||||||
|
}
|
||||||
|
|
||||||
|
private var resolvedScanState: ScanState {
|
||||||
|
guard let contentScannerService, let mediaSource else {
|
||||||
|
return .safe
|
||||||
|
}
|
||||||
|
|
||||||
|
if let scanState {
|
||||||
|
return scanState
|
||||||
|
}
|
||||||
|
|
||||||
|
// Until the scan triggered by this view resolves, reflect any verdict already in the cache.
|
||||||
|
switch contentScannerService.scanResultFromSource(mediaSource) {
|
||||||
|
case true: return .safe
|
||||||
|
case false: return .unsafe(.notSafe)
|
||||||
|
default: return .scanning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scan() async {
|
||||||
|
guard let contentScannerService, let mediaSource else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch await contentScannerService.loadScanResultFromSource(mediaSource) {
|
||||||
|
case .success(true):
|
||||||
|
scanState = .safe
|
||||||
|
case .success(false):
|
||||||
|
scanState = .unsafe(.notSafe)
|
||||||
|
case .failure:
|
||||||
|
scanState = .unsafe(.notFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ enum TestablePreviewsDictionary {
|
|||||||
"CollapsibleRoomTimelineView_Previews" : CollapsibleRoomTimelineView_Previews.self,
|
"CollapsibleRoomTimelineView_Previews" : CollapsibleRoomTimelineView_Previews.self,
|
||||||
"CompletionSuggestion_Previews" : CompletionSuggestion_Previews.self,
|
"CompletionSuggestion_Previews" : CompletionSuggestion_Previews.self,
|
||||||
"ComposerToolbar_Previews" : ComposerToolbar_Previews.self,
|
"ComposerToolbar_Previews" : ComposerToolbar_Previews.self,
|
||||||
|
"ContentScanningFailureView_Previews" : ContentScanningFailureView_Previews.self,
|
||||||
"CopyTextButton_Previews" : CopyTextButton_Previews.self,
|
"CopyTextButton_Previews" : CopyTextButton_Previews.self,
|
||||||
"CreateRoomSpaceSelectionSheet_Previews" : CreateRoomSpaceSelectionSheet_Previews.self,
|
"CreateRoomSpaceSelectionSheet_Previews" : CreateRoomSpaceSelectionSheet_Previews.self,
|
||||||
"CreateRoom_Previews" : CreateRoom_Previews.self,
|
"CreateRoom_Previews" : CreateRoom_Previews.self,
|
||||||
|
|||||||
@@ -1116,7 +1116,10 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
extension TimelineViewModel {
|
extension TimelineViewModel {
|
||||||
static let mock = mock(timelineKind: .live)
|
static let mock = mock(timelineKind: .live)
|
||||||
|
|
||||||
static func mock(timelineKind: TimelineKind = .live, timelineController: TimelineControllerMock? = nil, hasPredecessor: Bool = false) -> TimelineViewModel {
|
static func mock(timelineKind: TimelineKind = .live,
|
||||||
|
timelineController: TimelineControllerMock? = nil,
|
||||||
|
hasPredecessor: Bool = false,
|
||||||
|
contentScannerService: ContentScannerServiceProtocol? = nil) -> TimelineViewModel {
|
||||||
let clientProxyMock = ClientProxyMock(.init())
|
let clientProxyMock = ClientProxyMock(.init())
|
||||||
clientProxyMock.roomSummaryForAliasReturnValue = .mock(id: "!room:matrix.org", name: "Room")
|
clientProxyMock.roomSummaryForAliasReturnValue = .mock(id: "!room:matrix.org", name: "Room")
|
||||||
clientProxyMock.roomSummaryForIdentifierReturnValue = .mock(id: "!room:matrix.org", name: "Room", canonicalAlias: "#room:matrix.org")
|
clientProxyMock.roomSummaryForIdentifierReturnValue = .mock(id: "!room:matrix.org", name: "Room", canonicalAlias: "#room:matrix.org")
|
||||||
@@ -1127,7 +1130,7 @@ extension TimelineViewModel {
|
|||||||
return TimelineViewModel(roomProxy: roomProxy,
|
return TimelineViewModel(roomProxy: roomProxy,
|
||||||
focussedEventID: nil,
|
focussedEventID: nil,
|
||||||
timelineController: timelineController ?? TimelineControllerMock(.init(timelineKind: timelineKind)),
|
timelineController: timelineController ?? TimelineControllerMock(.init(timelineKind: timelineKind)),
|
||||||
userSession: UserSessionMock(.init(clientProxy: clientProxyMock)),
|
userSession: UserSessionMock(.init(clientProxy: clientProxyMock, contentScannerService: contentScannerService)),
|
||||||
mediaPlayerProvider: MediaPlayerProviderMock(),
|
mediaPlayerProvider: MediaPlayerProviderMock(),
|
||||||
userIndicatorController: UserIndicatorControllerMock(),
|
userIndicatorController: UserIndicatorControllerMock(),
|
||||||
appMediator: AppMediatorMock(.init()),
|
appMediator: AppMediatorMock(.init()),
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ extension View {
|
|||||||
/// - isOutgoing: rounds the corners according to the side it shows on, defaults to true
|
/// - isOutgoing: rounds the corners according to the side it shows on, defaults to true
|
||||||
/// - insets: defaults to what we use for file timeline items, text uses custom values
|
/// - insets: defaults to what we use for file timeline items, text uses custom values
|
||||||
/// - color: self explanatory, defaults to subtle secondary
|
/// - color: self explanatory, defaults to subtle secondary
|
||||||
|
/// - borderColor: an optional colour for a border around the bubble
|
||||||
func bubbleBackground(isOutgoing: Bool = true,
|
func bubbleBackground(isOutgoing: Bool = true,
|
||||||
insets: EdgeInsets = .init(top: 8, leading: 12, bottom: 8, trailing: 12),
|
insets: EdgeInsets = .init(top: 8, leading: 12, bottom: 8, trailing: 12),
|
||||||
color: @autoclosure @MainActor () -> Color? = .compound.bgSubtleSecondary) -> some View {
|
color: @autoclosure @MainActor () -> Color? = .compound.bgSubtleSecondary,
|
||||||
|
borderColor: @autoclosure @MainActor () -> Color? = nil) -> some View {
|
||||||
modifier(TimelineItemBubbleBackgroundModifier(isOutgoing: isOutgoing,
|
modifier(TimelineItemBubbleBackgroundModifier(isOutgoing: isOutgoing,
|
||||||
insets: insets,
|
insets: insets,
|
||||||
color: color()))
|
color: color(),
|
||||||
|
borderColor: borderColor()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,12 +32,19 @@ private struct TimelineItemBubbleBackgroundModifier: ViewModifier {
|
|||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let insets: EdgeInsets
|
let insets: EdgeInsets
|
||||||
var color: Color?
|
var color: Color?
|
||||||
|
var borderColor: Color?
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.padding(insets)
|
.padding(insets)
|
||||||
.background(color)
|
.background(color)
|
||||||
.cornerRadius(12, corners: roundedCorners)
|
.cornerRadius(12, corners: roundedCorners)
|
||||||
|
.overlay {
|
||||||
|
if let borderColor {
|
||||||
|
RoundedCornerShape(radius: 12, corners: roundedCorners)
|
||||||
|
.stroke(borderColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var roundedCorners: UIRectCorner {
|
private var roundedCorners: UIRectCorner {
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
let adjustedDeliveryStatus: TimelineItemDeliveryStatus?
|
let adjustedDeliveryStatus: TimelineItemDeliveryStatus?
|
||||||
@ViewBuilder let content: () -> Content
|
@ViewBuilder let content: () -> Content
|
||||||
|
|
||||||
|
/// Whether the item's media failed content scanning, in which case the whole bubble adopts
|
||||||
|
/// the critical styling. Reported by the item's `ContentScanningView` through the preference key.
|
||||||
|
@State private var contentScanningFailure: ContentScanningFailure?
|
||||||
|
|
||||||
|
private var hasContentScanningFailure: Bool {
|
||||||
|
contentScanningFailure != nil
|
||||||
|
}
|
||||||
|
|
||||||
private var isDM: Bool {
|
private var isDM: Bool {
|
||||||
context.viewState.isDM
|
context.viewState.isDM
|
||||||
}
|
}
|
||||||
@@ -78,6 +86,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
.padding(EdgeInsets(top: 1, leading: 8, bottom: 1, trailing: 8))
|
.padding(EdgeInsets(top: 1, leading: 8, bottom: 1, trailing: 8))
|
||||||
.highlightedTimelineItem(isFocussed)
|
.highlightedTimelineItem(isFocussed)
|
||||||
|
.onPreferenceChange(ContentScanningFailurePreferenceKey.self) { contentScanningFailure = $0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -179,10 +188,14 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
|
|
||||||
var messageBubble: some View {
|
var messageBubble: some View {
|
||||||
contentWithReply
|
contentWithReply
|
||||||
.timelineItemSendInfo(timelineItem: timelineItem, adjustedDeliveryStatus: adjustedDeliveryStatus, context: context)
|
.timelineItemSendInfo(timelineItem: timelineItem,
|
||||||
|
adjustedDeliveryStatus: adjustedDeliveryStatus,
|
||||||
|
hasContentScanningFailure: hasContentScanningFailure,
|
||||||
|
context: context)
|
||||||
.bubbleBackground(isOutgoing: timelineItem.isOutgoing,
|
.bubbleBackground(isOutgoing: timelineItem.isOutgoing,
|
||||||
insets: timelineItem.bubbleInsets,
|
insets: timelineItem.bubbleInsets(hasContentScanningFailure: hasContentScanningFailure),
|
||||||
color: timelineItem.bubbleBackgroundColor)
|
color: hasContentScanningFailure ? .compound.bgCriticalSubtle : timelineItem.bubbleBackgroundColor,
|
||||||
|
borderColor: hasContentScanningFailure ? .compound.borderCriticalSubtle : nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentWithReply: some View {
|
var contentWithReply: some View {
|
||||||
@@ -248,9 +261,14 @@ private extension EventBasedTimelineItemProtocol {
|
|||||||
|
|
||||||
/// The insets for the full bubble content.
|
/// The insets for the full bubble content.
|
||||||
/// Padding affecting just the "send info" should be added inside `TimelineItemSendInfoView`
|
/// Padding affecting just the "send info" should be added inside `TimelineItemSendInfoView`
|
||||||
var bubbleInsets: EdgeInsets {
|
func bubbleInsets(hasContentScanningFailure: Bool) -> EdgeInsets {
|
||||||
let defaultInsets: EdgeInsets = .init(around: 8)
|
let defaultInsets: EdgeInsets = .init(around: 8)
|
||||||
|
|
||||||
|
// The content scanner failure placeholder is always rendered inset within the critical bubble.
|
||||||
|
if hasContentScanningFailure {
|
||||||
|
return defaultInsets
|
||||||
|
}
|
||||||
|
|
||||||
switch self {
|
switch self {
|
||||||
case is StickerRoomTimelineItem:
|
case is StickerRoomTimelineItem:
|
||||||
return .zero
|
return .zero
|
||||||
@@ -370,6 +388,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
|||||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
static let unsafeViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: false)))
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
mockTimeline
|
mockTimeline
|
||||||
.previewDisplayName("Mock Timeline")
|
.previewDisplayName("Mock Timeline")
|
||||||
@@ -439,6 +459,26 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
|||||||
content: .init(body: "Short message"),
|
content: .init(body: "Short message"),
|
||||||
properties: properties),
|
properties: properties),
|
||||||
groupStyle: .single))
|
groupStyle: .single))
|
||||||
|
|
||||||
|
RoomTimelineItemView(viewState: .init(item: FileRoomTimelineItem(id: .randomEvent,
|
||||||
|
timestamp: .mock,
|
||||||
|
isOutgoing: true,
|
||||||
|
isEditable: false,
|
||||||
|
canBeRepliedTo: true,
|
||||||
|
sender: .init(id: "whoever"),
|
||||||
|
content: .init(filename: "unsafe.pdf",
|
||||||
|
caption: "Replying with an unsafe file.",
|
||||||
|
formattedCaption: nil,
|
||||||
|
source: try? MediaSourceProxy(url: .mockMXCFile, mimeType: nil),
|
||||||
|
fileSize: 3 * 1024 * 1024,
|
||||||
|
thumbnailSource: nil,
|
||||||
|
contentType: nil),
|
||||||
|
properties: .init(replyDetails: .loaded(sender: .init(id: "", displayName: "Alice"),
|
||||||
|
eventID: "123",
|
||||||
|
eventContent: .message(.text(.init(body: "Short")))))),
|
||||||
|
groupStyle: .single))
|
||||||
|
.environmentObject(unsafeViewModel.context)
|
||||||
|
.environment(\.timelineContext, unsafeViewModel.context)
|
||||||
}
|
}
|
||||||
.environmentObject(viewModel.context)
|
.environmentObject(viewModel.context)
|
||||||
.environment(\.timelineContext, viewModel.context)
|
.environment(\.timelineContext, viewModel.context)
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ extension View {
|
|||||||
/// Adds the send info (timestamp along indicators for edits and delivery/encryption issues) for the given timeline item to this view.
|
/// Adds the send info (timestamp along indicators for edits and delivery/encryption issues) for the given timeline item to this view.
|
||||||
func timelineItemSendInfo(timelineItem: EventBasedTimelineItemProtocol,
|
func timelineItemSendInfo(timelineItem: EventBasedTimelineItemProtocol,
|
||||||
adjustedDeliveryStatus: TimelineItemDeliveryStatus?,
|
adjustedDeliveryStatus: TimelineItemDeliveryStatus?,
|
||||||
|
hasContentScanningFailure: Bool = false,
|
||||||
context: TimelineViewModel.Context) -> some View {
|
context: TimelineViewModel.Context) -> some View {
|
||||||
modifier(TimelineItemSendInfoModifier(sendInfo: .init(timelineItem: timelineItem,
|
modifier(TimelineItemSendInfoModifier(sendInfo: .init(timelineItem: timelineItem,
|
||||||
adjustedDeliveryStatus: adjustedDeliveryStatus),
|
adjustedDeliveryStatus: adjustedDeliveryStatus,
|
||||||
|
hasContentScanningFailure: hasContentScanningFailure),
|
||||||
context: context))
|
context: context))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,7 +150,9 @@ private struct TimelineItemSendInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extension TimelineItemSendInfo {
|
private extension TimelineItemSendInfo {
|
||||||
init(timelineItem: EventBasedTimelineItemProtocol, adjustedDeliveryStatus: TimelineItemDeliveryStatus?) {
|
init(timelineItem: EventBasedTimelineItemProtocol,
|
||||||
|
adjustedDeliveryStatus: TimelineItemDeliveryStatus?,
|
||||||
|
hasContentScanningFailure: Bool = false) {
|
||||||
itemID = timelineItem.id
|
itemID = timelineItem.id
|
||||||
localizedString = timelineItem.localizedSendInfo
|
localizedString = timelineItem.localizedSendInfo
|
||||||
|
|
||||||
@@ -162,7 +166,12 @@ private extension TimelineItemSendInfo {
|
|||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
layoutType = switch timelineItem {
|
layoutType = if hasContentScanningFailure {
|
||||||
|
// The content scanner failure placeholder replaces the media,
|
||||||
|
// so the send info is laid out like it is for a text message.
|
||||||
|
timelineItem.hasMediaCaption ? .overlay(capsuleStyle: false) : .horizontal()
|
||||||
|
} else {
|
||||||
|
switch timelineItem {
|
||||||
case is TextBasedRoomTimelineItem:
|
case is TextBasedRoomTimelineItem:
|
||||||
.overlay(capsuleStyle: false)
|
.overlay(capsuleStyle: false)
|
||||||
case let liveLocationTimelineItem as LiveLocationRoomTimelineItem:
|
case let liveLocationTimelineItem as LiveLocationRoomTimelineItem:
|
||||||
@@ -187,6 +196,7 @@ private extension TimelineItemSendInfo {
|
|||||||
.horizontal()
|
.horizontal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension LiveLocationRoomTimelineItem {
|
private extension LiveLocationRoomTimelineItem {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ struct TimelineReadReceiptsView: View {
|
|||||||
}
|
}
|
||||||
.accessibilityElement(children: .ignore)
|
.accessibilityElement(children: .ignore)
|
||||||
.accessibilityLabel(accessibilityLabel)
|
.accessibilityLabel(accessibilityLabel)
|
||||||
.accessibilityHint(L10n.a11yReadReceiptsTapToShowAll)
|
.accessibilityHint(L10n.a11yReadReceiptsTapToShowAllIos)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var remaining: Int {
|
private var remaining: Int {
|
||||||
|
|||||||
+22
-2
@@ -21,7 +21,9 @@ struct AudioRoomTimelineView: View {
|
|||||||
formattedCaption: timelineItem.content.formattedCaption,
|
formattedCaption: timelineItem.content.formattedCaption,
|
||||||
trailingReservedSize: timelineItem.trailingReservedSize,
|
trailingReservedSize: timelineItem.trailingReservedSize,
|
||||||
shouldBoost: timelineItem.shouldBoost,
|
shouldBoost: timelineItem.shouldBoost,
|
||||||
isAudioFile: true) {
|
isAudioFile: true,
|
||||||
|
contentScannerService: context?.contentScannerService,
|
||||||
|
mediaSource: timelineItem.content.source) {
|
||||||
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
|
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
|
||||||
}
|
}
|
||||||
.accessibilityLabel(L10n.commonAudio)
|
.accessibilityLabel(L10n.commonAudio)
|
||||||
@@ -31,6 +33,8 @@ struct AudioRoomTimelineView: View {
|
|||||||
|
|
||||||
struct AudioRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
struct AudioRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||||
static let viewModel = TimelineViewModel.mock
|
static let viewModel = TimelineViewModel.mock
|
||||||
|
static let scanningViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: nil)))
|
||||||
|
static let unsafeViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: false)))
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
@@ -42,6 +46,22 @@ struct AudioRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
caption: "This song rocks!"))
|
caption: "This song rocks!"))
|
||||||
}
|
}
|
||||||
.environmentObject(viewModel.context)
|
.environmentObject(viewModel.context)
|
||||||
|
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
AudioRoomTimelineView(timelineItem: makeItem(filename: "scanning.ogg",
|
||||||
|
fileSize: 2 * 1024 * 1024,
|
||||||
|
caption: "The audio is being scanned."))
|
||||||
|
.environmentObject(scanningViewModel.context)
|
||||||
|
.environment(\.timelineContext, scanningViewModel.context)
|
||||||
|
|
||||||
|
AudioRoomTimelineView(timelineItem: makeItem(filename: "unsafe.ogg",
|
||||||
|
fileSize: 2 * 1024 * 1024,
|
||||||
|
caption: "The audio is not safe."))
|
||||||
|
.environmentObject(unsafeViewModel.context)
|
||||||
|
.environment(\.timelineContext, unsafeViewModel.context)
|
||||||
|
}
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
|
.previewDisplayName("Content Scanner")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeItem(filename: String, fileSize: UInt, caption: String? = nil) -> AudioRoomTimelineItem {
|
static func makeItem(filename: String, fileSize: UInt, caption: String? = nil) -> AudioRoomTimelineItem {
|
||||||
@@ -55,7 +75,7 @@ struct AudioRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
caption: caption,
|
caption: caption,
|
||||||
duration: 300,
|
duration: 300,
|
||||||
waveform: nil,
|
waveform: nil,
|
||||||
source: nil,
|
source: try? MediaSourceProxy(url: .mockMXCAudio, mimeType: nil),
|
||||||
fileSize: fileSize,
|
fileSize: fileSize,
|
||||||
contentType: nil))
|
contentType: nil))
|
||||||
}
|
}
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2026 Element Creations Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Compound
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// The placeholder shown in place of a media item when the content scanner wasn't able to
|
||||||
|
/// validate it. Preview and download of the content are disabled.
|
||||||
|
///
|
||||||
|
/// This view only renders the failure title and message - the critical background is applied
|
||||||
|
/// to the whole message bubble by `TimelineItemBubbledStylerView`, so that any caption and
|
||||||
|
/// reply preview are included in the critical area too.
|
||||||
|
struct ContentScanningFailureView: View {
|
||||||
|
let failure: ContentScanningFailure
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
CompoundIcon(\.error, size: .medium, relativeTo: .compound.bodyMDSemibold)
|
||||||
|
.foregroundStyle(.compound.iconCriticalPrimary)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(title)
|
||||||
|
.font(.compound.bodyMDSemibold)
|
||||||
|
.foregroundStyle(.compound.textCriticalPrimary)
|
||||||
|
|
||||||
|
Text(message)
|
||||||
|
.font(.compound.bodySM)
|
||||||
|
.foregroundStyle(.compound.textSecondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.accessibilityElement(children: .combine)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var title: String {
|
||||||
|
switch failure {
|
||||||
|
case .notSafe: L10n.contentScannerUnsafeTitle
|
||||||
|
case .notFound: L10n.contentScannerNotFoundTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var message: String {
|
||||||
|
switch failure {
|
||||||
|
case .notSafe: L10n.contentScannerUnsafeMessage
|
||||||
|
case .notFound: L10n.contentScannerNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
struct ContentScanningFailureView_Previews: PreviewProvider, TestablePreview {
|
||||||
|
static var previews: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
ContentScanningFailureView(failure: .notSafe)
|
||||||
|
ContentScanningFailureView(failure: .notFound)
|
||||||
|
}
|
||||||
|
.frame(width: 260)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
-8
@@ -20,7 +20,9 @@ struct FileRoomTimelineView: View {
|
|||||||
caption: timelineItem.content.caption,
|
caption: timelineItem.content.caption,
|
||||||
formattedCaption: timelineItem.content.formattedCaption,
|
formattedCaption: timelineItem.content.formattedCaption,
|
||||||
trailingReservedSize: timelineItem.trailingReservedSize,
|
trailingReservedSize: timelineItem.trailingReservedSize,
|
||||||
shouldBoost: timelineItem.shouldBoost) {
|
shouldBoost: timelineItem.shouldBoost,
|
||||||
|
contentScannerService: context?.contentScannerService,
|
||||||
|
mediaSource: timelineItem.content.source) {
|
||||||
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
|
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
|
||||||
}
|
}
|
||||||
.accessibilityLabel(L10n.commonFile)
|
.accessibilityLabel(L10n.commonFile)
|
||||||
@@ -38,6 +40,8 @@ struct MediaFileRoomTimelineContent: View {
|
|||||||
var trailingReservedSize: CGSize = .zero
|
var trailingReservedSize: CGSize = .zero
|
||||||
var shouldBoost = false
|
var shouldBoost = false
|
||||||
var isAudioFile = false
|
var isAudioFile = false
|
||||||
|
var contentScannerService: ContentScannerServiceProtocol?
|
||||||
|
var mediaSource: MediaSourceProxy?
|
||||||
|
|
||||||
private var fileDescription: String {
|
private var fileDescription: String {
|
||||||
var fileDescription = "\(filename.validatedFileExtension.uppercased())"
|
var fileDescription = "\(filename.validatedFileExtension.uppercased())"
|
||||||
@@ -55,13 +59,18 @@ struct MediaFileRoomTimelineContent: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
ContentScanningView(contentScannerService: contentScannerService,
|
||||||
|
mediaSource: mediaSource) {
|
||||||
if let onMediaTap {
|
if let onMediaTap {
|
||||||
filePreview
|
filePreview(isScanning: false)
|
||||||
.onTapGesture {
|
.onTapGesture(perform: onMediaTap)
|
||||||
onMediaTap()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
filePreview
|
filePreview(isScanning: false)
|
||||||
|
}
|
||||||
|
} scanningContent: {
|
||||||
|
filePreview(isScanning: true)
|
||||||
|
} unsafeContent: { failure in
|
||||||
|
ContentScanningFailureView(failure: failure)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let formattedCaption {
|
if let formattedCaption {
|
||||||
@@ -76,7 +85,7 @@ struct MediaFileRoomTimelineContent: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var filePreview: some View {
|
func filePreview(isScanning: Bool) -> some View {
|
||||||
Label {
|
Label {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
Text(filename)
|
Text(filename)
|
||||||
@@ -90,8 +99,15 @@ struct MediaFileRoomTimelineContent: View {
|
|||||||
.foregroundStyle(.compound.textPrimary)
|
.foregroundStyle(.compound.textPrimary)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
} icon: {
|
} icon: {
|
||||||
|
Group {
|
||||||
|
if isScanning {
|
||||||
|
ProgressView()
|
||||||
|
.scaledFrame(size: CompoundIcon.Size.medium.value, relativeTo: .body)
|
||||||
|
} else {
|
||||||
CompoundIcon(icon, size: .medium, relativeTo: .body)
|
CompoundIcon(icon, size: .medium, relativeTo: .body)
|
||||||
.foregroundColor(.compound.iconPrimary)
|
.foregroundColor(.compound.iconPrimary)
|
||||||
|
}
|
||||||
|
}
|
||||||
.scaledPadding(6)
|
.scaledPadding(6)
|
||||||
.background(.compound.iconOnSolidPrimary,
|
.background(.compound.iconOnSolidPrimary,
|
||||||
in: RoundedRectangle(cornerRadius: 4, style: .continuous))
|
in: RoundedRectangle(cornerRadius: 4, style: .continuous))
|
||||||
@@ -105,6 +121,8 @@ struct MediaFileRoomTimelineContent: View {
|
|||||||
|
|
||||||
struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||||
static let viewModel = TimelineViewModel.mock
|
static let viewModel = TimelineViewModel.mock
|
||||||
|
static let scanningViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: nil)))
|
||||||
|
static let unsafeViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: false)))
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(spacing: 20.0) {
|
VStack(spacing: 20.0) {
|
||||||
@@ -126,6 +144,21 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
formattedCaption: "Formatted caption"))
|
formattedCaption: "Formatted caption"))
|
||||||
}
|
}
|
||||||
.environmentObject(viewModel.context)
|
.environmentObject(viewModel.context)
|
||||||
|
|
||||||
|
VStack(spacing: 20.0) {
|
||||||
|
FileRoomTimelineView(timelineItem: makeItem(filename: "scanning.pdf",
|
||||||
|
fileSize: 3 * 1024 * 1024,
|
||||||
|
caption: "The file is being scanned."))
|
||||||
|
.environmentObject(scanningViewModel.context)
|
||||||
|
.environment(\.timelineContext, scanningViewModel.context)
|
||||||
|
|
||||||
|
FileRoomTimelineView(timelineItem: makeItem(filename: "unsafe.pdf",
|
||||||
|
fileSize: 3 * 1024 * 1024,
|
||||||
|
caption: "The file is not safe."))
|
||||||
|
.environmentObject(unsafeViewModel.context)
|
||||||
|
.environment(\.timelineContext, unsafeViewModel.context)
|
||||||
|
}
|
||||||
|
.previewDisplayName("Content Scanner")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func makeItem(filename: String,
|
static func makeItem(filename: String,
|
||||||
@@ -141,7 +174,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
content: .init(filename: filename,
|
content: .init(filename: filename,
|
||||||
caption: caption,
|
caption: caption,
|
||||||
formattedCaption: formattedCaption,
|
formattedCaption: formattedCaption,
|
||||||
source: nil,
|
source: try? MediaSourceProxy(url: .mockMXCFile, mimeType: nil),
|
||||||
fileSize: fileSize,
|
fileSize: fileSize,
|
||||||
thumbnailSource: nil,
|
thumbnailSource: nil,
|
||||||
contentType: nil))
|
contentType: nil))
|
||||||
|
|||||||
+34
-3
@@ -13,13 +13,18 @@ struct ImageRoomTimelineView: View {
|
|||||||
@Environment(\.timelineContext) private var context
|
@Environment(\.timelineContext) private var context
|
||||||
let timelineItem: ImageRoomTimelineItem
|
let timelineItem: ImageRoomTimelineItem
|
||||||
|
|
||||||
|
@State private var contentScanningFailure: ContentScanningFailure?
|
||||||
|
|
||||||
var hasMediaCaption: Bool {
|
var hasMediaCaption: Bool {
|
||||||
timelineItem.content.caption != nil
|
timelineItem.content.caption != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
// The caption sits 8pts below the content scanner failure placeholder, 4pts below the media.
|
||||||
|
VStack(alignment: .leading, spacing: contentScanningFailure == nil ? 4 : 8) {
|
||||||
|
ContentScanningView(contentScannerService: context?.contentScannerService,
|
||||||
|
mediaSource: timelineItem.content.imageInfo.source) {
|
||||||
loadableImage
|
loadableImage
|
||||||
.accessibilityElement(children: .ignore)
|
.accessibilityElement(children: .ignore)
|
||||||
.accessibilityLabel(L10n.commonImage)
|
.accessibilityLabel(L10n.commonImage)
|
||||||
@@ -29,7 +34,22 @@ struct ImageRoomTimelineView: View {
|
|||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
|
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
|
||||||
}
|
}
|
||||||
|
} scanningContent: {
|
||||||
|
placeholder
|
||||||
|
.overlay { ProgressView() }
|
||||||
|
.timelineMediaFrame(imageInfo: timelineItem.content.thumbnailInfo ?? timelineItem.content.imageInfo)
|
||||||
|
} unsafeContent: { failure in
|
||||||
|
ContentScanningFailureView(failure: failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
caption
|
||||||
|
}
|
||||||
|
.onPreferenceChange(ContentScanningFailurePreferenceKey.self) { contentScanningFailure = $0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var caption: some View {
|
||||||
if let attributedCaption = timelineItem.content.formattedCaption {
|
if let attributedCaption = timelineItem.content.formattedCaption {
|
||||||
FormattedBodyText(attributedString: attributedCaption,
|
FormattedBodyText(attributedString: attributedCaption,
|
||||||
trailingReservedSize: timelineItem.trailingReservedSize,
|
trailingReservedSize: timelineItem.trailingReservedSize,
|
||||||
@@ -40,8 +60,6 @@ struct ImageRoomTimelineView: View {
|
|||||||
boostFontSize: timelineItem.shouldBoost)
|
boostFontSize: timelineItem.shouldBoost)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var loadableImage: some View {
|
private var loadableImage: some View {
|
||||||
@@ -75,6 +93,8 @@ struct ImageRoomTimelineView: View {
|
|||||||
|
|
||||||
struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||||
static let viewModel = TimelineViewModel.mock
|
static let viewModel = TimelineViewModel.mock
|
||||||
|
static let scanningViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: nil)))
|
||||||
|
static let unsafeViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: false)))
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@@ -93,6 +113,17 @@ struct ImageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
.environment(\.timelineContext, viewModel.context)
|
.environment(\.timelineContext, viewModel.context)
|
||||||
.previewLayout(.fixed(width: 390, height: 1200))
|
.previewLayout(.fixed(width: 390, height: 1200))
|
||||||
.padding(.bottom, 20)
|
.padding(.bottom, 20)
|
||||||
|
|
||||||
|
VStack(spacing: 20.0) {
|
||||||
|
ImageRoomTimelineView(timelineItem: makeTimelineItem())
|
||||||
|
.environmentObject(scanningViewModel.context)
|
||||||
|
.environment(\.timelineContext, scanningViewModel.context)
|
||||||
|
ImageRoomTimelineView(timelineItem: makeTimelineItem(caption: "This is an unsafe image."))
|
||||||
|
.environmentObject(unsafeViewModel.context)
|
||||||
|
.environment(\.timelineContext, unsafeViewModel.context)
|
||||||
|
}
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
|
.previewDisplayName("Content Scanner")
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func makeTimelineItem(caption: String? = nil, isEdited: Bool = false) -> ImageRoomTimelineItem {
|
private static func makeTimelineItem(caption: String? = nil, isEdited: Bool = false) -> ImageRoomTimelineItem {
|
||||||
|
|||||||
+35
-14
@@ -15,6 +15,8 @@ struct StickerRoomTimelineView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
|
ContentScanningView(contentScannerService: context?.contentScannerService,
|
||||||
|
mediaSource: timelineItem.imageInfo.source) {
|
||||||
LoadableImage(mediaSource: timelineItem.imageInfo.source,
|
LoadableImage(mediaSource: timelineItem.imageInfo.source,
|
||||||
mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID),
|
mediaType: .timelineItem(uniqueID: timelineItem.id.uniqueID),
|
||||||
blurhash: timelineItem.blurhash,
|
blurhash: timelineItem.blurhash,
|
||||||
@@ -28,6 +30,13 @@ struct StickerRoomTimelineView: View {
|
|||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
|
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
|
||||||
}
|
}
|
||||||
|
} scanningContent: {
|
||||||
|
placeholder
|
||||||
|
.overlay { ProgressView() }
|
||||||
|
.timelineMediaFrame(imageInfo: timelineItem.imageInfo)
|
||||||
|
} unsafeContent: { failure in
|
||||||
|
ContentScanningFailureView(failure: failure)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,29 +49,41 @@ struct StickerRoomTimelineView: View {
|
|||||||
|
|
||||||
struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
struct StickerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||||
static let viewModel = TimelineViewModel.mock
|
static let viewModel = TimelineViewModel.mock
|
||||||
|
static let scanningViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: nil)))
|
||||||
|
static let unsafeViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: false)))
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(spacing: 20.0) {
|
VStack(spacing: 20.0) {
|
||||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent,
|
StickerRoomTimelineView(timelineItem: makeTimelineItem(body: "Some image"))
|
||||||
body: "Some image",
|
|
||||||
timestamp: .mock,
|
|
||||||
isOutgoing: false,
|
|
||||||
isEditable: false,
|
|
||||||
canBeRepliedTo: true,
|
|
||||||
sender: .init(id: "Bob"),
|
|
||||||
imageInfo: .mockImage))
|
|
||||||
|
|
||||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: .randomEvent,
|
StickerRoomTimelineView(timelineItem: makeTimelineItem(body: "Blurhashed image",
|
||||||
body: "Blurhashed image",
|
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))
|
||||||
|
}
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
|
.environment(\.timelineContext, viewModel.context)
|
||||||
|
|
||||||
|
VStack(spacing: 20.0) {
|
||||||
|
StickerRoomTimelineView(timelineItem: makeTimelineItem(body: "Scanning image"))
|
||||||
|
.environmentObject(scanningViewModel.context)
|
||||||
|
.environment(\.timelineContext, scanningViewModel.context)
|
||||||
|
|
||||||
|
StickerRoomTimelineView(timelineItem: makeTimelineItem(body: "Unsafe image"))
|
||||||
|
.environmentObject(unsafeViewModel.context)
|
||||||
|
.environment(\.timelineContext, unsafeViewModel.context)
|
||||||
|
}
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
|
.previewDisplayName("Content Scanner")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeTimelineItem(body: String, blurhash: String? = nil) -> StickerRoomTimelineItem {
|
||||||
|
StickerRoomTimelineItem(id: .randomEvent,
|
||||||
|
body: body,
|
||||||
timestamp: .mock,
|
timestamp: .mock,
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
canBeRepliedTo: true,
|
canBeRepliedTo: true,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
imageInfo: .mockImage,
|
imageInfo: .mockImage,
|
||||||
blurhash: "L%KUc%kqS$RP?Ks,WEf8OlrqaekW"))
|
blurhash: blurhash)
|
||||||
}
|
|
||||||
.environmentObject(viewModel.context)
|
|
||||||
.environment(\.timelineContext, viewModel.context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-1
@@ -13,13 +13,18 @@ struct VideoRoomTimelineView: View {
|
|||||||
@Environment(\.timelineContext) private var context
|
@Environment(\.timelineContext) private var context
|
||||||
let timelineItem: VideoRoomTimelineItem
|
let timelineItem: VideoRoomTimelineItem
|
||||||
|
|
||||||
|
@State private var contentScanningFailure: ContentScanningFailure?
|
||||||
|
|
||||||
private var hasMediaCaption: Bool {
|
private var hasMediaCaption: Bool {
|
||||||
timelineItem.content.caption != nil
|
timelineItem.content.caption != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
// The caption sits 8pts below the content scanner failure placeholder, 4pts below the media.
|
||||||
|
VStack(alignment: .leading, spacing: contentScanningFailure == nil ? 4 : 8) {
|
||||||
|
ContentScanningView(contentScannerService: context?.contentScannerService,
|
||||||
|
mediaSource: timelineItem.content.videoInfo.source) {
|
||||||
thumbnail
|
thumbnail
|
||||||
.timelineMediaFrame(imageInfo: timelineItem.content.thumbnailInfo)
|
.timelineMediaFrame(imageInfo: timelineItem.content.thumbnailInfo)
|
||||||
.accessibilityElement(children: .ignore)
|
.accessibilityElement(children: .ignore)
|
||||||
@@ -30,6 +35,13 @@ struct VideoRoomTimelineView: View {
|
|||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
|
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
|
||||||
}
|
}
|
||||||
|
} scanningContent: {
|
||||||
|
placeholder
|
||||||
|
.overlay { ProgressView() }
|
||||||
|
.timelineMediaFrame(imageInfo: timelineItem.content.thumbnailInfo)
|
||||||
|
} unsafeContent: { failure in
|
||||||
|
ContentScanningFailureView(failure: failure)
|
||||||
|
}
|
||||||
|
|
||||||
if let attributedCaption = timelineItem.content.formattedCaption {
|
if let attributedCaption = timelineItem.content.formattedCaption {
|
||||||
FormattedBodyText(attributedString: attributedCaption,
|
FormattedBodyText(attributedString: attributedCaption,
|
||||||
@@ -41,6 +53,7 @@ struct VideoRoomTimelineView: View {
|
|||||||
boostFontSize: timelineItem.shouldBoost)
|
boostFontSize: timelineItem.shouldBoost)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onPreferenceChange(ContentScanningFailurePreferenceKey.self) { contentScanningFailure = $0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +96,8 @@ struct VideoRoomTimelineView: View {
|
|||||||
|
|
||||||
struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||||
static let viewModel = TimelineViewModel.mock
|
static let viewModel = TimelineViewModel.mock
|
||||||
|
static let scanningViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: nil)))
|
||||||
|
static let unsafeViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: false)))
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@@ -101,6 +116,17 @@ struct VideoRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
.environment(\.timelineContext, viewModel.context)
|
.environment(\.timelineContext, viewModel.context)
|
||||||
.previewLayout(.fixed(width: 390, height: 975))
|
.previewLayout(.fixed(width: 390, height: 975))
|
||||||
.padding(.bottom, 20)
|
.padding(.bottom, 20)
|
||||||
|
|
||||||
|
VStack(spacing: 20.0) {
|
||||||
|
VideoRoomTimelineView(timelineItem: makeTimelineItem())
|
||||||
|
.environmentObject(scanningViewModel.context)
|
||||||
|
.environment(\.timelineContext, scanningViewModel.context)
|
||||||
|
VideoRoomTimelineView(timelineItem: makeTimelineItem(caption: "This is an unsafe video."))
|
||||||
|
.environmentObject(unsafeViewModel.context)
|
||||||
|
.environment(\.timelineContext, unsafeViewModel.context)
|
||||||
|
}
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
|
.previewDisplayName("Content Scanner")
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func makeTimelineItem(caption: String? = nil, isEdited: Bool = false) -> VideoRoomTimelineItem {
|
private static func makeTimelineItem(caption: String? = nil, isEdited: Bool = false) -> VideoRoomTimelineItem {
|
||||||
|
|||||||
+29
-1
@@ -30,12 +30,25 @@ struct VoiceMessageRoomTimelineContent: View {
|
|||||||
let playerState: AudioPlayerState
|
let playerState: AudioPlayerState
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
ContentScanningView(contentScannerService: context?.contentScannerService,
|
||||||
|
mediaSource: timelineItem.content.source) {
|
||||||
VoiceMessageRoomPlaybackView(playerState: playerState,
|
VoiceMessageRoomPlaybackView(playerState: playerState,
|
||||||
onPlayPause: onPlaybackPlayPause,
|
onPlayPause: onPlaybackPlayPause,
|
||||||
onSeek: { onPlaybackSeek($0) },
|
onSeek: { onPlaybackSeek($0) },
|
||||||
onScrubbing: { onPlaybackScrubbing($0) },
|
onScrubbing: { onPlaybackScrubbing($0) },
|
||||||
onPlaybackSpeedChange: onPlaybackSpeedChange)
|
onPlaybackSpeedChange: onPlaybackSpeedChange)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
} scanningContent: {
|
||||||
|
VoiceMessageRoomPlaybackView(playerState: playerState,
|
||||||
|
isScanning: true,
|
||||||
|
onPlayPause: { },
|
||||||
|
onSeek: { _ in },
|
||||||
|
onScrubbing: { _ in },
|
||||||
|
onPlaybackSpeedChange: { })
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
} unsafeContent: { failure in
|
||||||
|
ContentScanningFailureView(failure: failure)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onPlaybackPlayPause() {
|
private func onPlaybackPlayPause() {
|
||||||
@@ -67,6 +80,8 @@ struct VoiceMessageRoomTimelineContent: View {
|
|||||||
|
|
||||||
struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||||
static let viewModel = TimelineViewModel.mock
|
static let viewModel = TimelineViewModel.mock
|
||||||
|
static let scanningViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: nil)))
|
||||||
|
static let unsafeViewModel = TimelineViewModel.mock(contentScannerService: ContentScannerServiceMock(.init(scanResult: false)))
|
||||||
static let timelineItemIdentifier = TimelineItemIdentifier.randomEvent
|
static let timelineItemIdentifier = TimelineItemIdentifier.randomEvent
|
||||||
static let voiceRoomTimelineItem = VoiceMessageRoomTimelineItem(id: timelineItemIdentifier,
|
static let voiceRoomTimelineItem = VoiceMessageRoomTimelineItem(id: timelineItemIdentifier,
|
||||||
timestamp: .mock,
|
timestamp: .mock,
|
||||||
@@ -77,7 +92,7 @@ struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
content: .init(filename: "audio.ogg",
|
content: .init(filename: "audio.ogg",
|
||||||
duration: 300,
|
duration: 300,
|
||||||
waveform: EstimatedWaveform.mockWaveform,
|
waveform: EstimatedWaveform.mockWaveform,
|
||||||
source: nil,
|
source: try? MediaSourceProxy(url: .mockMXCAudio, mimeType: nil),
|
||||||
fileSize: nil,
|
fileSize: nil,
|
||||||
contentType: nil))
|
contentType: nil))
|
||||||
|
|
||||||
@@ -89,6 +104,19 @@ struct VoiceMessageRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
|||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
body.environmentObject(viewModel.context)
|
body.environmentObject(viewModel.context)
|
||||||
|
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
VoiceMessageRoomTimelineView(timelineItem: voiceRoomTimelineItem, playerState: playerState)
|
||||||
|
.environmentObject(scanningViewModel.context)
|
||||||
|
.environment(\.timelineContext, scanningViewModel.context)
|
||||||
|
|
||||||
|
VoiceMessageRoomTimelineView(timelineItem: voiceRoomTimelineItem, playerState: playerState)
|
||||||
|
.environmentObject(unsafeViewModel.context)
|
||||||
|
.environment(\.timelineContext, unsafeViewModel.context)
|
||||||
|
}
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
|
.previewDisplayName("Content Scanner")
|
||||||
}
|
}
|
||||||
|
|
||||||
static var body: some View {
|
static var body: some View {
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ struct VoiceMessageRoomPlaybackView: View {
|
|||||||
@ScaledMetric private var waveformLinePadding = 2.0
|
@ScaledMetric private var waveformLinePadding = 2.0
|
||||||
@GestureState var isDragging = false
|
@GestureState var isDragging = false
|
||||||
|
|
||||||
|
/// Whether the voice message is being scanned by the content scanner, in which case
|
||||||
|
/// the play button shows a spinner and playback isn't available (which prevents the audio file from being downloaded).
|
||||||
|
var isScanning = false
|
||||||
|
|
||||||
let onPlayPause: () -> Void
|
let onPlayPause: () -> Void
|
||||||
let onSeek: (Double) -> Void
|
let onSeek: (Double) -> Void
|
||||||
let onScrubbing: (Bool) -> Void
|
let onScrubbing: (Bool) -> Void
|
||||||
@@ -23,7 +27,7 @@ struct VoiceMessageRoomPlaybackView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
VoiceMessageButton(state: .init(playerState.playerButtonPlaybackState),
|
VoiceMessageButton(state: isScanning ? .loading : .init(playerState.playerButtonPlaybackState),
|
||||||
size: .medium,
|
size: .medium,
|
||||||
action: onPlayPause)
|
action: onPlayPause)
|
||||||
VStack(spacing: 2) {
|
VStack(spacing: 2) {
|
||||||
|
|||||||
@@ -172,6 +172,13 @@ extension PreviewTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func contentScanningFailureView() async throws {
|
||||||
|
for (index, preview) in ContentScanningFailureView_Previews._allPreviews.enumerated() {
|
||||||
|
try await assertSnapshots(matching: preview, step: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func copyTextButton() async throws {
|
func copyTextButton() async throws {
|
||||||
for (index, preview) in CopyTextButton_Previews._allPreviews.enumerated() {
|
for (index, preview) in CopyTextButton_Previews._allPreviews.enumerated() {
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:65daa24e31bd0cf9f8b729a4d725cf5cb798d58bd955e1e2fe18d367c6ac22f7
|
||||||
|
size 111447
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5e505ab7d0a5fdc71a8e1bf07e3cba7b053a71dafdfe4ffade41ea6dcb7bb0e7
|
||||||
|
size 119312
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e97574e905757bab6d1c37b0b74a3f1dea65d8414f7f229842d6a37335185360
|
||||||
|
size 67391
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:39d3a1d25f637f602cefde236633ac8443a5dc4f5775c9deb0b4256e9e117d71
|
||||||
|
size 77453
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7de83b005f4c5290c77e0b270c955f67670a4edf8184d191c091ae3d8b934534
|
||||||
|
size 89345
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:00af60cc60b8e60dbe765cefd9d75f5548d4e7008053795cd647291a21fa0e24
|
||||||
|
size 103800
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5b6e06014fdfe62eb843da48f30d105e2b4588eb43a2f6be70a4408b4343c792
|
||||||
|
size 46916
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cfa2cf6ed783a1bfa57949c21305ba2f32d77c6dd9fd616721e11c9612030ef4
|
||||||
|
size 59329
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9cf7cea65331f4916582cc81f765efb2f065a3b0905b9c2a0737a2a97ee76fea
|
||||||
|
size 110083
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:abcc9c5a4e9bcfad13f04f221bc52c6594583e41b190156f7daf0dc8dddfb6d9
|
||||||
|
size 117971
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:264fd91dbadbd8f8c42b43b9767e9c948e6ce7433accf15649299df5f6da7c7b
|
||||||
|
size 65882
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:bb5597e4068057e68013e7c02490b455871ebc2ec2c5c67fd65d356c72d9eca3
|
||||||
|
size 75424
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:c7a42aff861071be9f97b2c735cae29eea94665e42fd6be9bc9df49d9db3457c
|
||||||
|
size 102620
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:080a0d612ea42f8559da49a3a190b443db6a110a0933717d18fc5adfa423c9da
|
||||||
|
size 109680
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:147f7f2fd61429eff7e1368231aa43b56ac7373617c90f9ab91aaa8da3135978
|
||||||
|
size 56664
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:70be34bbd75a2955125202a3411a519106ec08c08a5a6307297daebdd2419901
|
||||||
|
size 67498
|
||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:65ab7c901ea0a4d1b07999e0a0d2df40f0983f5f9b443c61a27d1aa4f413ab96
|
oid sha256:7215700c390160c0d89b1fec6c1c4ba2ad275686f5a55fb1fa6af4e28f852f96
|
||||||
size 104323
|
size 103726
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c25001b8af4b9200f5648ac360d7b26dfd5f7e13dcf6ec9c9b4806eeb34c6186
|
oid sha256:8462b5eb394ebe57d41fc16159ffe6b39025c98081451a088364407908229f01
|
||||||
size 118399
|
size 117683
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5207545bb9767684f0203f37256dc99a66e153cb91b3fcaf0d5fef9484196784
|
oid sha256:733c3d43ee0571f40777c5378f6d32c3ccc30fea859e0b5a10abfb8650fa2b8d
|
||||||
size 57926
|
size 57436
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2cb1214f55b480046b05fa515a485c621f6721c9978d966cbf615fbe966d7612
|
oid sha256:4c245c89d9db7ceba59196857775bb66d3c00c75bb6261b498a58df982114e67
|
||||||
size 79324
|
size 77385
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9a74be5f10b6069527d4835d02bdb361c8ca42d059ea7c9254d318ae09b77f4e
|
oid sha256:f8505af6d122ebcb782c6d3d9fed36c17825dce88ae115e98a517ed43ff6f7b5
|
||||||
size 104457
|
size 103827
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b4cb2770329ff98b1935fde6c93e0e81a51d6ae19a608b5579edcad838b64d8c
|
oid sha256:41f522c0b39d6162f2e16f56fb899fec74a1d5eaca426031aea1127da940ef27
|
||||||
size 119070
|
size 118132
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:891e7337c733fc6b4a0ad5d6bd814968ea1aee4816615228e74624d4b9fd33ed
|
oid sha256:fb361a6b20735c980735cb59d17f5ff1903f597e97080e4c5ce71f61145e6069
|
||||||
size 58362
|
size 57696
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fedffe6f1d37153701a1b9367a2d741689f6492e397a5417389a3d5c34f675a7
|
oid sha256:cf8f5d300e94cb941735f4d33720b35225b3e45e92629a6f4a683f9778fc5942
|
||||||
size 80092
|
size 77665
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2bd069ba0540754cfb30969e0f4ce2e94de631d58646ac35fe8d7c66662bc03e
|
oid sha256:108c5554332df7a50211e69d1f398355ae343963c97c93bdfd9342f1c1e528b4
|
||||||
size 104513
|
size 103871
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0507c35e740560f65a1bdfb8b7dc8abf382fedb6f70ff18e537b33b929694f31
|
oid sha256:e84ceb1dd61f72a9c3ec94cd8962cf458149f3dde45f3849706d82f1cf947b28
|
||||||
size 117768
|
size 116996
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:97b455334031a640da9ece701875e4cce8194b4ef59dee4564e269a0cd010424
|
oid sha256:c9b73a4c47938dd9258d9c02f255476dc0f6aeebf75ed79ea984ca3d3a4d2320
|
||||||
size 59203
|
size 58541
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b4b5fc3bfeea57dc64896035a7ce6d79df38e6d56ad1726f264ba8f1e7fb871e
|
oid sha256:a262372a675ee77610959346887fa7fd760913047a1a84ef9c22159e45f1f82a
|
||||||
size 81753
|
size 79519
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2bd069ba0540754cfb30969e0f4ce2e94de631d58646ac35fe8d7c66662bc03e
|
oid sha256:108c5554332df7a50211e69d1f398355ae343963c97c93bdfd9342f1c1e528b4
|
||||||
size 104513
|
size 103871
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0507c35e740560f65a1bdfb8b7dc8abf382fedb6f70ff18e537b33b929694f31
|
oid sha256:e84ceb1dd61f72a9c3ec94cd8962cf458149f3dde45f3849706d82f1cf947b28
|
||||||
size 117768
|
size 116996
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:97b455334031a640da9ece701875e4cce8194b4ef59dee4564e269a0cd010424
|
oid sha256:c9b73a4c47938dd9258d9c02f255476dc0f6aeebf75ed79ea984ca3d3a4d2320
|
||||||
size 59203
|
size 58541
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b4b5fc3bfeea57dc64896035a7ce6d79df38e6d56ad1726f264ba8f1e7fb871e
|
oid sha256:a262372a675ee77610959346887fa7fd760913047a1a84ef9c22159e45f1f82a
|
||||||
size 81753
|
size 79519
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ecc3fbf8b73c07a7be69f2668d5c97dfa406b74df8175557ed43ab10ab069f30
|
oid sha256:82c84eba438609cf64f67469d57584eb03c7d4a8eabbebdb1e8fbea4f60bd16c
|
||||||
size 104663
|
size 104083
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3f705d62927a092377f67a21bd2d6c0605d7c8213138aa368386ad4496a052d0
|
oid sha256:a1cb5963c1867d5dbd38d91667b98ffab6b6a83d474949ea258e6d4d609ae578
|
||||||
size 119587
|
size 118840
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5add993fc38b9e25bd8f33269fe69c3cd6424f9ac4f1834abad4121e20cc35fa
|
oid sha256:cd5e8a0682249277ce704a6a23cda13ebeea5ee0bf0656c84978a0f1f3776050
|
||||||
size 59200
|
size 58524
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2cdae536ffa9143954cae633e5900ad93d5eacf21057a8c89c289f379b3507ef
|
oid sha256:fe2feb22eda8e2cc79d3a169ef535b12604ea3ec59e8f7677fe6af5fa11d0fac
|
||||||
size 82372
|
size 78846
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ecc3fbf8b73c07a7be69f2668d5c97dfa406b74df8175557ed43ab10ab069f30
|
oid sha256:82c84eba438609cf64f67469d57584eb03c7d4a8eabbebdb1e8fbea4f60bd16c
|
||||||
size 104663
|
size 104083
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3f705d62927a092377f67a21bd2d6c0605d7c8213138aa368386ad4496a052d0
|
oid sha256:a1cb5963c1867d5dbd38d91667b98ffab6b6a83d474949ea258e6d4d609ae578
|
||||||
size 119587
|
size 118840
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5add993fc38b9e25bd8f33269fe69c3cd6424f9ac4f1834abad4121e20cc35fa
|
oid sha256:cd5e8a0682249277ce704a6a23cda13ebeea5ee0bf0656c84978a0f1f3776050
|
||||||
size 59200
|
size 58524
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2cdae536ffa9143954cae633e5900ad93d5eacf21057a8c89c289f379b3507ef
|
oid sha256:fe2feb22eda8e2cc79d3a169ef535b12604ea3ec59e8f7677fe6af5fa11d0fac
|
||||||
size 82372
|
size 78846
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b1c3a092778dd89916edf9e1762b189d618334c62916d430dcb7f02a26da0e9d
|
oid sha256:be58f78685af914af8ef1f8a5b3e6c1c5acf08fac266011fdbe46863f3c1bc06
|
||||||
size 111447
|
size 110771
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:45b3b85dd9a96d903bf599b837dfe3a23d956c7989af40cbacfffa8283fb748a
|
oid sha256:56d1d8fad0878c1436f0854e103a2203113ce7b7f6a51ee18726971dcc376c22
|
||||||
size 126731
|
size 126048
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:70628211cd73a2d4e39f58467d577d5685126f90da9ba4b9413b90ed884e05c2
|
oid sha256:e4fa3d1c6d1363c4168fca71292ccffcf5f13686cd6b8bd9341bffad37aa1d25
|
||||||
size 65180
|
size 64572
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:63dcabfca6d5e294bc49e5518be0f8d3d65fbdbbe7e9ee59327137ced88c4575
|
oid sha256:9a3aca0d1cbb658a1cb7632101a935a144e514a899eb0a2db1b01f8fa30a32b3
|
||||||
size 93723
|
size 90503
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b1c3a092778dd89916edf9e1762b189d618334c62916d430dcb7f02a26da0e9d
|
oid sha256:be58f78685af914af8ef1f8a5b3e6c1c5acf08fac266011fdbe46863f3c1bc06
|
||||||
size 111447
|
size 110771
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:45b3b85dd9a96d903bf599b837dfe3a23d956c7989af40cbacfffa8283fb748a
|
oid sha256:56d1d8fad0878c1436f0854e103a2203113ce7b7f6a51ee18726971dcc376c22
|
||||||
size 126731
|
size 126048
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:70628211cd73a2d4e39f58467d577d5685126f90da9ba4b9413b90ed884e05c2
|
oid sha256:e4fa3d1c6d1363c4168fca71292ccffcf5f13686cd6b8bd9341bffad37aa1d25
|
||||||
size 65180
|
size 64572
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:63dcabfca6d5e294bc49e5518be0f8d3d65fbdbbe7e9ee59327137ced88c4575
|
oid sha256:9a3aca0d1cbb658a1cb7632101a935a144e514a899eb0a2db1b01f8fa30a32b3
|
||||||
size 93723
|
size 90503
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:69ddd3c435d8c50a231bdc128c059127219d35624a16912579ce0f878830a274
|
|
||||||
size 148724
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:680e82d9b7877d868e734783ac7d9dc9c615d7c53a80b816e7529cbda6e3be0d
|
|
||||||
size 149586
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:1a419004009267be00c3bb862408e82639e2fb45f6951e77da9309b1245f046d
|
|
||||||
size 103648
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:5a6a0b062ae788f675ada0f9011ff70dea366bf36c6b5d5a13a5bd909a03bc6c
|
|
||||||
size 104605
|
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f6910db0f35343c964a3263bb875e73330f5e5ad32ebb5a57a3fe739c1719df3
|
||||||
|
size 97392
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cbfd6addca1299a033ed07d2ba940b894db4529966790e5dac46a337c1ec1d1b
|
||||||
|
size 104975
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7c9b49824321958e528210f9615f2e29048f6f73167d49f6d72dc9eded795857
|
||||||
|
size 52714
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f393472b85e6c6e1174d2e1eaee22731cfa2fb623d50b6bff4c8dc9ee3e5e9d3
|
||||||
|
size 62543
|
||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:86a734b74a8d7b4fae4c9455f5ef6f449c6804b7dec96e0f362eaaf6bae2c3fe
|
oid sha256:e39edab07bcc2e86378cd7b20ca03c57118ec04f5548a5cda729680e01ef3ee6
|
||||||
size 107948
|
size 136868
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ca5ec021c85aa9c24c682c5db929b73f7baa56d4c6dcb3e04f06e17ab33836e5
|
oid sha256:f7cb8dc14692fa2817766fe8036a37b0a1d8e7a2d2fe45201cd25a2612e73a56
|
||||||
size 109542
|
size 145143
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:33ee078088ed44e3497cf33a2cc72eeb4a0bbd3c8537eb0b1a82d18c11264f0a
|
oid sha256:2a91badc1b4d5a0227464ddba3fb35e757a64365c9599e122697beb62f4361be
|
||||||
size 66260
|
size 93094
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:637d6d91ec261adfc56c5202aeb9a23f46c89bdc119c9e1cbe9989c49060ca4b
|
oid sha256:3f583508ac5f41bb91a5000610b0419e3bb3315ef23c8262853fd887ba320678
|
||||||
size 67367
|
size 103536
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:492dd038b467f4db9661e9f2cb721022f9f837103f4130c27e71fb03e5cdfbb2
|
||||||
|
size 103177
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b87d8652edb8c23a296dc9dbabf1a5b054d1df6abdbaebd32cd8fd11a2f43805
|
||||||
|
size 110250
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:300c06ba36a0bdd6e54e569dc969d32c3c0ec3bef6074542b0d4287bb441622d
|
||||||
|
size 56189
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:138c4afce0d265e31e7ad46f0ceb3a0ef0c69ea62577af0c6b714a838727ad81
|
||||||
|
size 66911
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:627f65806c9f2a8b4a81b35c3d1abde1e5f87de3d88cbd2c82fc65a2475a701b
|
||||||
|
size 99605
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:dff6c0041d79e8c2a21ee363163761cc851b8f427516c3bfeb83a322e25a704c
|
||||||
|
size 111051
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f7330ef6fcdf672e4880b84286eb369303de4590f2aa99a198193fb3bae1dd90
|
||||||
|
size 56283
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6b5038008c1a41a73b548990ec5e0bc8ba9147ae5f4a666242eb81fc965d841b
|
||||||
|
size 64558
|
||||||
@@ -27,7 +27,7 @@ public struct CompoundIcon: View {
|
|||||||
/// A custom icon size.
|
/// A custom icon size.
|
||||||
case custom(CGFloat)
|
case custom(CGFloat)
|
||||||
|
|
||||||
var value: CGFloat {
|
public var value: CGFloat {
|
||||||
switch self {
|
switch self {
|
||||||
case .xSmall: return 16
|
case .xSmall: return 16
|
||||||
case .small: return 20
|
case .small: return 20
|
||||||
|
|||||||
Reference in New Issue
Block a user