mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Implement Knock Logic (#3573)
* WIP RequestToJoin struct * implemented the logic to display the cells * knock request banner accept flow * mark all knocks as seen implemented * details logic * implemented accept, decline and ban in the list * added a loader and modified the stacked view of the banner * pr suggestions * updated naming and loading strings * added the initial loading state improved code and the tests * updated a string that has changed * code improvement * tests for the room screen view model * room details tests and improved the knock requests tests for the room screen * knock requests list tests * added error state alerts with retry * struct has been renamed on the sdk so I renamed it also on the app side * update SDK
This commit is contained in:
parent
016cdc687a
commit
45a630dd85
@ -70,6 +70,7 @@
|
|||||||
0A0625A271EE5B06D2AAA069 /* HomeScreenSlidingSyncMigrationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4691B8DE1D51DE152680098A /* HomeScreenSlidingSyncMigrationBanner.swift */; };
|
0A0625A271EE5B06D2AAA069 /* HomeScreenSlidingSyncMigrationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4691B8DE1D51DE152680098A /* HomeScreenSlidingSyncMigrationBanner.swift */; };
|
||||||
0A194F5E70B5A628C1BF4476 /* AdvancedSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */; };
|
0A194F5E70B5A628C1BF4476 /* AdvancedSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */; };
|
||||||
0ACAA31FD0399CEEBA3ECC21 /* UserDetailsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */; };
|
0ACAA31FD0399CEEBA3ECC21 /* UserDetailsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */; };
|
||||||
|
0AD8EF040A60D62F488C18B5 /* KnockRequestProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F957320D0EB7D7B4E30C79D /* KnockRequestProxyMock.swift */; };
|
||||||
0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; };
|
0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; };
|
||||||
0BAF83521871E69D222EE8E4 /* ClientBuilderHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */; };
|
0BAF83521871E69D222EE8E4 /* ClientBuilderHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC0CD1CAFD3F8B057F9AEA5 /* ClientBuilderHook.swift */; };
|
||||||
0BDA19079FD6E17C5AC62E22 /* RoomDetailsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */; };
|
0BDA19079FD6E17C5AC62E22 /* RoomDetailsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */; };
|
||||||
@ -139,6 +140,7 @@
|
|||||||
18E3786918486D4C9726BC84 /* FormButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89FBFC09F9DAFF1E4BA97849 /* FormButtonStyles.swift */; };
|
18E3786918486D4C9726BC84 /* FormButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89FBFC09F9DAFF1E4BA97849 /* FormButtonStyles.swift */; };
|
||||||
18FDE4ED6D83B0771452B43D /* RoomSelectionScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F104596B0620CEFE5DFD31B1 /* RoomSelectionScreenCoordinator.swift */; };
|
18FDE4ED6D83B0771452B43D /* RoomSelectionScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F104596B0620CEFE5DFD31B1 /* RoomSelectionScreenCoordinator.swift */; };
|
||||||
192A3CDCD0174AD1E4A128E4 /* AudioRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */; };
|
192A3CDCD0174AD1E4A128E4 /* AudioRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */; };
|
||||||
|
194585F6CD77242B36D4ADF1 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADECBBB672497BCD4822468 /* Result.swift */; };
|
||||||
1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; };
|
1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; };
|
||||||
197441F1EF23A5DABACCA79F /* StickerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5338450E6783A576B5C16DD /* StickerRoomTimelineView.swift */; };
|
197441F1EF23A5DABACCA79F /* StickerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5338450E6783A576B5C16DD /* StickerRoomTimelineView.swift */; };
|
||||||
19DED23340D0855B59693ED2 /* VoiceMessageRecorderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45C9EAA86423D7D3126DE4F /* VoiceMessageRecorderProtocol.swift */; };
|
19DED23340D0855B59693ED2 /* VoiceMessageRecorderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D45C9EAA86423D7D3126DE4F /* VoiceMessageRecorderProtocol.swift */; };
|
||||||
@ -646,6 +648,7 @@
|
|||||||
8358D145F9BF94F412BEDCA8 /* RoomRolesAndPermissionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */; };
|
8358D145F9BF94F412BEDCA8 /* RoomRolesAndPermissionsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE7969EBCAF078813E18EA1 /* RoomRolesAndPermissionsScreenModels.swift */; };
|
||||||
83A4DAB181C56987C3E804FF /* MapTilerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */; };
|
83A4DAB181C56987C3E804FF /* MapTilerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */; };
|
||||||
83B17A44D3E7E6DF22D9A2A4 /* RoomModerationRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */; };
|
83B17A44D3E7E6DF22D9A2A4 /* RoomModerationRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B32BBA8887BD7A5C4ECF16F /* RoomModerationRole.swift */; };
|
||||||
|
83D519C509F0F76EDBB60455 /* KnockRequestProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F062DD2CCD95DC33528A16F /* KnockRequestProxy.swift */; };
|
||||||
84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; };
|
84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; };
|
||||||
8446C2A7ECEFDA79F622725F /* TimelineReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AD70D6E03D2031AE1B5A52 /* TimelineReactionsView.swift */; };
|
8446C2A7ECEFDA79F622725F /* TimelineReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AD70D6E03D2031AE1B5A52 /* TimelineReactionsView.swift */; };
|
||||||
8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */; };
|
8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */; };
|
||||||
@ -1025,6 +1028,7 @@
|
|||||||
D10BA4F041DC58580A440A32 /* RoomRolesAndPermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B1DC3B3FB40A7F4AE9B7BF /* RoomRolesAndPermissionsScreen.swift */; };
|
D10BA4F041DC58580A440A32 /* RoomRolesAndPermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B1DC3B3FB40A7F4AE9B7BF /* RoomRolesAndPermissionsScreen.swift */; };
|
||||||
D12F440F7973F1489F61389D /* NotificationSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F64447FF544298A6A3BEF85 /* NotificationSettingsScreenModels.swift */; };
|
D12F440F7973F1489F61389D /* NotificationSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F64447FF544298A6A3BEF85 /* NotificationSettingsScreenModels.swift */; };
|
||||||
D181AC8FF236B7F91C0A8C28 /* MapTiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AA3F4B285570805CB0CCDD /* MapTiler.swift */; };
|
D181AC8FF236B7F91C0A8C28 /* MapTiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AA3F4B285570805CB0CCDD /* MapTiler.swift */; };
|
||||||
|
D18B70975644C24F60656C0D /* KnockRequestProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07851F4EA81AA3339806A7B /* KnockRequestProxyProtocol.swift */; };
|
||||||
D19A748E95E2FAB2940570F0 /* CallScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4103AB4340F2974D690A12A /* CallScreen.swift */; };
|
D19A748E95E2FAB2940570F0 /* CallScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4103AB4340F2974D690A12A /* CallScreen.swift */; };
|
||||||
D2048FD56760BDABA3DB5FC2 /* AppLockServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */; };
|
D2048FD56760BDABA3DB5FC2 /* AppLockServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */; };
|
||||||
D22345698F6548C1EE960940 /* IdentityConfirmedScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBE70FFB7936F35811772C1 /* IdentityConfirmedScreenModels.swift */; };
|
D22345698F6548C1EE960940 /* IdentityConfirmedScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBE70FFB7936F35811772C1 /* IdentityConfirmedScreenModels.swift */; };
|
||||||
@ -1906,6 +1910,7 @@
|
|||||||
7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomInviterLabel.swift; sourceTree = "<group>"; };
|
7EB58E4E8D6D634C246AD5C2 /* RoomInviterLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomInviterLabel.swift; sourceTree = "<group>"; };
|
||||||
7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelTests.swift; sourceTree = "<group>"; };
|
7EECE8B331CD169790EF284F /* BugReportScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceProtocol.swift; sourceTree = "<group>"; };
|
7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
7F957320D0EB7D7B4E30C79D /* KnockRequestProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestProxyMock.swift; sourceTree = "<group>"; };
|
||||||
7FB2253D36E81E045E1CB432 /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = "<group>"; };
|
7FB2253D36E81E045E1CB432 /* Duration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = "<group>"; };
|
||||||
7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenModels.swift; sourceTree = "<group>"; };
|
7FDF541AE914059942B575B4 /* IdentityConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmationScreenModels.swift; sourceTree = "<group>"; };
|
||||||
8063E65441E771200108C558 /* ReadReceiptsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptsSummaryView.swift; sourceTree = "<group>"; };
|
8063E65441E771200108C558 /* ReadReceiptsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceiptsSummaryView.swift; sourceTree = "<group>"; };
|
||||||
@ -1983,6 +1988,7 @@
|
|||||||
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
|
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
|
||||||
8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = "<group>"; };
|
8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = "<group>"; };
|
||||||
8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSDKMock.swift; sourceTree = "<group>"; };
|
8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSDKMock.swift; sourceTree = "<group>"; };
|
||||||
|
8F062DD2CCD95DC33528A16F /* KnockRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestProxy.swift; sourceTree = "<group>"; };
|
||||||
8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = "<group>"; };
|
8F21ED7205048668BEB44A38 /* AppActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppActivityView.swift; sourceTree = "<group>"; };
|
||||||
8F6210134203BE1F2DD5C679 /* RoomDirectoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectoryCell.swift; sourceTree = "<group>"; };
|
8F6210134203BE1F2DD5C679 /* RoomDirectoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectoryCell.swift; sourceTree = "<group>"; };
|
||||||
8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenViewModelTests.swift; sourceTree = "<group>"; };
|
8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangeRolesScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
@ -2219,6 +2225,7 @@
|
|||||||
BFEE91FB8ABB5F5884B6D940 /* WaveformInteractionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformInteractionModifier.swift; sourceTree = "<group>"; };
|
BFEE91FB8ABB5F5884B6D940 /* WaveformInteractionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformInteractionModifier.swift; sourceTree = "<group>"; };
|
||||||
C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXAttributeScope.swift; sourceTree = "<group>"; };
|
C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXAttributeScope.swift; sourceTree = "<group>"; };
|
||||||
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = "<group>"; };
|
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = "<group>"; };
|
||||||
|
C07851F4EA81AA3339806A7B /* KnockRequestProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestProxyProtocol.swift; sourceTree = "<group>"; };
|
||||||
C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelTests.swift; sourceTree = "<group>"; };
|
C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBasedMessageTimelineItemProtocol.swift; sourceTree = "<group>"; };
|
||||||
C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenEmptyStateView.swift; sourceTree = "<group>"; };
|
C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenEmptyStateView.swift; sourceTree = "<group>"; };
|
||||||
@ -2341,6 +2348,7 @@
|
|||||||
DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenUITests.swift; sourceTree = "<group>"; };
|
DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenUITests.swift; sourceTree = "<group>"; };
|
||||||
DA3D82522494E78746B2214E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/SAS.strings; sourceTree = "<group>"; };
|
DA3D82522494E78746B2214E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/SAS.strings; sourceTree = "<group>"; };
|
||||||
DAB8D7926A5684E18196B538 /* VoiceMessageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCache.swift; sourceTree = "<group>"; };
|
DAB8D7926A5684E18196B538 /* VoiceMessageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCache.swift; sourceTree = "<group>"; };
|
||||||
|
DADECBBB672497BCD4822468 /* Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = "<group>"; };
|
||||||
DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreen.swift; sourceTree = "<group>"; };
|
DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreen.swift; sourceTree = "<group>"; };
|
||||||
DBEDCEC9D908C19C63D24395 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
DBEDCEC9D908C19C63D24395 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = "<group>"; };
|
DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = "<group>"; };
|
||||||
@ -3152,6 +3160,7 @@
|
|||||||
3A21027F05874B1BCC3E452B /* InvitedRoomProxyMock.swift */,
|
3A21027F05874B1BCC3E452B /* InvitedRoomProxyMock.swift */,
|
||||||
867DC9530C42F7B5176BE465 /* JoinedRoomProxyMock.swift */,
|
867DC9530C42F7B5176BE465 /* JoinedRoomProxyMock.swift */,
|
||||||
9E8F4D7D61B80EBD5CB92F8A /* KnockedRoomProxyMock.swift */,
|
9E8F4D7D61B80EBD5CB92F8A /* KnockedRoomProxyMock.swift */,
|
||||||
|
7F957320D0EB7D7B4E30C79D /* KnockRequestProxyMock.swift */,
|
||||||
6F65E4BB9E82EB8373207CF8 /* MediaProviderMock.swift */,
|
6F65E4BB9E82EB8373207CF8 /* MediaProviderMock.swift */,
|
||||||
8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */,
|
8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */,
|
||||||
382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */,
|
382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */,
|
||||||
@ -3448,6 +3457,8 @@
|
|||||||
0E95B3BDB80531C85CD50AE6 /* InvitedRoomProxy.swift */,
|
0E95B3BDB80531C85CD50AE6 /* InvitedRoomProxy.swift */,
|
||||||
07C6B0B087FE6601C3F77816 /* JoinedRoomProxy.swift */,
|
07C6B0B087FE6601C3F77816 /* JoinedRoomProxy.swift */,
|
||||||
858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */,
|
858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */,
|
||||||
|
8F062DD2CCD95DC33528A16F /* KnockRequestProxy.swift */,
|
||||||
|
C07851F4EA81AA3339806A7B /* KnockRequestProxyProtocol.swift */,
|
||||||
B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */,
|
B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */,
|
||||||
40A66E8BC8D9AE4A08EFB2DF /* RoomInfoProxy.swift */,
|
40A66E8BC8D9AE4A08EFB2DF /* RoomInfoProxy.swift */,
|
||||||
974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */,
|
974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */,
|
||||||
@ -3527,6 +3538,7 @@
|
|||||||
077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */,
|
077B01C13BBA2996272C5FB5 /* ProcessInfo.swift */,
|
||||||
1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */,
|
1DFE0E493FB55E5A62E7852A /* ProposedViewSize.swift */,
|
||||||
7310D8DFE01AF45F0689C3AA /* Publisher.swift */,
|
7310D8DFE01AF45F0689C3AA /* Publisher.swift */,
|
||||||
|
DADECBBB672497BCD4822468 /* Result.swift */,
|
||||||
584A61D9C459FAFEF038A7C0 /* Section.swift */,
|
584A61D9C459FAFEF038A7C0 /* Section.swift */,
|
||||||
DF17EA323AD0205A6AB621AA /* Snapshotting.swift */,
|
DF17EA323AD0205A6AB621AA /* Snapshotting.swift */,
|
||||||
40B21E611DADDEF00307E7AC /* String.swift */,
|
40B21E611DADDEF00307E7AC /* String.swift */,
|
||||||
@ -7011,6 +7023,9 @@
|
|||||||
FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */,
|
FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */,
|
||||||
CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */,
|
CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */,
|
||||||
2748E5574A1031DD05E54FDA /* KnockRequestCell.swift in Sources */,
|
2748E5574A1031DD05E54FDA /* KnockRequestCell.swift in Sources */,
|
||||||
|
83D519C509F0F76EDBB60455 /* KnockRequestProxy.swift in Sources */,
|
||||||
|
0AD8EF040A60D62F488C18B5 /* KnockRequestProxyMock.swift in Sources */,
|
||||||
|
D18B70975644C24F60656C0D /* KnockRequestProxyProtocol.swift in Sources */,
|
||||||
D5E8EE8A288EFCCF646860EA /* KnockRequestsBannerView.swift in Sources */,
|
D5E8EE8A288EFCCF646860EA /* KnockRequestsBannerView.swift in Sources */,
|
||||||
E8B290CBB7E5FF5E3C1B6124 /* KnockRequestsListEmptyStateView.swift in Sources */,
|
E8B290CBB7E5FF5E3C1B6124 /* KnockRequestsListEmptyStateView.swift in Sources */,
|
||||||
AAA551AD8768309024D4907B /* KnockRequestsListScreen.swift in Sources */,
|
AAA551AD8768309024D4907B /* KnockRequestsListScreen.swift in Sources */,
|
||||||
@ -7212,6 +7227,7 @@
|
|||||||
9A0326D2375075871D2AB537 /* ResolveVerifiedUserSendFailureScreenViewModel.swift in Sources */,
|
9A0326D2375075871D2AB537 /* ResolveVerifiedUserSendFailureScreenViewModel.swift in Sources */,
|
||||||
ED3E91E6166E4923791ACA84 /* ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift in Sources */,
|
ED3E91E6166E4923791ACA84 /* ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift in Sources */,
|
||||||
A494741843F087881299ACF0 /* RestorationToken.swift in Sources */,
|
A494741843F087881299ACF0 /* RestorationToken.swift in Sources */,
|
||||||
|
194585F6CD77242B36D4ADF1 /* Result.swift in Sources */,
|
||||||
6E391F7F628D984AF44385D9 /* RoomAttachmentPicker.swift in Sources */,
|
6E391F7F628D984AF44385D9 /* RoomAttachmentPicker.swift in Sources */,
|
||||||
8587A53DE8EF94FD796DC375 /* RoomAvatarImage.swift in Sources */,
|
8587A53DE8EF94FD796DC375 /* RoomAvatarImage.swift in Sources */,
|
||||||
F8C87130FD999F7F1076208C /* RoomChangePermissionsScreen.swift in Sources */,
|
F8C87130FD999F7F1076208C /* RoomChangePermissionsScreen.swift in Sources */,
|
||||||
@ -8391,7 +8407,7 @@
|
|||||||
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
|
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = exactVersion;
|
kind = exactVersion;
|
||||||
version = 1.0.80;
|
version = 1.0.81;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
|
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
|
||||||
|
@ -149,8 +149,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
|
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "342dc2f1b6553dba7ed5d6f0a330d77d7fae13c4",
|
"revision" : "7c3d3abd370bd416c435790dc0c76999e018529b",
|
||||||
"version" : "1.0.80"
|
"version" : "1.0.81"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -908,7 +908,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func presentKnockRequestsList() {
|
private func presentKnockRequestsList() {
|
||||||
let parameters = KnockRequestsListScreenCoordinatorParameters(roomProxy: roomProxy, mediaProvider: userSession.mediaProvider)
|
let parameters = KnockRequestsListScreenCoordinatorParameters(roomProxy: roomProxy,
|
||||||
|
mediaProvider: userSession.mediaProvider,
|
||||||
|
userIndicatorController: userIndicatorController)
|
||||||
let coordinator = KnockRequestsListScreenCoordinator(parameters: parameters)
|
let coordinator = KnockRequestsListScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
navigationStackCoordinator.push(coordinator) { [weak self] in
|
navigationStackCoordinator.push(coordinator) { [weak self] in
|
||||||
@ -1723,14 +1725,3 @@ private extension RoomFlowCoordinator {
|
|||||||
case dismissSecurityAndPrivacyScreen
|
case dismissSecurityAndPrivacyScreen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension Result {
|
|
||||||
var isFailure: Bool {
|
|
||||||
switch self {
|
|
||||||
case .success:
|
|
||||||
return false
|
|
||||||
case .failure:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -388,7 +388,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
|||||||
case .unknownDevice, .unsignedDevice: .ExpectedSentByInsecureDevice
|
case .unknownDevice, .unsignedDevice: .ExpectedSentByInsecureDevice
|
||||||
case .verificationViolation: .ExpectedVerificationViolation
|
case .verificationViolation: .ExpectedVerificationViolation
|
||||||
case .sentBeforeWeJoined: .ExpectedDueToMembership
|
case .sentBeforeWeJoined: .ExpectedDueToMembership
|
||||||
case .historicalMessage: .HistoricalMessage
|
case .historicalMessageAndBackupIsDisabled, .historicalMessageAndDeviceIsUnverified: .HistoricalMessage
|
||||||
case .withheldForUnverifiedOrInsecureDevice: .RoomKeysWithheldForUnverifiedDevice
|
case .withheldForUnverifiedOrInsecureDevice: .RoomKeysWithheldForUnverifiedDevice
|
||||||
case .withheldBySender: .OlmKeysNotSentError
|
case .withheldBySender: .OlmKeysNotSentError
|
||||||
}
|
}
|
||||||
|
@ -5974,6 +5974,11 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol {
|
|||||||
set(value) { underlyingIdentityStatusChangesPublisher = value }
|
set(value) { underlyingIdentityStatusChangesPublisher = value }
|
||||||
}
|
}
|
||||||
var underlyingIdentityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never>!
|
var underlyingIdentityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never>!
|
||||||
|
var knockRequestsStatePublisher: CurrentValuePublisher<KnockRequestsState, Never> {
|
||||||
|
get { return underlyingKnockRequestsStatePublisher }
|
||||||
|
set(value) { underlyingKnockRequestsStatePublisher = value }
|
||||||
|
}
|
||||||
|
var underlyingKnockRequestsStatePublisher: CurrentValuePublisher<KnockRequestsState, Never>!
|
||||||
var timeline: TimelineProxyProtocol {
|
var timeline: TimelineProxyProtocol {
|
||||||
get { return underlyingTimeline }
|
get { return underlyingTimeline }
|
||||||
set(value) { underlyingTimeline = value }
|
set(value) { underlyingTimeline = value }
|
||||||
@ -9610,6 +9615,284 @@ class KeychainControllerMock: KeychainControllerProtocol {
|
|||||||
removePINCodeBiometricStateClosure?()
|
removePINCodeBiometricStateClosure?()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class KnockRequestProxyMock: KnockRequestProxyProtocol {
|
||||||
|
var eventID: String {
|
||||||
|
get { return underlyingEventID }
|
||||||
|
set(value) { underlyingEventID = value }
|
||||||
|
}
|
||||||
|
var underlyingEventID: String!
|
||||||
|
var userID: String {
|
||||||
|
get { return underlyingUserID }
|
||||||
|
set(value) { underlyingUserID = value }
|
||||||
|
}
|
||||||
|
var underlyingUserID: String!
|
||||||
|
var displayName: String?
|
||||||
|
var avatarURL: URL?
|
||||||
|
var reason: String?
|
||||||
|
var formattedTimestamp: String?
|
||||||
|
var isSeen: Bool {
|
||||||
|
get { return underlyingIsSeen }
|
||||||
|
set(value) { underlyingIsSeen = value }
|
||||||
|
}
|
||||||
|
var underlyingIsSeen: Bool!
|
||||||
|
|
||||||
|
//MARK: - accept
|
||||||
|
|
||||||
|
var acceptUnderlyingCallsCount = 0
|
||||||
|
var acceptCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return acceptUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = acceptUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
acceptUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
acceptUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var acceptCalled: Bool {
|
||||||
|
return acceptCallsCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var acceptUnderlyingReturnValue: Result<Void, KnockRequestProxyError>!
|
||||||
|
var acceptReturnValue: Result<Void, KnockRequestProxyError>! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return acceptUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: Result<Void, KnockRequestProxyError>? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = acceptUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
acceptUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
acceptUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var acceptClosure: (() async -> Result<Void, KnockRequestProxyError>)?
|
||||||
|
|
||||||
|
func accept() async -> Result<Void, KnockRequestProxyError> {
|
||||||
|
acceptCallsCount += 1
|
||||||
|
if let acceptClosure = acceptClosure {
|
||||||
|
return await acceptClosure()
|
||||||
|
} else {
|
||||||
|
return acceptReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - decline
|
||||||
|
|
||||||
|
var declineUnderlyingCallsCount = 0
|
||||||
|
var declineCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return declineUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = declineUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
declineUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
declineUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var declineCalled: Bool {
|
||||||
|
return declineCallsCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var declineUnderlyingReturnValue: Result<Void, KnockRequestProxyError>!
|
||||||
|
var declineReturnValue: Result<Void, KnockRequestProxyError>! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return declineUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: Result<Void, KnockRequestProxyError>? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = declineUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
declineUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
declineUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var declineClosure: (() async -> Result<Void, KnockRequestProxyError>)?
|
||||||
|
|
||||||
|
func decline() async -> Result<Void, KnockRequestProxyError> {
|
||||||
|
declineCallsCount += 1
|
||||||
|
if let declineClosure = declineClosure {
|
||||||
|
return await declineClosure()
|
||||||
|
} else {
|
||||||
|
return declineReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - ban
|
||||||
|
|
||||||
|
var banUnderlyingCallsCount = 0
|
||||||
|
var banCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return banUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = banUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
banUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
banUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var banCalled: Bool {
|
||||||
|
return banCallsCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var banUnderlyingReturnValue: Result<Void, KnockRequestProxyError>!
|
||||||
|
var banReturnValue: Result<Void, KnockRequestProxyError>! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return banUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: Result<Void, KnockRequestProxyError>? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = banUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
banUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
banUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var banClosure: (() async -> Result<Void, KnockRequestProxyError>)?
|
||||||
|
|
||||||
|
func ban() async -> Result<Void, KnockRequestProxyError> {
|
||||||
|
banCallsCount += 1
|
||||||
|
if let banClosure = banClosure {
|
||||||
|
return await banClosure()
|
||||||
|
} else {
|
||||||
|
return banReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - markAsSeen
|
||||||
|
|
||||||
|
var markAsSeenUnderlyingCallsCount = 0
|
||||||
|
var markAsSeenCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return markAsSeenUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = markAsSeenUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
markAsSeenUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
markAsSeenUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var markAsSeenCalled: Bool {
|
||||||
|
return markAsSeenCallsCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var markAsSeenUnderlyingReturnValue: Result<Void, KnockRequestProxyError>!
|
||||||
|
var markAsSeenReturnValue: Result<Void, KnockRequestProxyError>! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return markAsSeenUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: Result<Void, KnockRequestProxyError>? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = markAsSeenUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
markAsSeenUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
markAsSeenUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var markAsSeenClosure: (() async -> Result<Void, KnockRequestProxyError>)?
|
||||||
|
|
||||||
|
func markAsSeen() async -> Result<Void, KnockRequestProxyError> {
|
||||||
|
markAsSeenCallsCount += 1
|
||||||
|
if let markAsSeenClosure = markAsSeenClosure {
|
||||||
|
return await markAsSeenClosure()
|
||||||
|
} else {
|
||||||
|
return markAsSeenReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
class KnockedRoomProxyMock: KnockedRoomProxyProtocol {
|
class KnockedRoomProxyMock: KnockedRoomProxyProtocol {
|
||||||
var info: BaseRoomInfoProxyProtocol {
|
var info: BaseRoomInfoProxyProtocol {
|
||||||
get { return underlyingInfo }
|
get { return underlyingInfo }
|
||||||
|
@ -7938,6 +7938,189 @@ open class InReplyToDetailsSDKMock: MatrixRustSDK.InReplyToDetails {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
open class KnockRequestActionsSDKMock: MatrixRustSDK.KnockRequestActions {
|
||||||
|
init() {
|
||||||
|
super.init(noPointer: .init())
|
||||||
|
}
|
||||||
|
|
||||||
|
public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
|
||||||
|
fatalError("init(unsafeFromRawPointer:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate var pointer: UnsafeMutableRawPointer!
|
||||||
|
|
||||||
|
//MARK: - accept
|
||||||
|
|
||||||
|
open var acceptThrowableError: Error?
|
||||||
|
var acceptUnderlyingCallsCount = 0
|
||||||
|
open var acceptCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return acceptUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = acceptUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
acceptUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
acceptUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var acceptCalled: Bool {
|
||||||
|
return acceptCallsCount > 0
|
||||||
|
}
|
||||||
|
open var acceptClosure: (() async throws -> Void)?
|
||||||
|
|
||||||
|
open override func accept() async throws {
|
||||||
|
if let error = acceptThrowableError {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
acceptCallsCount += 1
|
||||||
|
try await acceptClosure?()
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - decline
|
||||||
|
|
||||||
|
open var declineReasonThrowableError: Error?
|
||||||
|
var declineReasonUnderlyingCallsCount = 0
|
||||||
|
open var declineReasonCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return declineReasonUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = declineReasonUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
declineReasonUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
declineReasonUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var declineReasonCalled: Bool {
|
||||||
|
return declineReasonCallsCount > 0
|
||||||
|
}
|
||||||
|
open var declineReasonReceivedReason: String?
|
||||||
|
open var declineReasonReceivedInvocations: [String?] = []
|
||||||
|
open var declineReasonClosure: ((String?) async throws -> Void)?
|
||||||
|
|
||||||
|
open override func decline(reason: String?) async throws {
|
||||||
|
if let error = declineReasonThrowableError {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
declineReasonCallsCount += 1
|
||||||
|
declineReasonReceivedReason = reason
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.declineReasonReceivedInvocations.append(reason)
|
||||||
|
}
|
||||||
|
try await declineReasonClosure?(reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - declineAndBan
|
||||||
|
|
||||||
|
open var declineAndBanReasonThrowableError: Error?
|
||||||
|
var declineAndBanReasonUnderlyingCallsCount = 0
|
||||||
|
open var declineAndBanReasonCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return declineAndBanReasonUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = declineAndBanReasonUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
declineAndBanReasonUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
declineAndBanReasonUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var declineAndBanReasonCalled: Bool {
|
||||||
|
return declineAndBanReasonCallsCount > 0
|
||||||
|
}
|
||||||
|
open var declineAndBanReasonReceivedReason: String?
|
||||||
|
open var declineAndBanReasonReceivedInvocations: [String?] = []
|
||||||
|
open var declineAndBanReasonClosure: ((String?) async throws -> Void)?
|
||||||
|
|
||||||
|
open override func declineAndBan(reason: String?) async throws {
|
||||||
|
if let error = declineAndBanReasonThrowableError {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
declineAndBanReasonCallsCount += 1
|
||||||
|
declineAndBanReasonReceivedReason = reason
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.declineAndBanReasonReceivedInvocations.append(reason)
|
||||||
|
}
|
||||||
|
try await declineAndBanReasonClosure?(reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
//MARK: - markAsSeen
|
||||||
|
|
||||||
|
open var markAsSeenThrowableError: Error?
|
||||||
|
var markAsSeenUnderlyingCallsCount = 0
|
||||||
|
open var markAsSeenCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return markAsSeenUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = markAsSeenUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
markAsSeenUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
markAsSeenUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var markAsSeenCalled: Bool {
|
||||||
|
return markAsSeenCallsCount > 0
|
||||||
|
}
|
||||||
|
open var markAsSeenClosure: (() async throws -> Void)?
|
||||||
|
|
||||||
|
open override func markAsSeen() async throws {
|
||||||
|
if let error = markAsSeenThrowableError {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
markAsSeenCallsCount += 1
|
||||||
|
try await markAsSeenClosure?()
|
||||||
|
}
|
||||||
|
}
|
||||||
open class LazyTimelineItemProviderSDKMock: MatrixRustSDK.LazyTimelineItemProvider {
|
open class LazyTimelineItemProviderSDKMock: MatrixRustSDK.LazyTimelineItemProvider {
|
||||||
init() {
|
init() {
|
||||||
super.init(noPointer: .init())
|
super.init(noPointer: .init())
|
||||||
@ -14018,6 +14201,81 @@ open class RoomSDKMock: MatrixRustSDK.Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//MARK: - subscribeToKnockRequests
|
||||||
|
|
||||||
|
open var subscribeToKnockRequestsListenerThrowableError: Error?
|
||||||
|
var subscribeToKnockRequestsListenerUnderlyingCallsCount = 0
|
||||||
|
open var subscribeToKnockRequestsListenerCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return subscribeToKnockRequestsListenerUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = subscribeToKnockRequestsListenerUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
subscribeToKnockRequestsListenerUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
subscribeToKnockRequestsListenerUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var subscribeToKnockRequestsListenerCalled: Bool {
|
||||||
|
return subscribeToKnockRequestsListenerCallsCount > 0
|
||||||
|
}
|
||||||
|
open var subscribeToKnockRequestsListenerReceivedListener: KnockRequestsListener?
|
||||||
|
open var subscribeToKnockRequestsListenerReceivedInvocations: [KnockRequestsListener] = []
|
||||||
|
|
||||||
|
var subscribeToKnockRequestsListenerUnderlyingReturnValue: TaskHandle!
|
||||||
|
open var subscribeToKnockRequestsListenerReturnValue: TaskHandle! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return subscribeToKnockRequestsListenerUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: TaskHandle? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = subscribeToKnockRequestsListenerUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
subscribeToKnockRequestsListenerUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
subscribeToKnockRequestsListenerUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var subscribeToKnockRequestsListenerClosure: ((KnockRequestsListener) async throws -> TaskHandle)?
|
||||||
|
|
||||||
|
open override func subscribeToKnockRequests(listener: KnockRequestsListener) async throws -> TaskHandle {
|
||||||
|
if let error = subscribeToKnockRequestsListenerThrowableError {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
subscribeToKnockRequestsListenerCallsCount += 1
|
||||||
|
subscribeToKnockRequestsListenerReceivedListener = listener
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.subscribeToKnockRequestsListenerReceivedInvocations.append(listener)
|
||||||
|
}
|
||||||
|
if let subscribeToKnockRequestsListenerClosure = subscribeToKnockRequestsListenerClosure {
|
||||||
|
return try await subscribeToKnockRequestsListenerClosure(listener)
|
||||||
|
} else {
|
||||||
|
return subscribeToKnockRequestsListenerReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//MARK: - subscribeToRoomInfoUpdates
|
//MARK: - subscribeToRoomInfoUpdates
|
||||||
|
|
||||||
var subscribeToRoomInfoUpdatesListenerUnderlyingCallsCount = 0
|
var subscribeToRoomInfoUpdatesListenerUnderlyingCallsCount = 0
|
||||||
|
@ -30,6 +30,7 @@ struct JoinedRoomProxyMockConfiguration {
|
|||||||
var timelineStartReached = false
|
var timelineStartReached = false
|
||||||
|
|
||||||
var members: [RoomMemberProxyMock] = .allMembers
|
var members: [RoomMemberProxyMock] = .allMembers
|
||||||
|
var knockRequestsState: KnockRequestsState = .loaded([])
|
||||||
var ownUserID = RoomMemberProxyMock.mockMe.userID
|
var ownUserID = RoomMemberProxyMock.mockMe.userID
|
||||||
var inviter: RoomMemberProxyProtocol?
|
var inviter: RoomMemberProxyProtocol?
|
||||||
|
|
||||||
@ -57,6 +58,7 @@ extension JoinedRoomProxyMock {
|
|||||||
|
|
||||||
infoPublisher = CurrentValueSubject(.init(roomInfo: .init(configuration))).asCurrentValuePublisher()
|
infoPublisher = CurrentValueSubject(.init(roomInfo: .init(configuration))).asCurrentValuePublisher()
|
||||||
membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher()
|
membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher()
|
||||||
|
knockRequestsStatePublisher = CurrentValueSubject(configuration.knockRequestsState).asCurrentValuePublisher()
|
||||||
typingMembersPublisher = CurrentValueSubject([]).asCurrentValuePublisher()
|
typingMembersPublisher = CurrentValueSubject([]).asCurrentValuePublisher()
|
||||||
identityStatusChangesPublisher = CurrentValueSubject([]).asCurrentValuePublisher()
|
identityStatusChangesPublisher = CurrentValueSubject([]).asCurrentValuePublisher()
|
||||||
|
|
||||||
|
35
ElementX/Sources/Mocks/KnockRequestProxyMock.swift
Normal file
35
ElementX/Sources/Mocks/KnockRequestProxyMock.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2024 New Vector Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
// Please see LICENSE in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct KnockRequestProxyMockConfiguration {
|
||||||
|
let eventID: String
|
||||||
|
let userID: String
|
||||||
|
var displayName: String?
|
||||||
|
var avatarURL: URL?
|
||||||
|
var timestamp: String?
|
||||||
|
var reason: String?
|
||||||
|
var isSeen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
extension KnockRequestProxyMock {
|
||||||
|
convenience init(_ configuration: KnockRequestProxyMockConfiguration) {
|
||||||
|
self.init()
|
||||||
|
eventID = configuration.eventID
|
||||||
|
userID = configuration.userID
|
||||||
|
displayName = configuration.displayName
|
||||||
|
avatarURL = configuration.avatarURL
|
||||||
|
reason = configuration.reason
|
||||||
|
formattedTimestamp = configuration.timestamp
|
||||||
|
isSeen = configuration.isSeen
|
||||||
|
acceptReturnValue = .success(())
|
||||||
|
declineReturnValue = .success(())
|
||||||
|
banReturnValue = .success(())
|
||||||
|
markAsSeenReturnValue = .success(())
|
||||||
|
}
|
||||||
|
}
|
@ -71,7 +71,7 @@ extension Array where Element == RoomSummary {
|
|||||||
static let mockRooms: [Element] = [
|
static let mockRooms: [Element] = [
|
||||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
id: "1",
|
id: "1",
|
||||||
joinRequestType: nil,
|
knockRequestType: nil,
|
||||||
name: "Foundation 🔭🪐🌌",
|
name: "Foundation 🔭🪐🌌",
|
||||||
isDirect: false,
|
isDirect: false,
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
@ -88,7 +88,7 @@ extension Array where Element == RoomSummary {
|
|||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
id: "2",
|
id: "2",
|
||||||
joinRequestType: nil,
|
knockRequestType: nil,
|
||||||
name: "Foundation and Empire",
|
name: "Foundation and Empire",
|
||||||
isDirect: false,
|
isDirect: false,
|
||||||
avatarURL: .mockMXCAvatar,
|
avatarURL: .mockMXCAvatar,
|
||||||
@ -105,7 +105,7 @@ extension Array where Element == RoomSummary {
|
|||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
id: "3",
|
id: "3",
|
||||||
joinRequestType: nil,
|
knockRequestType: nil,
|
||||||
name: "Second Foundation",
|
name: "Second Foundation",
|
||||||
isDirect: false,
|
isDirect: false,
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
@ -122,7 +122,7 @@ extension Array where Element == RoomSummary {
|
|||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
id: "4",
|
id: "4",
|
||||||
joinRequestType: nil,
|
knockRequestType: nil,
|
||||||
name: "Foundation's Edge",
|
name: "Foundation's Edge",
|
||||||
isDirect: false,
|
isDirect: false,
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
@ -139,7 +139,7 @@ extension Array where Element == RoomSummary {
|
|||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
id: "5",
|
id: "5",
|
||||||
joinRequestType: nil,
|
knockRequestType: nil,
|
||||||
name: "Foundation and Earth",
|
name: "Foundation and Earth",
|
||||||
isDirect: true,
|
isDirect: true,
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
@ -156,7 +156,7 @@ extension Array where Element == RoomSummary {
|
|||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
id: "6",
|
id: "6",
|
||||||
joinRequestType: nil,
|
knockRequestType: nil,
|
||||||
name: "Prelude to Foundation",
|
name: "Prelude to Foundation",
|
||||||
isDirect: true,
|
isDirect: true,
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
@ -173,7 +173,7 @@ extension Array where Element == RoomSummary {
|
|||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
id: "0",
|
id: "0",
|
||||||
joinRequestType: nil,
|
knockRequestType: nil,
|
||||||
name: "Unknown",
|
name: "Unknown",
|
||||||
isDirect: false,
|
isDirect: false,
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
@ -223,7 +223,7 @@ extension Array where Element == RoomSummary {
|
|||||||
static let mockInvites: [Element] = [
|
static let mockInvites: [Element] = [
|
||||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
id: "someAwesomeRoomId1",
|
id: "someAwesomeRoomId1",
|
||||||
joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
knockRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
||||||
name: "First room",
|
name: "First room",
|
||||||
isDirect: false,
|
isDirect: false,
|
||||||
avatarURL: .mockMXCAvatar,
|
avatarURL: .mockMXCAvatar,
|
||||||
@ -240,7 +240,7 @@ extension Array where Element == RoomSummary {
|
|||||||
isFavourite: false),
|
isFavourite: false),
|
||||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
id: "someAwesomeRoomId2",
|
id: "someAwesomeRoomId2",
|
||||||
joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
knockRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
||||||
name: "Second room",
|
name: "Second room",
|
||||||
isDirect: true,
|
isDirect: true,
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
|
17
ElementX/Sources/Other/Extensions/Result.swift
Normal file
17
ElementX/Sources/Other/Extensions/Result.swift
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2024 New Vector Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
// Please see LICENSE in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
extension Result {
|
||||||
|
var isFailure: Bool {
|
||||||
|
switch self {
|
||||||
|
case .success:
|
||||||
|
return false
|
||||||
|
case .failure:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -195,7 +195,6 @@ struct CreateRoomScreen: View {
|
|||||||
Text("#")
|
Text("#")
|
||||||
.font(.compound.bodyLG)
|
.font(.compound.bodyLG)
|
||||||
.foregroundStyle(.compound.textSecondary)
|
.foregroundStyle(.compound.textSecondary)
|
||||||
|
|
||||||
TextField("", text: aliasBinding)
|
TextField("", text: aliasBinding)
|
||||||
.textInputAutocapitalization(.never)
|
.textInputAutocapitalization(.never)
|
||||||
.autocorrectionDisabled()
|
.autocorrectionDisabled()
|
||||||
|
@ -219,13 +219,13 @@ extension HomeScreenRoom {
|
|||||||
|
|
||||||
let hasUnreadMessages = hideUnreadMessagesBadge ? false : summary.hasUnreadMessages
|
let hasUnreadMessages = hideUnreadMessagesBadge ? false : summary.hasUnreadMessages
|
||||||
|
|
||||||
let isDotShown = hasUnreadMessages || summary.hasUnreadMentions || summary.hasUnreadNotifications || summary.isMarkedUnread || summary.joinRequestType?.isKnock == true
|
let isDotShown = hasUnreadMessages || summary.hasUnreadMentions || summary.hasUnreadNotifications || summary.isMarkedUnread || summary.knockRequestType?.isKnock == true
|
||||||
let isMentionShown = summary.hasUnreadMentions && !summary.isMuted
|
let isMentionShown = summary.hasUnreadMentions && !summary.isMuted
|
||||||
let isMuteShown = summary.isMuted
|
let isMuteShown = summary.isMuted
|
||||||
let isCallShown = summary.hasOngoingCall
|
let isCallShown = summary.hasOngoingCall
|
||||||
let isHighlighted = summary.isMarkedUnread || (!summary.isMuted && (summary.hasUnreadNotifications || summary.hasUnreadMentions)) || summary.joinRequestType?.isKnock == true
|
let isHighlighted = summary.isMarkedUnread || (!summary.isMuted && (summary.hasUnreadNotifications || summary.hasUnreadMentions)) || summary.knockRequestType?.isKnock == true
|
||||||
|
|
||||||
let type: HomeScreenRoom.RoomType = switch summary.joinRequestType {
|
let type: HomeScreenRoom.RoomType = switch summary.knockRequestType {
|
||||||
case .invite(let inviter): .invite(inviterDetails: inviter.map(RoomInviterDetails.init))
|
case .invite(let inviter): .invite(inviterDetails: inviter.map(RoomInviterDetails.init))
|
||||||
case .knock: .knock
|
case .knock: .knock
|
||||||
case .none: .room
|
case .none: .room
|
||||||
|
@ -178,7 +178,7 @@ private extension HomeScreenRoom {
|
|||||||
|
|
||||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
id: "@someone:somewhere.com",
|
id: "@someone:somewhere.com",
|
||||||
joinRequestType: .invite(inviter: inviter),
|
knockRequestType: .invite(inviter: inviter),
|
||||||
name: "Some Guy",
|
name: "Some Guy",
|
||||||
isDirect: true,
|
isDirect: true,
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
@ -205,7 +205,7 @@ private extension HomeScreenRoom {
|
|||||||
|
|
||||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
id: "@someone:somewhere.com",
|
id: "@someone:somewhere.com",
|
||||||
joinRequestType: .invite(inviter: inviter),
|
knockRequestType: .invite(inviter: inviter),
|
||||||
name: "Awesome Room",
|
name: "Awesome Room",
|
||||||
isDirect: false,
|
isDirect: false,
|
||||||
avatarURL: avatarURL,
|
avatarURL: avatarURL,
|
||||||
|
@ -152,7 +152,7 @@ private extension HomeScreenRoom {
|
|||||||
|
|
||||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
id: "@someone:somewhere.com",
|
id: "@someone:somewhere.com",
|
||||||
joinRequestType: .invite(inviter: inviter),
|
knockRequestType: .invite(inviter: inviter),
|
||||||
name: "Some Guy",
|
name: "Some Guy",
|
||||||
isDirect: true,
|
isDirect: true,
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
@ -179,7 +179,7 @@ private extension HomeScreenRoom {
|
|||||||
|
|
||||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||||
id: "@someone:somewhere.com",
|
id: "@someone:somewhere.com",
|
||||||
joinRequestType: .invite(inviter: inviter),
|
knockRequestType: .invite(inviter: inviter),
|
||||||
name: "Awesome Room",
|
name: "Awesome Room",
|
||||||
isDirect: false,
|
isDirect: false,
|
||||||
avatarURL: avatarURL,
|
avatarURL: avatarURL,
|
||||||
|
@ -13,6 +13,7 @@ import SwiftUI
|
|||||||
struct KnockRequestsListScreenCoordinatorParameters {
|
struct KnockRequestsListScreenCoordinatorParameters {
|
||||||
let roomProxy: JoinedRoomProxyProtocol
|
let roomProxy: JoinedRoomProxyProtocol
|
||||||
let mediaProvider: MediaProviderProtocol
|
let mediaProvider: MediaProviderProtocol
|
||||||
|
let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
enum KnockRequestsListScreenCoordinatorAction { }
|
enum KnockRequestsListScreenCoordinatorAction { }
|
||||||
@ -29,7 +30,8 @@ final class KnockRequestsListScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
init(parameters: KnockRequestsListScreenCoordinatorParameters) {
|
init(parameters: KnockRequestsListScreenCoordinatorParameters) {
|
||||||
viewModel = KnockRequestsListScreenViewModel(roomProxy: parameters.roomProxy,
|
viewModel = KnockRequestsListScreenViewModel(roomProxy: parameters.roomProxy,
|
||||||
mediaProvider: parameters.mediaProvider)
|
mediaProvider: parameters.mediaProvider,
|
||||||
|
userIndicatorController: parameters.userIndicatorController)
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() { }
|
func start() { }
|
||||||
|
@ -10,18 +10,43 @@ import Foundation
|
|||||||
enum KnockRequestsListScreenViewModelAction { }
|
enum KnockRequestsListScreenViewModelAction { }
|
||||||
|
|
||||||
struct KnockRequestsListScreenViewState: BindableState {
|
struct KnockRequestsListScreenViewState: BindableState {
|
||||||
// TODO: Not sure yet how we will fetch this, this is just for testing purposes
|
var requestsState: KnockRequestsListState = .loading
|
||||||
var requests: [KnockRequestCellInfo] = [.init(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "Now", reason: "Hello")]
|
|
||||||
|
var displayedRequests: [KnockRequestCellInfo] {
|
||||||
|
guard case let .loaded(requests) = requestsState else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return requests.filter { !handledEventIDs.contains($0.id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var isLoading: Bool {
|
||||||
|
switch requestsState {
|
||||||
|
case .loading:
|
||||||
|
true
|
||||||
|
default:
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If you are in this view one of these must have been true so by default we assume all of them to be true
|
// If you are in this view one of these must have been true so by default we assume all of them to be true
|
||||||
var canAccept = true
|
var canAccept = true
|
||||||
var canDecline = true
|
var canDecline = true
|
||||||
var canBan = true
|
var canBan = true
|
||||||
var isKnockableRoom = true
|
var isKnockableRoom = true
|
||||||
|
var handledEventIDs: Set<String> = []
|
||||||
|
|
||||||
// If all the permissions are denied or the join rule changes while we are in the view
|
// If all the permissions are denied or the join rule changes while we are in the view
|
||||||
// we want to stop displaying any request
|
// we want to stop displaying any request
|
||||||
var shouldDisplayRequests: Bool {
|
var shouldDisplayRequests: Bool {
|
||||||
!requests.isEmpty && isKnockableRoom && (canAccept || canDecline || canBan)
|
!displayedRequests.isEmpty && isKnockableRoom && (canAccept || canDecline || canBan)
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldDisplayAcceptAllButton: Bool {
|
||||||
|
!isLoading && shouldDisplayRequests && displayedRequests.count > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldDisplayEmptyView: Bool {
|
||||||
|
!isLoading && !shouldDisplayRequests
|
||||||
}
|
}
|
||||||
|
|
||||||
var bindings = KnockRequestsListStateBindings()
|
var bindings = KnockRequestsListStateBindings()
|
||||||
@ -35,11 +60,39 @@ enum KnockRequestsListAlertType {
|
|||||||
case acceptAllRequests
|
case acceptAllRequests
|
||||||
case declineRequest
|
case declineRequest
|
||||||
case declineAndBan
|
case declineAndBan
|
||||||
|
case acceptAllFailed
|
||||||
|
case acceptFailed
|
||||||
|
case declineFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
enum KnockRequestsListScreenViewAction {
|
enum KnockRequestsListScreenViewAction {
|
||||||
case acceptAllRequests
|
case acceptAllRequests
|
||||||
case acceptRequest(userID: String)
|
case acceptRequest(eventID: String)
|
||||||
case declineRequest(userID: String)
|
case declineRequest(eventID: String)
|
||||||
case ban(userID: String)
|
case ban(eventID: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum KnockRequestsListState: Equatable {
|
||||||
|
case loading
|
||||||
|
case loaded([KnockRequestCellInfo])
|
||||||
|
|
||||||
|
init(from state: KnockRequestsState) {
|
||||||
|
switch state {
|
||||||
|
case .loading:
|
||||||
|
self = .loading
|
||||||
|
case .loaded(let requests):
|
||||||
|
self = .loaded(requests.map(KnockRequestCellInfo.init))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension KnockRequestCellInfo {
|
||||||
|
init(from proxy: KnockRequestProxyProtocol) {
|
||||||
|
self.init(eventID: proxy.eventID,
|
||||||
|
userID: proxy.userID,
|
||||||
|
displayName: proxy.displayName,
|
||||||
|
avatarURL: proxy.avatarURL,
|
||||||
|
timestamp: proxy.formattedTimestamp,
|
||||||
|
reason: proxy.reason)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,14 +12,18 @@ typealias KnockRequestsListScreenViewModelType = StateStoreViewModel<KnockReques
|
|||||||
|
|
||||||
class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, KnockRequestsListScreenViewModelProtocol {
|
class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, KnockRequestsListScreenViewModelProtocol {
|
||||||
private let roomProxy: JoinedRoomProxyProtocol
|
private let roomProxy: JoinedRoomProxyProtocol
|
||||||
|
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||||
|
|
||||||
private let actionsSubject: PassthroughSubject<KnockRequestsListScreenViewModelAction, Never> = .init()
|
private let actionsSubject: PassthroughSubject<KnockRequestsListScreenViewModelAction, Never> = .init()
|
||||||
var actionsPublisher: AnyPublisher<KnockRequestsListScreenViewModelAction, Never> {
|
var actionsPublisher: AnyPublisher<KnockRequestsListScreenViewModelAction, Never> {
|
||||||
actionsSubject.eraseToAnyPublisher()
|
actionsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(roomProxy: JoinedRoomProxyProtocol, mediaProvider: MediaProviderProtocol) {
|
init(roomProxy: JoinedRoomProxyProtocol,
|
||||||
|
mediaProvider: MediaProviderProtocol,
|
||||||
|
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||||
self.roomProxy = roomProxy
|
self.roomProxy = roomProxy
|
||||||
|
self.userIndicatorController = userIndicatorController
|
||||||
super.init(initialViewState: KnockRequestsListScreenViewState(), mediaProvider: mediaProvider)
|
super.init(initialViewState: KnockRequestsListScreenViewState(), mediaProvider: mediaProvider)
|
||||||
|
|
||||||
updateRoomInfo(roomInfo: roomProxy.infoPublisher.value)
|
updateRoomInfo(roomInfo: roomProxy.infoPublisher.value)
|
||||||
@ -39,35 +43,147 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn
|
|||||||
title: L10n.screenKnockRequestsListAcceptAllAlertTitle,
|
title: L10n.screenKnockRequestsListAcceptAllAlertTitle,
|
||||||
message: L10n.screenKnockRequestsListAcceptAllAlertDescription,
|
message: L10n.screenKnockRequestsListAcceptAllAlertDescription,
|
||||||
primaryButton: .init(title: L10n.screenKnockRequestsListAcceptAllAlertConfirmButtonTitle,
|
primaryButton: .init(title: L10n.screenKnockRequestsListAcceptAllAlertConfirmButtonTitle,
|
||||||
// TODO: Implement action
|
action: acceptAll),
|
||||||
action: nil),
|
|
||||||
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||||
case .acceptRequest(let userID):
|
case .acceptRequest(let eventID):
|
||||||
// TODO: Implement
|
guard let request = getRequest(eventID: eventID) else {
|
||||||
break
|
return
|
||||||
case .declineRequest(let userID):
|
}
|
||||||
|
accept(request: request)
|
||||||
|
case .declineRequest(let eventID):
|
||||||
|
guard let request = getRequest(eventID: eventID) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
state.bindings.alertInfo = .init(id: .declineRequest,
|
state.bindings.alertInfo = .init(id: .declineRequest,
|
||||||
title: L10n.screenKnockRequestsListDeclineAlertTitle,
|
title: L10n.screenKnockRequestsListDeclineAlertTitle,
|
||||||
message: L10n.screenKnockRequestsListDeclineAlertDescription(userID),
|
message: L10n.screenKnockRequestsListDeclineAlertDescription(request.userID),
|
||||||
primaryButton: .init(title: L10n.screenKnockRequestsListDeclineAlertConfirmButtonTitle,
|
primaryButton: .init(title: L10n.screenKnockRequestsListDeclineAlertConfirmButtonTitle,
|
||||||
role: .destructive,
|
role: .destructive) { [weak self] in self?.decline(request: request) },
|
||||||
// TODO: Implement action
|
|
||||||
action: nil),
|
|
||||||
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||||
case .ban(let userID):
|
case .ban(let eventID):
|
||||||
|
guard let request = getRequest(eventID: eventID) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
state.bindings.alertInfo = .init(id: .declineAndBan,
|
state.bindings.alertInfo = .init(id: .declineAndBan,
|
||||||
title: L10n.screenKnockRequestsListBanAlertTitle,
|
title: L10n.screenKnockRequestsListBanAlertTitle,
|
||||||
message: L10n.screenKnockRequestsListBanAlertDescription(userID),
|
|
||||||
// TODO: Implement action
|
|
||||||
primaryButton: .init(title: L10n.screenKnockRequestsListBanAlertConfirmButtonTitle,
|
primaryButton: .init(title: L10n.screenKnockRequestsListBanAlertConfirmButtonTitle,
|
||||||
role: .destructive,
|
role: .destructive) { [weak self] in self?.declineAndBan(request: request) },
|
||||||
action: nil),
|
|
||||||
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func getRequest(eventID: String) -> KnockRequestProxyProtocol? {
|
||||||
|
guard case let .loaded(requests) = roomProxy.knockRequestsStatePublisher.value,
|
||||||
|
let request = requests.first(where: { $0.eventID == eventID }) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
private func accept(request: KnockRequestProxyProtocol) {
|
||||||
|
showLoadingIndicator(title: L10n.screenKnockRequestsListAcceptLoadingTitle)
|
||||||
|
|
||||||
|
let eventID = request.eventID
|
||||||
|
state.handledEventIDs.insert(eventID)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
switch await request.accept() {
|
||||||
|
case .success:
|
||||||
|
hideLoadingIndicator()
|
||||||
|
case .failure:
|
||||||
|
hideLoadingIndicator()
|
||||||
|
state.handledEventIDs.remove(eventID)
|
||||||
|
state.bindings.alertInfo = .init(id: .acceptFailed,
|
||||||
|
title: L10n.screenKnockRequestsListAcceptFailedAlertTitle,
|
||||||
|
message: L10n.screenKnockRequestsListAcceptFailedAlertDescription,
|
||||||
|
primaryButton: .init(title: L10n.actionYesTryAgain) { [weak self] in self?.accept(request: request) },
|
||||||
|
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decline(request: KnockRequestProxyProtocol) {
|
||||||
|
showLoadingIndicator(title: L10n.screenKnockRequestsListDeclineLoadingTitle)
|
||||||
|
|
||||||
|
let eventID = request.eventID
|
||||||
|
state.handledEventIDs.insert(eventID)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
switch await request.decline() {
|
||||||
|
case .success:
|
||||||
|
hideLoadingIndicator()
|
||||||
|
case .failure:
|
||||||
|
hideLoadingIndicator()
|
||||||
|
state.handledEventIDs.remove(eventID)
|
||||||
|
state.bindings.alertInfo = .init(id: .declineFailed,
|
||||||
|
title: L10n.screenKnockRequestsListDeclineFailedAlertTitle,
|
||||||
|
message: L10n.screenKnockRequestsListDeclineFailedAlertDescription,
|
||||||
|
primaryButton: .init(title: L10n.actionYesTryAgain) { [weak self] in self?.decline(request: request) },
|
||||||
|
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func declineAndBan(request: KnockRequestProxyProtocol) {
|
||||||
|
showLoadingIndicator(title: L10n.screenKnockRequestsListBanLoadingTitle)
|
||||||
|
|
||||||
|
let eventID = request.eventID
|
||||||
|
state.handledEventIDs.insert(eventID)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
switch await request.ban() {
|
||||||
|
case .success:
|
||||||
|
hideLoadingIndicator()
|
||||||
|
case .failure:
|
||||||
|
hideLoadingIndicator()
|
||||||
|
state.handledEventIDs.remove(eventID)
|
||||||
|
state.bindings.alertInfo = .init(id: .declineFailed,
|
||||||
|
title: L10n.screenKnockRequestsListDeclineFailedAlertTitle,
|
||||||
|
message: L10n.screenKnockRequestsListDeclineFailedAlertDescription,
|
||||||
|
primaryButton: .init(title: L10n.actionYesTryAgain) { [weak self] in self?.declineAndBan(request: request) },
|
||||||
|
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func acceptAll() {
|
||||||
|
guard case let .loaded(requests) = roomProxy.knockRequestsStatePublisher.value else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showLoadingIndicator(title: L10n.screenKnockRequestsListAcceptAllLoadingTitle)
|
||||||
|
state.handledEventIDs.formUnion(Set(requests.map(\.eventID)))
|
||||||
|
|
||||||
|
Task {
|
||||||
|
let failedIDs = await withTaskGroup(of: (String, Result<Void, KnockRequestProxyError>).self) { group in
|
||||||
|
for request in requests {
|
||||||
|
group.addTask {
|
||||||
|
await (request.eventID, request.accept())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var failedIDs = [String]()
|
||||||
|
for await result in group where result.1.isFailure {
|
||||||
|
failedIDs.append(result.0)
|
||||||
|
}
|
||||||
|
return failedIDs
|
||||||
|
}
|
||||||
|
hideLoadingIndicator()
|
||||||
|
|
||||||
|
if !failedIDs.isEmpty {
|
||||||
|
state.handledEventIDs.subtract(failedIDs)
|
||||||
|
state.bindings.alertInfo = .init(id: .acceptAllFailed,
|
||||||
|
title: L10n.screenKnockRequestsListAcceptAllFailedAlertTitle,
|
||||||
|
message: L10n.screenKnockRequestsListAcceptAllFailedAlertDescription,
|
||||||
|
primaryButton: .init(title: L10n.actionYesTryAgain) { [weak self] in self?.acceptAll() },
|
||||||
|
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func setupSubscriptions() {
|
private func setupSubscriptions() {
|
||||||
roomProxy.infoPublisher
|
roomProxy.infoPublisher
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
@ -76,6 +192,26 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn
|
|||||||
Task { await self?.updatePermissions() }
|
Task { await self?.updatePermissions() }
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
roomProxy.knockRequestsStatePublisher
|
||||||
|
.map(KnockRequestsListState.init)
|
||||||
|
.removeDuplicates()
|
||||||
|
.throttle(for: .milliseconds(100), scheduler: DispatchQueue.main, latest: true)
|
||||||
|
.weakAssign(to: \.state.requestsState, on: self)
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
context.$viewState
|
||||||
|
.map(\.isLoading)
|
||||||
|
.removeDuplicates()
|
||||||
|
.sink { [weak self] isLoading in
|
||||||
|
guard let self else { return }
|
||||||
|
if isLoading {
|
||||||
|
showInitialLoadingIndicator()
|
||||||
|
} else {
|
||||||
|
hideLoadingIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateRoomInfo(roomInfo: RoomInfoProxy) {
|
private func updateRoomInfo(roomInfo: RoomInfoProxy) {
|
||||||
@ -93,15 +229,29 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn
|
|||||||
state.canBan = await (try? roomProxy.canUserBan(userID: roomProxy.ownUserID).get()) == true
|
state.canBan = await (try? roomProxy.canUserBan(userID: roomProxy.ownUserID).get()) == true
|
||||||
}
|
}
|
||||||
|
|
||||||
// For testing purposes
|
private static let loadingIndicatorIdentifier = "\(KnockRequestsListScreenViewModel.self)-Loading"
|
||||||
private init(initialViewState: KnockRequestsListScreenViewState) {
|
|
||||||
roomProxy = JoinedRoomProxyMock(.init())
|
|
||||||
super.init(initialViewState: initialViewState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension KnockRequestsListScreenViewModel {
|
private func showInitialLoadingIndicator() {
|
||||||
static func mockWithInitialState(_ initialViewState: KnockRequestsListScreenViewState) -> KnockRequestsListScreenViewModel {
|
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||||
.init(initialViewState: initialViewState)
|
type: .modal(progress: .indeterminate,
|
||||||
|
interactiveDismissDisabled: false,
|
||||||
|
allowsInteraction: true),
|
||||||
|
title: L10n.screenKnockRequestsListInitialLoadingTitle,
|
||||||
|
persistent: true),
|
||||||
|
delay: .milliseconds(100))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showLoadingIndicator(title: String) {
|
||||||
|
userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||||
|
type: .modal(progress: .indeterminate,
|
||||||
|
interactiveDismissDisabled: false,
|
||||||
|
allowsInteraction: false),
|
||||||
|
title: title,
|
||||||
|
persistent: true),
|
||||||
|
delay: .milliseconds(200))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideLoadingIndicator() {
|
||||||
|
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
import Compound
|
import Compound
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct KnockRequestCellInfo: Identifiable {
|
struct KnockRequestCellInfo: Equatable {
|
||||||
/// user identifier of the usee that sent the request
|
let eventID: String
|
||||||
let id: String
|
let userID: String
|
||||||
let displayName: String?
|
let displayName: String?
|
||||||
let avatarURL: URL?
|
let avatarURL: URL?
|
||||||
let timestamp: String?
|
let timestamp: String?
|
||||||
@ -35,7 +35,7 @@ struct KnockRequestCell: View {
|
|||||||
HStack(alignment: .top, spacing: 16) {
|
HStack(alignment: .top, spacing: 16) {
|
||||||
LoadableAvatarImage(url: cellInfo.avatarURL,
|
LoadableAvatarImage(url: cellInfo.avatarURL,
|
||||||
name: cellInfo.displayName,
|
name: cellInfo.displayName,
|
||||||
contentID: cellInfo.id,
|
contentID: cellInfo.userID,
|
||||||
avatarSize: .user(on: .knockingUserList),
|
avatarSize: .user(on: .knockingUserList),
|
||||||
mediaProvider: mediaProvider)
|
mediaProvider: mediaProvider)
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
@ -60,7 +60,7 @@ struct KnockRequestCell: View {
|
|||||||
private var header: some View {
|
private var header: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
HStack(alignment: .top, spacing: 0) {
|
HStack(alignment: .top, spacing: 0) {
|
||||||
Text(cellInfo.displayName ?? cellInfo.id)
|
Text(cellInfo.displayName ?? cellInfo.userID)
|
||||||
.font(.compound.bodyLGSemibold)
|
.font(.compound.bodyLGSemibold)
|
||||||
.foregroundStyle(.compound.textPrimary)
|
.foregroundStyle(.compound.textPrimary)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
@ -71,7 +71,7 @@ struct KnockRequestCell: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cellInfo.displayName != nil {
|
if cellInfo.displayName != nil {
|
||||||
Text(cellInfo.id)
|
Text(cellInfo.userID)
|
||||||
.font(.compound.bodyMD)
|
.font(.compound.bodyMD)
|
||||||
.foregroundStyle(.compound.textSecondary)
|
.foregroundStyle(.compound.textSecondary)
|
||||||
}
|
}
|
||||||
@ -85,14 +85,14 @@ struct KnockRequestCell: View {
|
|||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
if let onDecline {
|
if let onDecline {
|
||||||
Button(L10n.actionDecline) {
|
Button(L10n.actionDecline) {
|
||||||
onDecline(cellInfo.id)
|
onDecline(cellInfo.eventID)
|
||||||
}
|
}
|
||||||
.buttonStyle(.compound(.secondary, size: .medium))
|
.buttonStyle(.compound(.secondary, size: .medium))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let onAccept {
|
if let onAccept {
|
||||||
Button(L10n.actionAccept) {
|
Button(L10n.actionAccept) {
|
||||||
onAccept(cellInfo.id)
|
onAccept(cellInfo.eventID)
|
||||||
}
|
}
|
||||||
.buttonStyle(.compound(.primary, size: .medium))
|
.buttonStyle(.compound(.primary, size: .medium))
|
||||||
}
|
}
|
||||||
@ -101,7 +101,7 @@ struct KnockRequestCell: View {
|
|||||||
|
|
||||||
if let onDeclineAndBan {
|
if let onDeclineAndBan {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
onDeclineAndBan(cellInfo.id)
|
onDeclineAndBan(cellInfo.eventID)
|
||||||
} label: {
|
} label: {
|
||||||
Text(L10n.screenKnockRequestsListDeclineAndBanActionTitle)
|
Text(L10n.screenKnockRequestsListDeclineAndBanActionTitle)
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
@ -166,15 +166,19 @@ private struct DisclosableText: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension KnockRequestCellInfo: Identifiable {
|
||||||
|
var id: String { eventID }
|
||||||
|
}
|
||||||
|
|
||||||
struct KnockRequestCell_Previews: PreviewProvider, TestablePreview {
|
struct KnockRequestCell_Previews: PreviewProvider, TestablePreview {
|
||||||
// swiftlint:disable:next line_length
|
// swiftlint:disable:next line_length
|
||||||
static let aliceWithLongReason = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "20 Nov 2024", reason: "Hello would like to join this room, also this is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long reason")
|
static let aliceWithLongReason = KnockRequestCellInfo(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "20 Nov 2024", reason: "Hello would like to join this room, also this is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long reason")
|
||||||
|
|
||||||
static let aliceWithShortReason = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "20 Nov 2024", reason: "Hello, I am Alice and would like to join this room, please")
|
static let aliceWithShortReason = KnockRequestCellInfo(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "20 Nov 2024", reason: "Hello, I am Alice and would like to join this room, please")
|
||||||
|
|
||||||
static let aliceWithNoReason = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "20 Nov 2024", reason: nil)
|
static let aliceWithNoReason = KnockRequestCellInfo(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "20 Nov 2024", reason: nil)
|
||||||
|
|
||||||
static let aliceWithNoName = KnockRequestCellInfo(id: "@alice:matrix.org", displayName: nil, avatarURL: nil, timestamp: "20 Nov 2024", reason: nil)
|
static let aliceWithNoName = KnockRequestCellInfo(eventID: "1", userID: "@alice:matrix.org", displayName: nil, avatarURL: nil, timestamp: "20 Nov 2024", reason: nil)
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
KnockRequestCell(cellInfo: aliceWithLongReason) { _ in } onDecline: { _ in } onDeclineAndBan: { _ in }
|
KnockRequestCell(cellInfo: aliceWithLongReason) { _ in } onDecline: { _ in } onDeclineAndBan: { _ in }
|
||||||
|
@ -17,12 +17,12 @@ struct KnockRequestsListScreen: View {
|
|||||||
.navigationTitle(L10n.screenKnockRequestsListTitle)
|
.navigationTitle(L10n.screenKnockRequestsListTitle)
|
||||||
.background(.compound.bgCanvasDefault)
|
.background(.compound.bgCanvasDefault)
|
||||||
.overlay {
|
.overlay {
|
||||||
if !context.viewState.shouldDisplayRequests {
|
if context.viewState.shouldDisplayEmptyView {
|
||||||
KnockRequestsListEmptyStateView()
|
KnockRequestsListEmptyStateView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.safeAreaInset(edge: .bottom) {
|
.safeAreaInset(edge: .bottom) {
|
||||||
if context.viewState.shouldDisplayRequests {
|
if context.viewState.shouldDisplayAcceptAllButton {
|
||||||
acceptAllButton
|
acceptAllButton
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -31,10 +31,18 @@ struct KnockRequestsListScreen: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var mainContent: some View {
|
private var mainContent: some View {
|
||||||
|
if context.viewState.isLoading {
|
||||||
|
EmptyView()
|
||||||
|
} else {
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var list: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
if context.viewState.shouldDisplayRequests {
|
if context.viewState.shouldDisplayRequests {
|
||||||
ForEach(context.viewState.requests) { requestInfo in
|
ForEach(context.viewState.displayedRequests) { requestInfo in
|
||||||
ListRow(kind: .custom {
|
ListRow(kind: .custom {
|
||||||
KnockRequestCell(cellInfo: requestInfo,
|
KnockRequestCell(cellInfo: requestInfo,
|
||||||
mediaProvider: context.mediaProvider,
|
mediaProvider: context.mediaProvider,
|
||||||
@ -60,37 +68,66 @@ struct KnockRequestsListScreen: View {
|
|||||||
.background(.compound.bgCanvasDefault)
|
.background(.compound.bgCanvasDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onAccept(userID: String) {
|
private func onAccept(eventID: String) {
|
||||||
context.send(viewAction: .acceptRequest(userID: userID))
|
context.send(viewAction: .acceptRequest(eventID: eventID))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onDecline(userID: String) {
|
private func onDecline(eventID: String) {
|
||||||
context.send(viewAction: .declineRequest(userID: userID))
|
context.send(viewAction: .declineRequest(eventID: eventID))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onDeclineAndBan(userID: String) {
|
private func onDeclineAndBan(eventID: String) {
|
||||||
context.send(viewAction: .ban(userID: userID))
|
context.send(viewAction: .ban(eventID: eventID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
struct KnockRequestsListScreen_Previews: PreviewProvider, TestablePreview {
|
struct KnockRequestsListScreen_Previews: PreviewProvider, TestablePreview {
|
||||||
static let emptyViewModel = KnockRequestsListScreenViewModel.mockWithInitialState(.init(requests: []))
|
static let loadingViewModel = KnockRequestsListScreenViewModel.mockWithRequestsState(.loading)
|
||||||
|
|
||||||
static let viewModel = KnockRequestsListScreenViewModel.mockWithInitialState(.init(requests: [.init(id: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "Now", reason: "Hello"),
|
static let emptyViewModel = KnockRequestsListScreenViewModel.mockWithRequestsState(.loaded([]))
|
||||||
|
|
||||||
|
static let singleRequestViewModel = KnockRequestsListScreenViewModel.mockWithRequestsState(.loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "Now", reason: "Hello"))]))
|
||||||
|
|
||||||
|
static let viewModel = KnockRequestsListScreenViewModel.mockWithRequestsState(.loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", avatarURL: nil, timestamp: "Now", reason: "Hello")),
|
||||||
// swiftlint:disable:next line_length
|
// swiftlint:disable:next line_length
|
||||||
.init(id: "@bob:matrix.org", displayName: "Bob", avatarURL: nil, timestamp: "Now", reason: "Hello this one is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long reason"),
|
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org", displayName: "Bob", avatarURL: nil, timestamp: "Now", reason: "Hello this one is a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long reason")),
|
||||||
.init(id: "@charlie:matrix.org", displayName: "Charlie", avatarURL: nil, timestamp: "Now", reason: nil),
|
KnockRequestProxyMock(.init(eventID: "3", userID: "@charlie:matrix.org", displayName: "Charlie", avatarURL: nil, timestamp: "Now", reason: nil)),
|
||||||
.init(id: "@dan:matrix.org", displayName: "Dan", avatarURL: nil, timestamp: "Now", reason: "Hello! It's a me! Dan!")]))
|
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org", displayName: "Dan", avatarURL: nil, timestamp: "Now", reason: "Hello! It's a me! Dan!"))]))
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
KnockRequestsListScreen(context: viewModel.context)
|
KnockRequestsListScreen(context: viewModel.context)
|
||||||
}
|
}
|
||||||
|
.snapshotPreferences(delay: 0.2)
|
||||||
|
|
||||||
|
NavigationStack {
|
||||||
|
KnockRequestsListScreen(context: singleRequestViewModel.context)
|
||||||
|
}
|
||||||
|
.previewDisplayName("Single Request")
|
||||||
|
.snapshotPreferences(delay: 0.2)
|
||||||
|
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
KnockRequestsListScreen(context: emptyViewModel.context)
|
KnockRequestsListScreen(context: emptyViewModel.context)
|
||||||
}
|
}
|
||||||
.previewDisplayName("Empty state")
|
.previewDisplayName("Empty state")
|
||||||
|
.snapshotPreferences(delay: 0.2)
|
||||||
|
|
||||||
|
NavigationStack {
|
||||||
|
KnockRequestsListScreen(context: loadingViewModel.context)
|
||||||
|
}
|
||||||
|
.previewDisplayName("Loading state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension KnockRequestsListScreenViewModel {
|
||||||
|
static func mockWithRequestsState(_ requestsState: KnockRequestsState) -> KnockRequestsListScreenViewModel {
|
||||||
|
.init(roomProxy: JoinedRoomProxyMock(.init(members: [.mockAdmin],
|
||||||
|
knockRequestsState: requestsState,
|
||||||
|
ownUserID: RoomMemberProxyMock.mockAdmin.userID,
|
||||||
|
joinRule: .knock)),
|
||||||
|
mediaProvider: MediaProviderMock(),
|
||||||
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,8 @@ struct RoomDetailsScreenViewState: BindableState {
|
|||||||
|
|
||||||
var knockingEnabled = false
|
var knockingEnabled = false
|
||||||
var isKnockableRoom = false
|
var isKnockableRoom = false
|
||||||
|
var knockRequestsCount = 0
|
||||||
|
|
||||||
var canSeeKnockingRequests: Bool {
|
var canSeeKnockingRequests: Bool {
|
||||||
knockingEnabled && dmRecipient == nil && isKnockableRoom && (canInviteUsers || canKickUsers || canBanUsers)
|
knockingEnabled && dmRecipient == nil && isKnockableRoom && (canInviteUsers || canKickUsers || canBanUsers)
|
||||||
}
|
}
|
||||||
|
@ -187,6 +187,18 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
|
|||||||
Task { await self?.updatePowerLevelPermissions() }
|
Task { await self?.updatePowerLevelPermissions() }
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
roomProxy.knockRequestsStatePublisher
|
||||||
|
.map { requestsState in
|
||||||
|
guard case let .loaded(requests) = requestsState else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return requests.count
|
||||||
|
}
|
||||||
|
.removeDuplicates()
|
||||||
|
.throttle(for: .milliseconds(100), scheduler: DispatchQueue.main, latest: true)
|
||||||
|
.weakAssign(to: \.state.knockRequestsCount, on: self)
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateRoomInfo(_ roomInfo: RoomInfoProxy) {
|
private func updateRoomInfo(_ roomInfo: RoomInfoProxy) {
|
||||||
|
@ -166,8 +166,7 @@ struct RoomDetailsScreen: View {
|
|||||||
if context.viewState.canSeeKnockingRequests {
|
if context.viewState.canSeeKnockingRequests {
|
||||||
ListRow(label: .default(title: L10n.screenRoomDetailsRequestsToJoinTitle,
|
ListRow(label: .default(title: L10n.screenRoomDetailsRequestsToJoinTitle,
|
||||||
icon: \.askToJoin),
|
icon: \.askToJoin),
|
||||||
// TODO: Display count if requests > 0 when an API for them is available
|
details: context.viewState.knockRequestsCount > 0 ? .counter(context.viewState.knockRequestsCount) : nil,
|
||||||
details: .counter(1),
|
|
||||||
kind: .navigationLink {
|
kind: .navigationLink {
|
||||||
context.send(viewAction: .processTapRequestsToJoin)
|
context.send(viewAction: .processTapRequestsToJoin)
|
||||||
})
|
})
|
||||||
@ -324,6 +323,7 @@ struct RoomDetailsScreen: View {
|
|||||||
struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
||||||
static let genericRoomViewModel = {
|
static let genericRoomViewModel = {
|
||||||
ServiceLocator.shared.settings.knockingEnabled = true
|
ServiceLocator.shared.settings.knockingEnabled = true
|
||||||
|
let knockRequests: [KnockRequestProxyMock] = [.init()]
|
||||||
let members: [RoomMemberProxyMock] = [
|
let members: [RoomMemberProxyMock] = [
|
||||||
.mockMeAdmin,
|
.mockMeAdmin,
|
||||||
.mockAlice,
|
.mockAlice,
|
||||||
@ -344,6 +344,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
isEncrypted: true,
|
isEncrypted: true,
|
||||||
canonicalAlias: "#alias:domain.com",
|
canonicalAlias: "#alias:domain.com",
|
||||||
members: members,
|
members: members,
|
||||||
|
knockRequestsState: .loaded(knockRequests),
|
||||||
joinRule: .knock))
|
joinRule: .knock))
|
||||||
|
|
||||||
var notificationSettingsProxyMockConfiguration = NotificationSettingsProxyMockConfiguration()
|
var notificationSettingsProxyMockConfiguration = NotificationSettingsProxyMockConfiguration()
|
||||||
@ -388,6 +389,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
static let simpleRoomViewModel = {
|
static let simpleRoomViewModel = {
|
||||||
|
let knockRequests: [KnockRequestProxyMock] = [.init()]
|
||||||
ServiceLocator.shared.settings.knockingEnabled = true
|
ServiceLocator.shared.settings.knockingEnabled = true
|
||||||
let members: [RoomMemberProxyMock] = [
|
let members: [RoomMemberProxyMock] = [
|
||||||
.mockMeAdmin,
|
.mockMeAdmin,
|
||||||
@ -400,6 +402,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
isDirect: false,
|
isDirect: false,
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
members: members,
|
members: members,
|
||||||
|
knockRequestsState: .loaded(knockRequests),
|
||||||
joinRule: .knock))
|
joinRule: .knock))
|
||||||
let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init())
|
let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init())
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import OrderedCollections
|
import OrderedCollections
|
||||||
|
|
||||||
enum RoomScreenViewModelAction {
|
enum RoomScreenViewModelAction: Equatable {
|
||||||
case focusEvent(eventID: String)
|
case focusEvent(eventID: String)
|
||||||
case displayPinnedEventsTimeline
|
case displayPinnedEventsTimeline
|
||||||
case displayRoomDetails
|
case displayRoomDetails
|
||||||
@ -23,7 +23,7 @@ enum RoomScreenViewAction {
|
|||||||
case displayRoomDetails
|
case displayRoomDetails
|
||||||
case displayCall
|
case displayCall
|
||||||
case footerViewAction(RoomScreenFooterViewAction)
|
case footerViewAction(RoomScreenFooterViewAction)
|
||||||
case acceptKnock(userID: String)
|
case acceptKnock(eventID: String)
|
||||||
case dismissKnockRequests
|
case dismissKnockRequests
|
||||||
case viewKnockRequests
|
case viewKnockRequests
|
||||||
}
|
}
|
||||||
@ -48,11 +48,18 @@ struct RoomScreenViewState: BindableState {
|
|||||||
var canAcceptKnocks = false
|
var canAcceptKnocks = false
|
||||||
var canDeclineKnocks = false
|
var canDeclineKnocks = false
|
||||||
var canBan = false
|
var canBan = false
|
||||||
// TODO: We still don't know how to get these, but these will be the non already seen knock requests of the room, for now we are using this as a mock for testing purposes
|
var unseenKnockRequests: [KnockRequestInfo] = []
|
||||||
var unseenKnockRequests: [KnockRequestInfo] = [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Helloooo")]
|
var handledEventIDs: Set<String> = []
|
||||||
|
|
||||||
|
var displayedKnockRequests: [KnockRequestInfo] {
|
||||||
|
unseenKnockRequests.filter { !handledEventIDs.contains($0.eventID) }
|
||||||
|
}
|
||||||
|
|
||||||
var shouldSeeKnockRequests: Bool {
|
var shouldSeeKnockRequests: Bool {
|
||||||
isKnockingEnabled && isKnockableRoom && !unseenKnockRequests.isEmpty && (canAcceptKnocks || canDeclineKnocks || canBan)
|
isKnockingEnabled &&
|
||||||
|
isKnockableRoom &&
|
||||||
|
!displayedKnockRequests.isEmpty &&
|
||||||
|
(canAcceptKnocks || canDeclineKnocks || canBan)
|
||||||
}
|
}
|
||||||
|
|
||||||
var footerDetails: RoomScreenFooterViewDetails?
|
var footerDetails: RoomScreenFooterViewDetails?
|
||||||
|
@ -103,12 +103,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
case .resolvePinViolation(let userID):
|
case .resolvePinViolation(let userID):
|
||||||
Task { await resolveIdentityPinningViolation(userID) }
|
Task { await resolveIdentityPinningViolation(userID) }
|
||||||
}
|
}
|
||||||
case .acceptKnock(userID: let userID):
|
case .acceptKnock(let eventID):
|
||||||
// TODO: API to accept a knock required
|
Task { await acceptKnock(eventID: eventID) }
|
||||||
break
|
|
||||||
case .dismissKnockRequests:
|
case .dismissKnockRequests:
|
||||||
// TODO: API to mark knocks as seen required
|
Task { await markAllKnocksAsSeen() }
|
||||||
break
|
|
||||||
case .viewKnockRequests:
|
case .viewKnockRequests:
|
||||||
actionsSubject.send(.displayKnockRequests)
|
actionsSubject.send(.displayKnockRequests)
|
||||||
}
|
}
|
||||||
@ -181,6 +179,23 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
state.shouldShowCallButton = ongoingCallRoomID != roomProxy.id
|
state.shouldShowCallButton = ongoingCallRoomID != roomProxy.id
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
roomProxy.knockRequestsStatePublisher
|
||||||
|
// We only care about unseen requests
|
||||||
|
.map { knockRequestsState in
|
||||||
|
guard case let .loaded(requests) = knockRequestsState else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return requests
|
||||||
|
.filter { !$0.isSeen }
|
||||||
|
.map(KnockRequestInfo.init)
|
||||||
|
}
|
||||||
|
// If the requests have the same event ids we can discard the output
|
||||||
|
.removeDuplicates { Set($0.map(\.eventID)) == Set($1.map(\.eventID)) }
|
||||||
|
.throttle(for: .milliseconds(100), scheduler: DispatchQueue.main, latest: true)
|
||||||
|
.weakAssign(to: \.state.unseenKnockRequests, on: self)
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processIdentityStatusChanges(_ changes: [IdentityStatusChange]) async {
|
private func processIdentityStatusChanges(_ changes: [IdentityStatusChange]) async {
|
||||||
@ -278,9 +293,48 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func acceptKnock(eventID: String) async {
|
||||||
|
guard case let .loaded(requests) = roomProxy.knockRequestsStatePublisher.value,
|
||||||
|
let request = requests.first(where: { $0.eventID == eventID }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.handledEventIDs.insert(eventID)
|
||||||
|
switch await request.accept() {
|
||||||
|
case .success:
|
||||||
|
break
|
||||||
|
case .failure:
|
||||||
|
userIndicatorController.submitIndicator(.init(id: Self.errorIndicatorIdentifier, type: .toast, title: L10n.errorUnknown))
|
||||||
|
state.handledEventIDs.remove(eventID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func markAllKnocksAsSeen() async {
|
||||||
|
guard case let .loaded(requests) = roomProxy.knockRequestsStatePublisher.value else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.handledEventIDs.formUnion(Set(requests.map(\.eventID)))
|
||||||
|
|
||||||
|
let failedIDs = await withTaskGroup(of: (String, Result<Void, KnockRequestProxyError>).self) { group in
|
||||||
|
for request in requests {
|
||||||
|
group.addTask {
|
||||||
|
await (request.eventID, request.markAsSeen())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var failedIDs = [String]()
|
||||||
|
for await result in group where result.1.isFailure {
|
||||||
|
failedIDs.append(result.0)
|
||||||
|
}
|
||||||
|
return failedIDs
|
||||||
|
}
|
||||||
|
state.handledEventIDs.subtract(failedIDs)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Loading indicators
|
// MARK: Loading indicators
|
||||||
|
|
||||||
private static let loadingIndicatorIdentifier = "\(RoomScreenViewModel.self)-Loading"
|
private static let loadingIndicatorIdentifier = "\(RoomScreenViewModel.self)-Loading"
|
||||||
|
private static let errorIndicatorIdentifier = "\(RoomScreenViewModel.self)-Error"
|
||||||
|
|
||||||
private func showLoadingIndicator() {
|
private func showLoadingIndicator() {
|
||||||
userIndicatorController.submitIndicator(.init(id: Self.loadingIndicatorIdentifier, type: .toast, title: L10n.commonLoading))
|
userIndicatorController.submitIndicator(.init(id: Self.loadingIndicatorIdentifier, type: .toast, title: L10n.commonLoading))
|
||||||
@ -304,3 +358,13 @@ extension RoomScreenViewModel {
|
|||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension KnockRequestInfo {
|
||||||
|
init(from proxy: KnockRequestProxyProtocol) {
|
||||||
|
self.init(displayName: proxy.displayName,
|
||||||
|
avatarURL: proxy.avatarURL,
|
||||||
|
userID: proxy.userID,
|
||||||
|
reason: proxy.reason,
|
||||||
|
eventID: proxy.eventID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -8,11 +8,12 @@
|
|||||||
import Compound
|
import Compound
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct KnockRequestInfo {
|
struct KnockRequestInfo: Equatable {
|
||||||
let displayName: String?
|
let displayName: String?
|
||||||
let avatarURL: URL?
|
let avatarURL: URL?
|
||||||
let userID: String
|
let userID: String
|
||||||
let reason: String?
|
let reason: String?
|
||||||
|
let eventID: String
|
||||||
}
|
}
|
||||||
|
|
||||||
struct KnockRequestsBannerView: View {
|
struct KnockRequestsBannerView: View {
|
||||||
@ -102,9 +103,7 @@ private struct SingleKnockRequestBannerContent: View {
|
|||||||
Button(L10n.screenRoomSingleKnockRequestViewButtonTitle, action: onViewAll)
|
Button(L10n.screenRoomSingleKnockRequestViewButtonTitle, action: onViewAll)
|
||||||
.buttonStyle(.compound(.secondary, size: .medium))
|
.buttonStyle(.compound(.secondary, size: .medium))
|
||||||
if let onAccept {
|
if let onAccept {
|
||||||
Button(L10n.screenRoomSingleKnockRequestAcceptButtonTitle) {
|
Button(L10n.screenRoomSingleKnockRequestAcceptButtonTitle) { onAccept(request.eventID) }
|
||||||
onAccept(request.userID)
|
|
||||||
}
|
|
||||||
.buttonStyle(.compound(.primary, size: .medium))
|
.buttonStyle(.compound(.primary, size: .medium))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,7 +122,6 @@ private struct MultipleKnockRequestsBannerContent: View {
|
|||||||
requests
|
requests
|
||||||
.prefix(3)
|
.prefix(3)
|
||||||
.map { .init(url: $0.avatarURL, name: $0.displayName, contentID: $0.userID) }
|
.map { .init(url: $0.avatarURL, name: $0.displayName, contentID: $0.userID) }
|
||||||
.reversed()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var multipleKnockRequestsTitle: String {
|
private var multipleKnockRequestsTitle: String {
|
||||||
@ -138,7 +136,7 @@ private struct MultipleKnockRequestsBannerContent: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 14) {
|
VStack(spacing: 14) {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
StackedAvatarsView(overlap: 16, lineWidth: 2, shouldStackFromLast: true, avatars: avatars, avatarSize: .user(on: .knockingUsersBannerStack), mediaProvider: mediaProvider)
|
StackedAvatarsView(overlap: 16, lineWidth: 2, avatars: avatars, avatarSize: .user(on: .knockingUsersBannerStack), mediaProvider: mediaProvider)
|
||||||
HStack(alignment: .top, spacing: 0) {
|
HStack(alignment: .top, spacing: 0) {
|
||||||
Text(multipleKnockRequestsTitle)
|
Text(multipleKnockRequestsTitle)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
@ -173,18 +171,22 @@ private struct KnockRequestsBannerDismissButton: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct KnockRequestsBannerView_Previews: PreviewProvider, TestablePreview {
|
struct KnockRequestsBannerView_Previews: PreviewProvider, TestablePreview {
|
||||||
static let singleRequest: [KnockRequestInfo] = [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: nil)]
|
static let singleRequest: [KnockRequestInfo] = [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: nil, eventID: "1")]
|
||||||
|
|
||||||
static let singleRequestWithReason: [KnockRequestInfo] = [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Hey, I’d like to join this room because of xyz topic and I’d like to participate in the room.")]
|
static let singleRequestWithReason: [KnockRequestInfo] = [.init(displayName: "Alice",
|
||||||
|
avatarURL: nil,
|
||||||
|
userID: "@alice:matrix.org",
|
||||||
|
reason: "Hey, I’d like to join this room because of xyz topic and I’d like to participate in the room.",
|
||||||
|
eventID: "1")]
|
||||||
|
|
||||||
static let singleRequestNoDisplayName: [KnockRequestInfo] = [.init(displayName: nil, avatarURL: nil, userID: "@alice:matrix.org", reason: nil)]
|
static let singleRequestNoDisplayName: [KnockRequestInfo] = [.init(displayName: nil, avatarURL: nil, userID: "@alice:matrix.org", reason: nil, eventID: "1")]
|
||||||
|
|
||||||
static let multipleRequests: [KnockRequestInfo] = [
|
static let multipleRequests: [KnockRequestInfo] = [
|
||||||
.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: nil),
|
.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: nil, eventID: "1"),
|
||||||
.init(displayName: "Bob", avatarURL: nil, userID: "@bob:matrix.org", reason: nil),
|
.init(displayName: "Bob", avatarURL: nil, userID: "@bob:matrix.org", reason: nil, eventID: "2"),
|
||||||
.init(displayName: "Charlie", avatarURL: nil, userID: "@charlie:matrix.org", reason: nil),
|
.init(displayName: "Charlie", avatarURL: nil, userID: "@charlie:matrix.org", reason: nil, eventID: "3"),
|
||||||
.init(displayName: "Dan", avatarURL: nil, userID: "@dan:matrix.org", reason: nil),
|
.init(displayName: "Dan", avatarURL: nil, userID: "@dan:matrix.org", reason: nil, eventID: "4"),
|
||||||
.init(displayName: "Test", avatarURL: nil, userID: "@dan:matrix.org", reason: nil)
|
.init(displayName: "Test", avatarURL: nil, userID: "@dan:matrix.org", reason: nil, eventID: "5")
|
||||||
]
|
]
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
|
@ -137,7 +137,7 @@ struct RoomScreen: View {
|
|||||||
private var knockRequestsBanner: some View {
|
private var knockRequestsBanner: some View {
|
||||||
Group {
|
Group {
|
||||||
if roomContext.viewState.shouldSeeKnockRequests {
|
if roomContext.viewState.shouldSeeKnockRequests {
|
||||||
KnockRequestsBannerView(requests: roomContext.viewState.unseenKnockRequests,
|
KnockRequestsBannerView(requests: roomContext.viewState.displayedKnockRequests,
|
||||||
onDismiss: dismissKnockRequestsBanner,
|
onDismiss: dismissKnockRequestsBanner,
|
||||||
onAccept: roomContext.viewState.canAcceptKnocks ? acceptKnockRequest : nil,
|
onAccept: roomContext.viewState.canAcceptKnocks ? acceptKnockRequest : nil,
|
||||||
onViewAll: onViewAllKnockRequests,
|
onViewAll: onViewAllKnockRequests,
|
||||||
@ -153,8 +153,8 @@ struct RoomScreen: View {
|
|||||||
roomContext.send(viewAction: .dismissKnockRequests)
|
roomContext.send(viewAction: .dismissKnockRequests)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func acceptKnockRequest(userID: String) {
|
private func acceptKnockRequest(eventID: String) {
|
||||||
roomContext.send(viewAction: .acceptKnock(userID: userID))
|
roomContext.send(viewAction: .acceptKnock(eventID: eventID))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func onViewAllKnockRequests() {
|
private func onViewAllKnockRequests() {
|
||||||
|
@ -1146,10 +1146,31 @@ private extension RoomPreviewDetails {
|
|||||||
topic: roomPreviewInfo.topic,
|
topic: roomPreviewInfo.topic,
|
||||||
avatarURL: roomPreviewInfo.avatarUrl.flatMap(URL.init(string:)),
|
avatarURL: roomPreviewInfo.avatarUrl.flatMap(URL.init(string:)),
|
||||||
memberCount: UInt(roomPreviewInfo.numJoinedMembers),
|
memberCount: UInt(roomPreviewInfo.numJoinedMembers),
|
||||||
isHistoryWorldReadable: roomPreviewInfo.isHistoryWorldReadable,
|
isHistoryWorldReadable: roomPreviewInfo.isHistoryWorldReadable ?? false,
|
||||||
isJoined: roomPreviewInfo.membership == .joined,
|
isJoined: roomPreviewInfo.membership == .joined,
|
||||||
isInvited: roomPreviewInfo.membership == .invited,
|
isInvited: roomPreviewInfo.membership == .invited,
|
||||||
isPublic: roomPreviewInfo.joinRule == .public,
|
isPublic: roomPreviewInfo.isPublic,
|
||||||
canKnock: roomPreviewInfo.joinRule == .knock)
|
canKnock: roomPreviewInfo.canKnock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension RoomPreviewInfo {
|
||||||
|
var canKnock: Bool {
|
||||||
|
switch joinRule {
|
||||||
|
case .knock, .knockRestricted:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isPublic: Bool {
|
||||||
|
switch joinRule {
|
||||||
|
// for restricted rooms we want to show optimistically that the we may be able to join the room
|
||||||
|
case .public, .restricted:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,8 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
|||||||
private var typingNotificationObservationToken: TaskHandle?
|
private var typingNotificationObservationToken: TaskHandle?
|
||||||
// periphery:ignore - required for instance retention in the rust codebase
|
// periphery:ignore - required for instance retention in the rust codebase
|
||||||
private var identityStatusChangesObservationToken: TaskHandle?
|
private var identityStatusChangesObservationToken: TaskHandle?
|
||||||
|
// periphery:ignore - required for instance retention in the rust codebase
|
||||||
|
private var knockRequestsChangesObservationToken: TaskHandle?
|
||||||
|
|
||||||
private var subscribedForUpdates = false
|
private var subscribedForUpdates = false
|
||||||
|
|
||||||
@ -83,6 +85,11 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
|||||||
identityStatusChangesSubject.asCurrentValuePublisher()
|
identityStatusChangesSubject.asCurrentValuePublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let knockRequestsStateSubject = CurrentValueSubject<KnockRequestsState, Never>(.loading)
|
||||||
|
var knockRequestsStatePublisher: CurrentValuePublisher<KnockRequestsState, Never> {
|
||||||
|
knockRequestsStateSubject.asCurrentValuePublisher()
|
||||||
|
}
|
||||||
|
|
||||||
// A room identifier is constant and lazy stops it from being fetched
|
// A room identifier is constant and lazy stops it from being fetched
|
||||||
// multiple times over FFI
|
// multiple times over FFI
|
||||||
lazy var id: String = room.id()
|
lazy var id: String = room.id()
|
||||||
@ -131,6 +138,8 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subscribeToTypingNotifications()
|
subscribeToTypingNotifications()
|
||||||
|
|
||||||
|
await subscribeToKnockRequests()
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribeToRoomInfoUpdates() {
|
func subscribeToRoomInfoUpdates() {
|
||||||
@ -645,6 +654,19 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
|
|||||||
identityStatusChangesSubject.send(changes)
|
identityStatusChangesSubject.send(changes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func subscribeToKnockRequests() async {
|
||||||
|
do {
|
||||||
|
knockRequestsChangesObservationToken = try await room.subscribeToKnockRequests(listener: RoomKnockRequestsListener { [weak self] requests in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
MXLog.info("Received requests to join update, requests id: \(requests.map(\.eventId))")
|
||||||
|
knockRequestsStateSubject.send(.loaded(requests.map(KnockRequestProxy.init)))
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed observing requests to join with error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class RoomInfoUpdateListener: RoomInfoListener {
|
private final class RoomInfoUpdateListener: RoomInfoListener {
|
||||||
@ -682,3 +704,15 @@ private final class RoomIdentityStatusChangeListener: IdentityStatusChangeListen
|
|||||||
onUpdateClosure(identityStatusChange)
|
onUpdateClosure(identityStatusChange)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class RoomKnockRequestsListener: KnockRequestsListener {
|
||||||
|
private let onUpdateClosure: ([KnockRequest]) -> Void
|
||||||
|
|
||||||
|
init(_ onUpdateClosure: @escaping ([KnockRequest]) -> Void) {
|
||||||
|
self.onUpdateClosure = onUpdateClosure
|
||||||
|
}
|
||||||
|
|
||||||
|
func call(joinRequests: [KnockRequest]) {
|
||||||
|
onUpdateClosure(joinRequests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
90
ElementX/Sources/Services/Room/KnockRequestProxy.swift
Normal file
90
ElementX/Sources/Services/Room/KnockRequestProxy.swift
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2024 New Vector Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
// Please see LICENSE in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import MatrixRustSDK
|
||||||
|
|
||||||
|
struct KnockRequestProxy: KnockRequestProxyProtocol {
|
||||||
|
private let knockRequest: KnockRequest
|
||||||
|
|
||||||
|
init(knockRequest: KnockRequest) {
|
||||||
|
self.knockRequest = knockRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventID: String {
|
||||||
|
knockRequest.eventId
|
||||||
|
}
|
||||||
|
|
||||||
|
var userID: String {
|
||||||
|
knockRequest.userId
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayName: String? {
|
||||||
|
knockRequest.displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
var avatarURL: URL? {
|
||||||
|
knockRequest.avatarUrl.flatMap(URL.init)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reason: String? {
|
||||||
|
knockRequest.reason
|
||||||
|
}
|
||||||
|
|
||||||
|
var formattedTimestamp: String? {
|
||||||
|
guard let timestamp = knockRequest.timestamp else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Date(timeIntervalSince1970: TimeInterval(timestamp / 1000)).formattedMinimal()
|
||||||
|
}
|
||||||
|
|
||||||
|
var isSeen: Bool {
|
||||||
|
knockRequest.isSeen
|
||||||
|
}
|
||||||
|
|
||||||
|
func accept() async -> Result<Void, KnockRequestProxyError> {
|
||||||
|
do {
|
||||||
|
try await knockRequest.actions.accept()
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed accepting request with eventID: \(eventID) to join error: \(error)")
|
||||||
|
return .failure(.sdkError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decline() async -> Result<Void, KnockRequestProxyError> {
|
||||||
|
do {
|
||||||
|
// As of right now we don't provide reasons in the app for declining
|
||||||
|
try await knockRequest.actions.decline(reason: nil)
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed declining request with eventID: \(eventID) to join error: \(error)")
|
||||||
|
return .failure(.sdkError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ban() async -> Result<Void, KnockRequestProxyError> {
|
||||||
|
do {
|
||||||
|
// As of right now we don't provide reasons in the app for declining and banning
|
||||||
|
try await knockRequest.actions.declineAndBan(reason: nil)
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed declining and banning user for request with eventID: \(eventID) with error: \(error)")
|
||||||
|
return .failure(.sdkError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func markAsSeen() async -> Result<Void, KnockRequestProxyError> {
|
||||||
|
do {
|
||||||
|
try await knockRequest.actions.markAsSeen()
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed marking request with eventID: \(eventID) to join as seen error: \(error)")
|
||||||
|
return .failure(.sdkError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2024 New Vector Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
// Please see LICENSE in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum KnockRequestProxyError: Error {
|
||||||
|
case sdkError(Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sourcery: AutoMockable
|
||||||
|
protocol KnockRequestProxyProtocol {
|
||||||
|
var eventID: String { get }
|
||||||
|
var userID: String { get }
|
||||||
|
var displayName: String? { get }
|
||||||
|
var avatarURL: URL? { get }
|
||||||
|
var reason: String? { get }
|
||||||
|
var formattedTimestamp: String? { get }
|
||||||
|
var isSeen: Bool { get }
|
||||||
|
|
||||||
|
func accept() async -> Result<Void, KnockRequestProxyError>
|
||||||
|
func decline() async -> Result<Void, KnockRequestProxyError>
|
||||||
|
func ban() async -> Result<Void, KnockRequestProxyError>
|
||||||
|
func markAsSeen() async -> Result<Void, KnockRequestProxyError>
|
||||||
|
}
|
@ -48,6 +48,11 @@ enum JoinedRoomProxyAction: Equatable {
|
|||||||
case roomInfoUpdate
|
case roomInfoUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum KnockRequestsState {
|
||||||
|
case loading
|
||||||
|
case loaded([KnockRequestProxyProtocol])
|
||||||
|
}
|
||||||
|
|
||||||
// sourcery: AutoMockable
|
// sourcery: AutoMockable
|
||||||
protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
|
protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
|
||||||
var isEncrypted: Bool { get }
|
var isEncrypted: Bool { get }
|
||||||
@ -60,6 +65,8 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
|
|||||||
|
|
||||||
var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { get }
|
var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { get }
|
||||||
|
|
||||||
|
var knockRequestsStatePublisher: CurrentValuePublisher<KnockRequestsState, Never> { get }
|
||||||
|
|
||||||
var timeline: TimelineProxyProtocol { get }
|
var timeline: TimelineProxyProtocol { get }
|
||||||
|
|
||||||
var pinnedEventsTimeline: TimelineProxyProtocol? { get async }
|
var pinnedEventsTimeline: TimelineProxyProtocol? { get async }
|
||||||
|
@ -9,7 +9,7 @@ import Foundation
|
|||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
|
|
||||||
struct RoomSummary {
|
struct RoomSummary {
|
||||||
enum JoinRequestType {
|
enum KnockRequestType {
|
||||||
case invite(inviter: RoomMemberProxyProtocol?)
|
case invite(inviter: RoomMemberProxyProtocol?)
|
||||||
case knock
|
case knock
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ struct RoomSummary {
|
|||||||
|
|
||||||
let id: String
|
let id: String
|
||||||
|
|
||||||
let joinRequestType: JoinRequestType?
|
let knockRequestType: KnockRequestType?
|
||||||
|
|
||||||
let name: String
|
let name: String
|
||||||
let isDirect: Bool
|
let isDirect: Bool
|
||||||
@ -103,7 +103,7 @@ extension RoomSummary {
|
|||||||
canonicalAlias = nil
|
canonicalAlias = nil
|
||||||
hasOngoingCall = false
|
hasOngoingCall = false
|
||||||
|
|
||||||
joinRequestType = nil
|
knockRequestType = nil
|
||||||
isMarkedUnread = false
|
isMarkedUnread = false
|
||||||
isFavourite = false
|
isFavourite = false
|
||||||
}
|
}
|
||||||
|
@ -255,7 +255,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
|
|
||||||
let notificationMode = roomInfo.cachedUserDefinedNotificationMode.flatMap { RoomNotificationModeProxy.from(roomNotificationMode: $0) }
|
let notificationMode = roomInfo.cachedUserDefinedNotificationMode.flatMap { RoomNotificationModeProxy.from(roomNotificationMode: $0) }
|
||||||
|
|
||||||
let joinRequestType: RoomSummary.JoinRequestType? = switch roomInfo.membership {
|
let knockRequestType: RoomSummary.KnockRequestType? = switch roomInfo.membership {
|
||||||
case .invited: .invite(inviter: inviterProxy)
|
case .invited: .invite(inviter: inviterProxy)
|
||||||
case .knocked: .knock
|
case .knocked: .knock
|
||||||
default: nil
|
default: nil
|
||||||
@ -263,7 +263,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
|
|
||||||
return RoomSummary(roomListItem: roomListItem,
|
return RoomSummary(roomListItem: roomListItem,
|
||||||
id: roomInfo.id,
|
id: roomInfo.id,
|
||||||
joinRequestType: joinRequestType,
|
knockRequestType: knockRequestType,
|
||||||
name: roomInfo.displayName ?? roomInfo.id,
|
name: roomInfo.displayName ?? roomInfo.id,
|
||||||
isDirect: roomInfo.isDirect,
|
isDirect: roomInfo.isDirect,
|
||||||
avatarURL: roomInfo.avatarUrl.flatMap(URL.init(string:)),
|
avatarURL: roomInfo.avatarUrl.flatMap(URL.init(string:)),
|
||||||
|
@ -59,7 +59,7 @@ struct RoomStateEventStringBuilder {
|
|||||||
case .knocked:
|
case .knocked:
|
||||||
return memberIsYou ? L10n.stateEventRoomKnockByYou : L10n.stateEventRoomKnock(member)
|
return memberIsYou ? L10n.stateEventRoomKnockByYou : L10n.stateEventRoomKnock(member)
|
||||||
case .knockAccepted:
|
case .knockAccepted:
|
||||||
return senderIsYou ? L10n.stateEventRoomKnockAcceptedByYou(senderDisplayName) : L10n.stateEventRoomKnockAccepted(senderDisplayName, member)
|
return senderIsYou ? L10n.stateEventRoomKnockAcceptedByYou(member) : L10n.stateEventRoomKnockAccepted(senderDisplayName, member)
|
||||||
case .knockRetracted:
|
case .knockRetracted:
|
||||||
return memberIsYou ? L10n.stateEventRoomKnockRetractedByYou : L10n.stateEventRoomKnockRetracted(member)
|
return memberIsYou ? L10n.stateEventRoomKnockRetractedByYou : L10n.stateEventRoomKnockRetracted(member)
|
||||||
case .knockDenied:
|
case .knockDenied:
|
||||||
|
@ -153,7 +153,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
case .sentBeforeWeJoined:
|
case .sentBeforeWeJoined:
|
||||||
encryptionType = .megolmV1AesSha2(sessionID: sessionID, cause: .sentBeforeWeJoined)
|
encryptionType = .megolmV1AesSha2(sessionID: sessionID, cause: .sentBeforeWeJoined)
|
||||||
errorLabel = L10n.commonUnableToDecryptNoAccess
|
errorLabel = L10n.commonUnableToDecryptNoAccess
|
||||||
case .historicalMessage:
|
case .historicalMessageAndBackupIsDisabled, .historicalMessageAndDeviceIsUnverified:
|
||||||
encryptionType = .megolmV1AesSha2(sessionID: sessionID, cause: .historicalMessage)
|
encryptionType = .megolmV1AesSha2(sessionID: sessionID, cause: .historicalMessage)
|
||||||
errorLabel = L10n.timelineDecryptionFailureHistoricalEventNoKeyBackup
|
errorLabel = L10n.timelineDecryptionFailureHistoricalEventNoKeyBackup
|
||||||
case .withheldForUnverifiedOrInsecureDevice:
|
case .withheldForUnverifiedOrInsecureDevice:
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPad-en-GB.Loading-state.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPad-en-GB.Loading-state.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPad-en-GB.Single-Request.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPad-en-GB.Single-Request.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPad-pseudo.Loading-state.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPad-pseudo.Loading-state.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPad-pseudo.Single-Request.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPad-pseudo.Single-Request.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPhone-16-en-GB.Loading-state.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPhone-16-en-GB.Loading-state.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPhone-16-en-GB.Single-Request.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPhone-16-en-GB.Single-Request.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPhone-16-pseudo.Loading-state.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPhone-16-pseudo.Loading-state.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPhone-16-pseudo.Single-Request.png
(Stored with Git LFS)
Normal file
BIN
PreviewTests/Sources/__Snapshots__/PreviewTests/test_knockRequestsListScreen-iPhone-16-pseudo.Single-Request.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -23,7 +23,7 @@ class HomeScreenRoomTests: XCTestCase {
|
|||||||
hasOngoingCall: Bool) {
|
hasOngoingCall: Bool) {
|
||||||
roomSummary = RoomSummary(roomListItem: .init(noPointer: .init()),
|
roomSummary = RoomSummary(roomListItem: .init(noPointer: .init()),
|
||||||
id: "Test room",
|
id: "Test room",
|
||||||
joinRequestType: nil,
|
knockRequestType: nil,
|
||||||
name: "Test room",
|
name: "Test room",
|
||||||
isDirect: false,
|
isDirect: false,
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
|
@ -18,6 +18,209 @@ class KnockRequestsListScreenViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
override func setUpWithError() throws {
|
||||||
viewModel = KnockRequestsListScreenViewModel(roomProxy: JoinedRoomProxyMock(.init()), mediaProvider: MediaProviderMock())
|
AppSettings.resetAllSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoadingState() async throws {
|
||||||
|
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loading, joinRule: .knock))
|
||||||
|
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
mediaProvider: MediaProviderMock(),
|
||||||
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|
||||||
|
let deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
!state.shouldDisplayRequests &&
|
||||||
|
state.isKnockableRoom &&
|
||||||
|
state.canAccept &&
|
||||||
|
!state.canBan &&
|
||||||
|
!state.canDecline &&
|
||||||
|
state.isLoading &&
|
||||||
|
!state.shouldDisplayEmptyView
|
||||||
|
}
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEmptyState() async throws {
|
||||||
|
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([]), joinRule: .knock))
|
||||||
|
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
mediaProvider: MediaProviderMock(),
|
||||||
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|
||||||
|
let deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
!state.shouldDisplayRequests &&
|
||||||
|
state.isKnockableRoom &&
|
||||||
|
state.canAccept &&
|
||||||
|
!state.canBan &&
|
||||||
|
!state.canDecline &&
|
||||||
|
!state.isLoading &&
|
||||||
|
state.shouldDisplayEmptyView
|
||||||
|
}
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoadedState() async throws {
|
||||||
|
let roomProxyMock = JoinedRoomProxyMock(.init(members: [.mockAdmin],
|
||||||
|
knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")),
|
||||||
|
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org")),
|
||||||
|
KnockRequestProxyMock(.init(eventID: "3", userID: "@charlie:matrix.org")),
|
||||||
|
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
|
||||||
|
ownUserID: RoomMemberProxyMock.mockAdmin.userID,
|
||||||
|
joinRule: .knock))
|
||||||
|
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
mediaProvider: MediaProviderMock(),
|
||||||
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|
||||||
|
var deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.shouldDisplayRequests &&
|
||||||
|
state.isKnockableRoom &&
|
||||||
|
state.canAccept &&
|
||||||
|
state.canBan &&
|
||||||
|
state.canDecline &&
|
||||||
|
!state.isLoading &&
|
||||||
|
!state.shouldDisplayEmptyView &&
|
||||||
|
state.displayedRequests.count == 4 &&
|
||||||
|
state.handledEventIDs.isEmpty &&
|
||||||
|
state.shouldDisplayAcceptAllButton
|
||||||
|
}
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.shouldDisplayRequests &&
|
||||||
|
state.handledEventIDs == ["1"] &&
|
||||||
|
!state.shouldDisplayEmptyView &&
|
||||||
|
state.displayedRequests.count == 3 &&
|
||||||
|
state.shouldDisplayAcceptAllButton
|
||||||
|
}
|
||||||
|
context.send(viewAction: .acceptRequest(eventID: "1"))
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.bindings.alertInfo?.id == .declineRequest
|
||||||
|
}
|
||||||
|
context.send(viewAction: .declineRequest(eventID: "2"))
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
guard let declineAlertInfo = context.alertInfo else {
|
||||||
|
XCTFail("Can't be nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.shouldDisplayRequests &&
|
||||||
|
state.handledEventIDs == ["1", "2"] &&
|
||||||
|
!state.shouldDisplayEmptyView &&
|
||||||
|
state.displayedRequests.count == 2 &&
|
||||||
|
state.shouldDisplayAcceptAllButton
|
||||||
|
}
|
||||||
|
declineAlertInfo.primaryButton.action?()
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.bindings.alertInfo?.id == .declineAndBan
|
||||||
|
}
|
||||||
|
context.send(viewAction: .ban(eventID: "3"))
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
guard let banAlertInfo = context.alertInfo else {
|
||||||
|
XCTFail("Can't be nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.shouldDisplayRequests &&
|
||||||
|
state.handledEventIDs == ["1", "2", "3"] &&
|
||||||
|
!state.shouldDisplayEmptyView &&
|
||||||
|
state.displayedRequests.count == 1 &&
|
||||||
|
!state.shouldDisplayAcceptAllButton
|
||||||
|
}
|
||||||
|
banAlertInfo.primaryButton.action?()
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAcceptAll() async throws {
|
||||||
|
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")),
|
||||||
|
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org")),
|
||||||
|
KnockRequestProxyMock(.init(eventID: "3", userID: "@charlie:matrix.org")),
|
||||||
|
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
|
||||||
|
joinRule: .knock))
|
||||||
|
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
mediaProvider: MediaProviderMock(),
|
||||||
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|
||||||
|
var deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.shouldDisplayRequests &&
|
||||||
|
state.isKnockableRoom &&
|
||||||
|
state.canAccept &&
|
||||||
|
!state.canBan &&
|
||||||
|
!state.canDecline &&
|
||||||
|
!state.isLoading &&
|
||||||
|
!state.shouldDisplayEmptyView &&
|
||||||
|
state.displayedRequests.count == 4 &&
|
||||||
|
state.handledEventIDs.isEmpty &&
|
||||||
|
state.shouldDisplayAcceptAllButton
|
||||||
|
}
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.bindings.alertInfo?.id == .acceptAllRequests
|
||||||
|
}
|
||||||
|
context.send(viewAction: .acceptAllRequests)
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
guard let alertInfo = context.alertInfo else {
|
||||||
|
XCTFail("Can't be nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
!state.shouldDisplayRequests &&
|
||||||
|
state.handledEventIDs == ["1", "2", "3", "4"] &&
|
||||||
|
!state.isLoading &&
|
||||||
|
state.shouldDisplayEmptyView
|
||||||
|
}
|
||||||
|
alertInfo.primaryButton.action?()
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoadedStateBecomesEmptyIfTheJoinRuleIsNotKnocking() async throws {
|
||||||
|
// If there is a sudden change in the rule, but the requests are still published, we want to hide all of them and show the empty view
|
||||||
|
let roomProxyMock = JoinedRoomProxyMock(.init(members: [.mockAdmin],
|
||||||
|
knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")),
|
||||||
|
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org")),
|
||||||
|
KnockRequestProxyMock(.init(eventID: "3", userID: "@charlie:matrix.org")),
|
||||||
|
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
|
||||||
|
ownUserID: RoomMemberProxyMock.mockAdmin.userID,
|
||||||
|
joinRule: .invite))
|
||||||
|
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
mediaProvider: MediaProviderMock(),
|
||||||
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|
||||||
|
let deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
!state.shouldDisplayRequests &&
|
||||||
|
state.shouldDisplayEmptyView &&
|
||||||
|
!state.isLoading &&
|
||||||
|
!state.isKnockableRoom
|
||||||
|
}
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoadedStateBecomesEmptyIfPermissionsAreRemoved() async throws {
|
||||||
|
// If there is a sudden change in permissions, and the user can't do any other action, we hide all the requests and shoe the empty view
|
||||||
|
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org")),
|
||||||
|
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org")),
|
||||||
|
KnockRequestProxyMock(.init(eventID: "3", userID: "@charlie:matrix.org")),
|
||||||
|
KnockRequestProxyMock(.init(eventID: "4", userID: "@dan:matrix.org"))]),
|
||||||
|
canUserInvite: false,
|
||||||
|
joinRule: .knock))
|
||||||
|
viewModel = KnockRequestsListScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
mediaProvider: MediaProviderMock(),
|
||||||
|
userIndicatorController: UserIndicatorControllerMock())
|
||||||
|
|
||||||
|
let deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
!state.shouldDisplayRequests &&
|
||||||
|
state.shouldDisplayEmptyView &&
|
||||||
|
!state.canAccept &&
|
||||||
|
!state.canBan &&
|
||||||
|
!state.canDecline &&
|
||||||
|
!state.isLoading
|
||||||
|
}
|
||||||
|
try await deferred.fulfill()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ class LoggingTests: XCTestCase {
|
|||||||
let heroName = "Pseudonym"
|
let heroName = "Pseudonym"
|
||||||
let roomSummary = RoomSummary(roomListItem: .init(noPointer: .init()),
|
let roomSummary = RoomSummary(roomListItem: .init(noPointer: .init()),
|
||||||
id: "myroomid",
|
id: "myroomid",
|
||||||
joinRequestType: nil,
|
knockRequestType: nil,
|
||||||
name: roomName,
|
name: roomName,
|
||||||
isDirect: true,
|
isDirect: true,
|
||||||
avatarURL: nil,
|
avatarURL: nil,
|
||||||
|
@ -21,6 +21,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
var cancellables = Set<AnyCancellable>()
|
var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
|
AppSettings.resetAllSettings()
|
||||||
cancellables.removeAll()
|
cancellables.removeAll()
|
||||||
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test"))
|
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test"))
|
||||||
notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
|
notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration())
|
||||||
@ -33,8 +34,6 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||||
appMediator: AppMediatorMock.default,
|
appMediator: AppMediatorMock.default,
|
||||||
appSettings: ServiceLocator.shared.settings)
|
appSettings: ServiceLocator.shared.settings)
|
||||||
|
|
||||||
AppSettings.resetAllSettings()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLeaveRoomTappedWhenPublic() async throws {
|
func testLeaveRoomTappedWhenPublic() async throws {
|
||||||
@ -672,4 +671,99 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
|
|||||||
XCTFail("invalid state")
|
XCTFail("invalid state")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Knock Requests
|
||||||
|
|
||||||
|
func testKnockRequestsCounter() async throws {
|
||||||
|
ServiceLocator.shared.settings.knockingEnabled = true
|
||||||
|
let mockedRequests: [KnockRequestProxyMock] = [.init(), .init()]
|
||||||
|
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, knockRequestsState: .loaded(mockedRequests), joinRule: .knock))
|
||||||
|
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
clientProxy: ClientProxyMock(.init()),
|
||||||
|
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||||
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
|
notificationSettingsProxy: notificationSettingsProxyMock,
|
||||||
|
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||||
|
appMediator: AppMediatorMock.default,
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
|
|
||||||
|
let deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.knockRequestsCount == 2 && state.canSeeKnockingRequests
|
||||||
|
}
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
let deferredAction = deferFulfillment(viewModel.actions) { $0 == .displayKnockingRequests }
|
||||||
|
context.send(viewAction: .processTapRequestsToJoin)
|
||||||
|
try await deferredAction.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKnockRequestsCounterIsLoading() async throws {
|
||||||
|
ServiceLocator.shared.settings.knockingEnabled = true
|
||||||
|
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, knockRequestsState: .loading, joinRule: .knock))
|
||||||
|
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
clientProxy: ClientProxyMock(.init()),
|
||||||
|
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||||
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
|
notificationSettingsProxy: notificationSettingsProxyMock,
|
||||||
|
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||||
|
appMediator: AppMediatorMock.default,
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
|
|
||||||
|
let deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.knockRequestsCount == 0 && state.canSeeKnockingRequests
|
||||||
|
}
|
||||||
|
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKnockRequestsCounterIsNotShownIfNoPermissions() async throws {
|
||||||
|
ServiceLocator.shared.settings.knockingEnabled = true
|
||||||
|
let mockedRequests: [KnockRequestProxyMock] = [.init(), .init()]
|
||||||
|
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, knockRequestsState: .loaded(mockedRequests), canUserInvite: false, joinRule: .knock))
|
||||||
|
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
clientProxy: ClientProxyMock(.init()),
|
||||||
|
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||||
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
|
notificationSettingsProxy: notificationSettingsProxyMock,
|
||||||
|
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||||
|
appMediator: AppMediatorMock.default,
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
|
|
||||||
|
let deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.knockRequestsCount == 2 &&
|
||||||
|
state.dmRecipient == nil &&
|
||||||
|
!state.canSeeKnockingRequests &&
|
||||||
|
!state.canInviteUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKnockRequestsCounterIsNotShownIfDM() async throws {
|
||||||
|
ServiceLocator.shared.settings.knockingEnabled = true
|
||||||
|
let mockedRequests: [KnockRequestProxyMock] = [.init(), .init()]
|
||||||
|
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockAlice]
|
||||||
|
roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: true, isPublic: false, members: mockedMembers, knockRequestsState: .loaded(mockedRequests), joinRule: .knock))
|
||||||
|
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock,
|
||||||
|
clientProxy: ClientProxyMock(.init()),
|
||||||
|
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||||
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||||
|
notificationSettingsProxy: notificationSettingsProxyMock,
|
||||||
|
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||||
|
appMediator: AppMediatorMock.default,
|
||||||
|
appSettings: ServiceLocator.shared.settings)
|
||||||
|
|
||||||
|
let deferred = deferFulfillment(context.$viewState) { state in
|
||||||
|
state.knockRequestsCount == 2 &&
|
||||||
|
!state.canSeeKnockingRequests &&
|
||||||
|
state.dmRecipient != nil &&
|
||||||
|
state.canInviteUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -239,4 +239,116 @@ class RoomScreenViewModelTests: XCTestCase {
|
|||||||
// Then the call button should remain visible shown.
|
// Then the call button should remain visible shown.
|
||||||
XCTAssertTrue(viewModel.state.shouldShowCallButton)
|
XCTAssertTrue(viewModel.state.shouldShowCallButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Knock Requests
|
||||||
|
|
||||||
|
func testKnockRequestBanner() async throws {
|
||||||
|
ServiceLocator.shared.settings.knockingEnabled = true
|
||||||
|
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", reason: "Hello World!")),
|
||||||
|
// This one should be filtered
|
||||||
|
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org", isSeen: true))]),
|
||||||
|
joinRule: .knock))
|
||||||
|
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
|
||||||
|
roomProxy: roomProxyMock,
|
||||||
|
initialSelectedPinnedEventID: nil,
|
||||||
|
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||||
|
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
||||||
|
appMediator: AppMediatorMock.default,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
|
self.viewModel = viewModel
|
||||||
|
|
||||||
|
var deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||||
|
state.shouldSeeKnockRequests &&
|
||||||
|
state.unseenKnockRequests == [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Hello World!", eventID: "1")]
|
||||||
|
}
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
let deferredAction = deferFulfillment(viewModel.actions) { $0 == .displayKnockRequests }
|
||||||
|
viewModel.context.send(viewAction: .viewKnockRequests)
|
||||||
|
try await deferredAction.fulfill()
|
||||||
|
|
||||||
|
deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||||
|
state.handledEventIDs == ["1"] &&
|
||||||
|
!state.shouldSeeKnockRequests
|
||||||
|
}
|
||||||
|
viewModel.context.send(viewAction: .acceptKnock(eventID: "1"))
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKnockRequestBannerMarkAsSeen() async throws {
|
||||||
|
ServiceLocator.shared.settings.knockingEnabled = true
|
||||||
|
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", reason: "Hello World!")),
|
||||||
|
// This one should be filtered
|
||||||
|
KnockRequestProxyMock(.init(eventID: "2", userID: "@bob:matrix.org"))]),
|
||||||
|
joinRule: .knock))
|
||||||
|
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
|
||||||
|
roomProxy: roomProxyMock,
|
||||||
|
initialSelectedPinnedEventID: nil,
|
||||||
|
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||||
|
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
||||||
|
appMediator: AppMediatorMock.default,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
|
self.viewModel = viewModel
|
||||||
|
|
||||||
|
var deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||||
|
state.shouldSeeKnockRequests &&
|
||||||
|
state.unseenKnockRequests == [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Hello World!", eventID: "1"),
|
||||||
|
.init(displayName: nil, avatarURL: nil, userID: "@bob:matrix.org", reason: nil, eventID: "2")]
|
||||||
|
}
|
||||||
|
try await deferred.fulfill()
|
||||||
|
|
||||||
|
deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||||
|
state.handledEventIDs == ["1", "2"] &&
|
||||||
|
!state.shouldSeeKnockRequests
|
||||||
|
}
|
||||||
|
viewModel.context.send(viewAction: .dismissKnockRequests)
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoadingKnockRequests() async throws {
|
||||||
|
ServiceLocator.shared.settings.knockingEnabled = true
|
||||||
|
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loading,
|
||||||
|
joinRule: .knock))
|
||||||
|
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
|
||||||
|
roomProxy: roomProxyMock,
|
||||||
|
initialSelectedPinnedEventID: nil,
|
||||||
|
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||||
|
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
||||||
|
appMediator: AppMediatorMock.default,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
|
self.viewModel = viewModel
|
||||||
|
|
||||||
|
// Loading state just does not appear at all
|
||||||
|
let deferred = deferFulfillment(viewModel.context.$viewState) { !$0.shouldSeeKnockRequests }
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKnockRequestsBannerDoesNotAppearIfUserHasNoPermission() async throws {
|
||||||
|
ServiceLocator.shared.settings.knockingEnabled = true
|
||||||
|
let roomProxyMock = JoinedRoomProxyMock(.init(knockRequestsState: .loaded([KnockRequestProxyMock(.init(eventID: "1", userID: "@alice:matrix.org", displayName: "Alice", reason: "Hello World!"))]),
|
||||||
|
canUserInvite: false,
|
||||||
|
joinRule: .knock))
|
||||||
|
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
|
||||||
|
roomProxy: roomProxyMock,
|
||||||
|
initialSelectedPinnedEventID: nil,
|
||||||
|
mediaProvider: MediaProviderMock(configuration: .init()),
|
||||||
|
ongoingCallRoomIDPublisher: .init(.init(nil)),
|
||||||
|
appMediator: AppMediatorMock.default,
|
||||||
|
appSettings: ServiceLocator.shared.settings,
|
||||||
|
analyticsService: ServiceLocator.shared.analytics,
|
||||||
|
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||||
|
self.viewModel = viewModel
|
||||||
|
|
||||||
|
var deferred = deferFulfillment(viewModel.context.$viewState) { state in
|
||||||
|
state.unseenKnockRequests == [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Hello World!", eventID: "1")] &&
|
||||||
|
!state.shouldSeeKnockRequests
|
||||||
|
}
|
||||||
|
try await deferred.fulfill()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ class RoomSummaryTests: XCTestCase {
|
|||||||
func makeSummary(isDirect: Bool, hasRoomAvatar: Bool) -> RoomSummary {
|
func makeSummary(isDirect: Bool, hasRoomAvatar: Bool) -> RoomSummary {
|
||||||
RoomSummary(roomListItem: .init(noPointer: .init()),
|
RoomSummary(roomListItem: .init(noPointer: .init()),
|
||||||
id: roomDetails.id,
|
id: roomDetails.id,
|
||||||
joinRequestType: nil,
|
knockRequestType: nil,
|
||||||
name: roomDetails.name,
|
name: roomDetails.name,
|
||||||
isDirect: isDirect,
|
isDirect: isDirect,
|
||||||
avatarURL: hasRoomAvatar ? roomDetails.avatarURL : nil,
|
avatarURL: hasRoomAvatar ? roomDetails.avatarURL : nil,
|
||||||
|
@ -61,7 +61,7 @@ packages:
|
|||||||
# Element/Matrix dependencies
|
# Element/Matrix dependencies
|
||||||
MatrixRustSDK:
|
MatrixRustSDK:
|
||||||
url: https://github.com/element-hq/matrix-rust-components-swift
|
url: https://github.com/element-hq/matrix-rust-components-swift
|
||||||
exactVersion: 1.0.80
|
exactVersion: 1.0.81
|
||||||
# path: ../matrix-rust-sdk
|
# path: ../matrix-rust-sdk
|
||||||
Compound:
|
Compound:
|
||||||
url: https://github.com/element-hq/compound-ios
|
url: https://github.com/element-hq/compound-ios
|
||||||
|
Loading…
x
Reference in New Issue
Block a user