mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
Implement the flow for leaving a space. (#4568)
This commit is contained in:
@@ -243,6 +243,10 @@ extension AccessibilityTests {
|
||||
try await performAccessibilityAudit(named: "KnockRequestsListScreen_Previews")
|
||||
}
|
||||
|
||||
func testLeaveSpaceView() async throws {
|
||||
try await performAccessibilityAudit(named: "LeaveSpaceView_Previews")
|
||||
}
|
||||
|
||||
func testLegalInformationScreen() async throws {
|
||||
try await performAccessibilityAudit(named: "LegalInformationScreen_Previews")
|
||||
}
|
||||
|
||||
@@ -1012,6 +1012,7 @@
|
||||
B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; };
|
||||
B6064D82FCDCB829601C1F59 /* SecureBackupLogoutConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FEE10AB666891E6A675E5E /* SecureBackupLogoutConfirmationScreen.swift */; };
|
||||
B6B62437B92B6CA4083AA899 /* SessionDirectories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C2067FF58B4996323EB40C /* SessionDirectories.swift */; };
|
||||
B6CA5D18D702D0919BEF0263 /* LeaveSpaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7813824C547ED121F6F8E0F /* LeaveSpaceView.swift */; };
|
||||
B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; };
|
||||
B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; };
|
||||
B6EC2148FA5443C9289BEEBA /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; };
|
||||
@@ -1206,6 +1207,7 @@
|
||||
DC68E866D6E664B0D2B06E74 /* MockImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */; };
|
||||
DC77E9DB2CFBE84A2BDF20C5 /* RoomRolesAndPermissionsFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0833F51229E166BCA141D004 /* RoomRolesAndPermissionsFlowCoordinator.swift */; };
|
||||
DCFE7CB3B9A104330BBB96AD /* AnalyticsPromptScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B67DF223EEB8DCAF178A1D4 /* AnalyticsPromptScreenCoordinator.swift */; };
|
||||
DD21CE51DF9BD04FC8155972 /* LeaveSpaceHandleSDKMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 580BDCD23DD02481AB5FFB47 /* LeaveSpaceHandleSDKMock.swift */; };
|
||||
DDB47D29C6865669288BF87C /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; };
|
||||
DDE7B4771452300C103B1EB8 /* RoomDirectoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F6210134203BE1F2DD5C679 /* RoomDirectoryCell.swift */; };
|
||||
DDFBDEE1DC32BDD5488F898C /* ClientProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */; };
|
||||
@@ -1308,6 +1310,7 @@
|
||||
EF79B9EFD094C17FBB4942C2 /* UserPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E6DE144D887A254F4CAF203 /* UserPreference.swift */; };
|
||||
EF890DEF0479E66548F2BA23 /* AppLockTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490BEADEFB2D6B7C9F618AE8 /* AppLockTimer.swift */; };
|
||||
EFBBD44C0A16F017C32D2099 /* TimelineItemStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72614BFF35B8394C6E13F55A /* TimelineItemStatusView.swift */; };
|
||||
EFF735EC040BEB669AFBAB50 /* LeaveSpaceHandleProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75B3CE05643C7791D46AC54B /* LeaveSpaceHandleProxy.swift */; };
|
||||
F0570F1ECD70C4C851FB2052 /* SecureBackupRecoveryKeyScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E7304F5ECB4CB11CB10E60 /* SecureBackupRecoveryKeyScreenViewModelProtocol.swift */; };
|
||||
F06CE9132855E81EBB6DDC32 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 940C605265DD82DA0C655E23 /* Kingfisher */; };
|
||||
F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */; };
|
||||
@@ -1961,6 +1964,7 @@
|
||||
57AD14D3ADADE8F6A10F9E88 /* EncryptionSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionSettingsTests.swift; sourceTree = "<group>"; };
|
||||
57EAAF82432B0B53881CF826 /* AudioRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineItem.swift; sourceTree = "<group>"; };
|
||||
57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLocator.swift; sourceTree = "<group>"; };
|
||||
580BDCD23DD02481AB5FFB47 /* LeaveSpaceHandleSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveSpaceHandleSDKMock.swift; sourceTree = "<group>"; };
|
||||
584A61D9C459FAFEF038A7C0 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = "<group>"; };
|
||||
5875F7C0A2398E9F134B1284 /* EncryptionResetScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
@@ -2096,6 +2100,7 @@
|
||||
752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signposter.swift; sourceTree = "<group>"; };
|
||||
753B4C6C0EDDCBF0708DC384 /* TimelineItemSendInfoLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemSendInfoLabel.swift; sourceTree = "<group>"; };
|
||||
75821CD31A4BD02B99C327A4 /* DataProtectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = "<group>"; };
|
||||
75B3CE05643C7791D46AC54B /* LeaveSpaceHandleProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveSpaceHandleProxy.swift; sourceTree = "<group>"; };
|
||||
76310030C831D4610A705603 /* URLComponentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLComponentsTests.swift; sourceTree = "<group>"; };
|
||||
76A46ABD27628CB5FC402541 /* Backports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backports.swift; sourceTree = "<group>"; };
|
||||
7720ACAC6155AB7F9C70B546 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nb; path = nb.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -2609,6 +2614,7 @@
|
||||
D751AA05AD2182BFC4608DE6 /* ur */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ur; path = ur.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
D7673F2B0B038FAB2A8D16AD /* ElementTextFieldStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementTextFieldStyle.swift; sourceTree = "<group>"; };
|
||||
D77F75B3E9F99864048A422A /* DeactivateAccountScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||
D7813824C547ED121F6F8E0F /* LeaveSpaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveSpaceView.swift; sourceTree = "<group>"; };
|
||||
D78C13EF5035879B6131030F /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = "<group>"; };
|
||||
D79BB714D28C9F588DD69353 /* SecureBackupScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
D7B18089ED50324583BB2FB7 /* EditRoomAddressScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
@@ -5503,6 +5509,7 @@
|
||||
AAAB1791344F28CDC62E764D /* Spaces */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
75B3CE05643C7791D46AC54B /* LeaveSpaceHandleProxy.swift */,
|
||||
4B2B564CA6570E1487A7C7CC /* SpaceRoomListProxy.swift */,
|
||||
EACD4855BDAD0799FD86B7B5 /* SpaceRoomListProxyProtocol.swift */,
|
||||
355C8C46DA9C0B45F1B7FC4F /* SpaceRoomProxy.swift */,
|
||||
@@ -6218,6 +6225,7 @@
|
||||
8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */,
|
||||
0A81FD0C60175FA081EB19AD /* EventTimelineItem.swift */,
|
||||
5EFB1D29B0870AFB6A56E9B8 /* IdentityResetHandleSDKMock.swift */,
|
||||
580BDCD23DD02481AB5FFB47 /* LeaveSpaceHandleSDKMock.swift */,
|
||||
);
|
||||
path = SDK;
|
||||
sourceTree = "<group>";
|
||||
@@ -6425,6 +6433,7 @@
|
||||
FA1D480A302295CFC3582543 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7813824C547ED121F6F8E0F /* LeaveSpaceView.swift */,
|
||||
646B50583A2CE6DA67F7739A /* SpaceScreen.swift */,
|
||||
);
|
||||
path = View;
|
||||
@@ -7828,6 +7837,9 @@
|
||||
E468CC731C3F4D678499E52F /* LAContextMock.swift in Sources */,
|
||||
D5681C80D8281560AACE0035 /* Label.swift in Sources */,
|
||||
EEAE954289DE813A61656AE0 /* LayoutDirection.swift in Sources */,
|
||||
EFF735EC040BEB669AFBAB50 /* LeaveSpaceHandleProxy.swift in Sources */,
|
||||
DD21CE51DF9BD04FC8155972 /* LeaveSpaceHandleSDKMock.swift in Sources */,
|
||||
B6CA5D18D702D0919BEF0263 /* LeaveSpaceView.swift in Sources */,
|
||||
42B084FDE621FBEE433AF444 /* LegalInformationScreen.swift in Sources */,
|
||||
9EBDC79CAC9B63A0D626E333 /* LegalInformationScreenCoordinator.swift in Sources */,
|
||||
F40B097470D3110DFDB1FAAA /* LegalInformationScreenModels.swift in Sources */,
|
||||
@@ -9364,7 +9376,7 @@
|
||||
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 25.09.26;
|
||||
version = 25.10.02;
|
||||
};
|
||||
};
|
||||
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
|
||||
|
||||
@@ -150,8 +150,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
|
||||
"state" : {
|
||||
"revision" : "c79dc9374e580b74084535ebe09925764939058b",
|
||||
"version" : "25.9.26"
|
||||
"revision" : "bd2e8c25c5e179b272a0718ac6beca9dcb32f0a3",
|
||||
"version" : "25.10.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
"action_manage_account" = "Manage account";
|
||||
"action_manage_devices" = "Manage devices";
|
||||
"action_message" = "Message";
|
||||
"action_minimize" = "Minimise";
|
||||
"action_next" = "Next";
|
||||
"action_no" = "No";
|
||||
"action_not_now" = "Not now";
|
||||
@@ -149,11 +150,11 @@
|
||||
"action_tap_for_options" = "Tap for options";
|
||||
"action_try_again" = "Try again";
|
||||
"action_unpin" = "Unpin";
|
||||
"action_view" = "View";
|
||||
"action_view_in_timeline" = "View in timeline";
|
||||
"action_view_source" = "View source";
|
||||
"action_yes" = "Yes";
|
||||
"action_yes_try_again" = "Yes, try again";
|
||||
"action.view" = "View";
|
||||
"banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade";
|
||||
"banner_migrate_to_native_sliding_sync_app_force_logout_title" = "%1$@ no longer supports the old protocol. Please log out and log back in to continue using the app.";
|
||||
"banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later.";
|
||||
@@ -219,6 +220,7 @@
|
||||
"common_in_reply_to" = "In reply to %1$@";
|
||||
"common_invite_unknown_profile" = "This Matrix ID can't be found, so the invite might not be received.";
|
||||
"common_leaving_room" = "Leaving room";
|
||||
"common_leaving_space" = "Leaving space";
|
||||
"common_light" = "Light";
|
||||
"common_line_copied_to_clipboard" = "Line copied to clipboard";
|
||||
"common_link_copied_to_clipboard" = "Link copied to clipboard";
|
||||
@@ -565,8 +567,9 @@
|
||||
"screen_knock_requests_list_empty_state_description" = "When somebody will ask to join the room, you’ll be able to see their request here.";
|
||||
"screen_knock_requests_list_empty_state_title" = "No pending request to join";
|
||||
"screen_knock_requests_list_initial_loading_title" = "Loading requests to join…";
|
||||
"screen_leave_space_last_admin_info" = "(Admin)";
|
||||
"screen_leave_space_last_admin_info" = "%1$@ (Admin)";
|
||||
"screen_leave_space_subtitle" = "Select the rooms you’d like to leave which you're not the only administrator for:";
|
||||
"screen_leave_space_subtitle_only_last_admin" = "You will not be removed from the following room(s) because you're the only administrator:";
|
||||
"screen_leave_space_title" = "Leave %1$@?";
|
||||
"screen_media_details_file_format" = "File format";
|
||||
"screen_media_details_filename" = "File name";
|
||||
|
||||
@@ -290,22 +290,6 @@
|
||||
<string>Leave %1$d rooms and space</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>screen_leave_space_subtitle_only_last_admin</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>You will not be removed from the following room because you're the only administrator:</string>
|
||||
<key>other</key>
|
||||
<string>You will not be removed from the following rooms because you're the only administrator:</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>screen_pinned_timeline_screen_title</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
|
||||
@@ -104,6 +104,7 @@ class SpaceExplorerFlowCoordinator: FlowCoordinatorProtocol {
|
||||
return .spaceList(selectedSpaceID: nil)
|
||||
} handler: { [weak self] _ in
|
||||
guard let self else { return }
|
||||
navigationSplitCoordinator.setDetailCoordinator(nil) // If we forget to do this, the tab bar remains hidden.
|
||||
selectedSpaceSubject.send(nil)
|
||||
spaceFlowCoordinator = nil
|
||||
}
|
||||
|
||||
@@ -242,6 +242,8 @@ internal enum L10n {
|
||||
internal static var actionManageDevices: String { return L10n.tr("Localizable", "action_manage_devices") }
|
||||
/// Message
|
||||
internal static var actionMessage: String { return L10n.tr("Localizable", "action_message") }
|
||||
/// Minimise
|
||||
internal static var actionMinimize: String { return L10n.tr("Localizable", "action_minimize") }
|
||||
/// Next
|
||||
internal static var actionNext: String { return L10n.tr("Localizable", "action_next") }
|
||||
/// No
|
||||
@@ -336,6 +338,8 @@ internal enum L10n {
|
||||
internal static var actionTryAgain: String { return L10n.tr("Localizable", "action_try_again") }
|
||||
/// Unpin
|
||||
internal static var actionUnpin: String { return L10n.tr("Localizable", "action_unpin") }
|
||||
/// View
|
||||
internal static var actionView: String { return L10n.tr("Localizable", "action_view") }
|
||||
/// View in timeline
|
||||
internal static var actionViewInTimeline: String { return L10n.tr("Localizable", "action_view_in_timeline") }
|
||||
/// View source
|
||||
@@ -484,6 +488,8 @@ internal enum L10n {
|
||||
internal static var commonInviteUnknownProfile: String { return L10n.tr("Localizable", "common_invite_unknown_profile") }
|
||||
/// Leaving room
|
||||
internal static var commonLeavingRoom: String { return L10n.tr("Localizable", "common_leaving_room") }
|
||||
/// Leaving space
|
||||
internal static var commonLeavingSpace: String { return L10n.tr("Localizable", "common_leaving_space") }
|
||||
/// Light
|
||||
internal static var commonLight: String { return L10n.tr("Localizable", "common_light") }
|
||||
/// Line copied to clipboard
|
||||
@@ -1808,18 +1814,18 @@ internal enum L10n {
|
||||
internal static var screenKnockRequestsListInitialLoadingTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_initial_loading_title") }
|
||||
/// Requests to join
|
||||
internal static var screenKnockRequestsListTitle: String { return L10n.tr("Localizable", "screen_knock_requests_list_title") }
|
||||
/// (Admin)
|
||||
internal static var screenLeaveSpaceLastAdminInfo: String { return L10n.tr("Localizable", "screen_leave_space_last_admin_info") }
|
||||
/// %1$@ (Admin)
|
||||
internal static func screenLeaveSpaceLastAdminInfo(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_leave_space_last_admin_info", String(describing: p1))
|
||||
}
|
||||
/// Plural format key: "%#@COUNT@"
|
||||
internal static func screenLeaveSpaceSubmit(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "screen_leave_space_submit", p1)
|
||||
}
|
||||
/// Select the rooms you’d like to leave which you're not the only administrator for:
|
||||
internal static var screenLeaveSpaceSubtitle: String { return L10n.tr("Localizable", "screen_leave_space_subtitle") }
|
||||
/// Plural format key: "%#@COUNT@"
|
||||
internal static func screenLeaveSpaceSubtitleOnlyLastAdmin(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "screen_leave_space_subtitle_only_last_admin", p1)
|
||||
}
|
||||
/// You will not be removed from the following room(s) because you're the only administrator:
|
||||
internal static var screenLeaveSpaceSubtitleOnlyLastAdmin: String { return L10n.tr("Localizable", "screen_leave_space_subtitle_only_last_admin") }
|
||||
/// Leave %1$@?
|
||||
internal static func screenLeaveSpaceTitle(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_leave_space_title", String(describing: p1))
|
||||
@@ -3340,11 +3346,6 @@ internal enum L10n {
|
||||
internal static var yourAvatar: String { return L10n.tr("Localizable", "a11y.your_avatar") }
|
||||
}
|
||||
|
||||
internal enum Action {
|
||||
/// View
|
||||
internal static var view: String { return L10n.tr("Localizable", "action.view") }
|
||||
}
|
||||
|
||||
internal enum Common {
|
||||
/// Add an account
|
||||
internal static var addAccount: String { return L10n.tr("Localizable", "common.add_account") }
|
||||
|
||||
@@ -16111,6 +16111,76 @@ class SpaceServiceProxyMock: SpaceServiceProxyProtocol, @unchecked Sendable {
|
||||
return spaceRoomListSpaceIDParentReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - leaveSpace
|
||||
|
||||
var leaveSpaceSpaceIDUnderlyingCallsCount = 0
|
||||
var leaveSpaceSpaceIDCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return leaveSpaceSpaceIDUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = leaveSpaceSpaceIDUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
leaveSpaceSpaceIDUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
leaveSpaceSpaceIDUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var leaveSpaceSpaceIDCalled: Bool {
|
||||
return leaveSpaceSpaceIDCallsCount > 0
|
||||
}
|
||||
var leaveSpaceSpaceIDReceivedSpaceID: String?
|
||||
var leaveSpaceSpaceIDReceivedInvocations: [String] = []
|
||||
|
||||
var leaveSpaceSpaceIDUnderlyingReturnValue: Result<LeaveSpaceHandleProxy, SpaceServiceProxyError>!
|
||||
var leaveSpaceSpaceIDReturnValue: Result<LeaveSpaceHandleProxy, SpaceServiceProxyError>! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return leaveSpaceSpaceIDUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: Result<LeaveSpaceHandleProxy, SpaceServiceProxyError>? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = leaveSpaceSpaceIDUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
leaveSpaceSpaceIDUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
leaveSpaceSpaceIDUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var leaveSpaceSpaceIDClosure: ((String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError>)?
|
||||
|
||||
func leaveSpace(spaceID: String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError> {
|
||||
leaveSpaceSpaceIDCallsCount += 1
|
||||
leaveSpaceSpaceIDReceivedSpaceID = spaceID
|
||||
DispatchQueue.main.async {
|
||||
self.leaveSpaceSpaceIDReceivedInvocations.append(spaceID)
|
||||
}
|
||||
if let leaveSpaceSpaceIDClosure = leaveSpaceSpaceIDClosure {
|
||||
return await leaveSpaceSpaceIDClosure(spaceID)
|
||||
} else {
|
||||
return leaveSpaceSpaceIDReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
class StaticRoomSummaryProviderMock: StaticRoomSummaryProviderProtocol, @unchecked Sendable {
|
||||
var roomListPublisher: CurrentValuePublisher<[RoomSummary], Never> {
|
||||
|
||||
@@ -10135,6 +10135,128 @@ open class LazyTimelineItemProviderSDKMock: MatrixRustSDK.LazyTimelineItemProvid
|
||||
}
|
||||
}
|
||||
}
|
||||
open class LeaveSpaceHandleSDKMock: MatrixRustSDK.LeaveSpaceHandle, @unchecked Sendable {
|
||||
init() {
|
||||
super.init(noPointer: .init())
|
||||
}
|
||||
|
||||
public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
|
||||
fatalError("init(unsafeFromRawPointer:) has not been implemented")
|
||||
}
|
||||
|
||||
fileprivate var pointer: UnsafeMutableRawPointer!
|
||||
|
||||
//MARK: - leave
|
||||
|
||||
open var leaveRoomIdsThrowableError: Error?
|
||||
var leaveRoomIdsUnderlyingCallsCount = 0
|
||||
open var leaveRoomIdsCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return leaveRoomIdsUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = leaveRoomIdsUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
leaveRoomIdsUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
leaveRoomIdsUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var leaveRoomIdsCalled: Bool {
|
||||
return leaveRoomIdsCallsCount > 0
|
||||
}
|
||||
open var leaveRoomIdsReceivedRoomIds: [String]?
|
||||
open var leaveRoomIdsReceivedInvocations: [[String]] = []
|
||||
open var leaveRoomIdsClosure: (([String]) async throws -> Void)?
|
||||
|
||||
open override func leave(roomIds: [String]) async throws {
|
||||
if let error = leaveRoomIdsThrowableError {
|
||||
throw error
|
||||
}
|
||||
leaveRoomIdsCallsCount += 1
|
||||
leaveRoomIdsReceivedRoomIds = roomIds
|
||||
DispatchQueue.main.async {
|
||||
self.leaveRoomIdsReceivedInvocations.append(roomIds)
|
||||
}
|
||||
try await leaveRoomIdsClosure?(roomIds)
|
||||
}
|
||||
|
||||
//MARK: - rooms
|
||||
|
||||
var roomsUnderlyingCallsCount = 0
|
||||
open var roomsCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return roomsUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = roomsUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
roomsUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
roomsUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var roomsCalled: Bool {
|
||||
return roomsCallsCount > 0
|
||||
}
|
||||
|
||||
var roomsUnderlyingReturnValue: [LeaveSpaceRoom]!
|
||||
open var roomsReturnValue: [LeaveSpaceRoom]! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return roomsUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: [LeaveSpaceRoom]? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = roomsUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
roomsUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
roomsUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var roomsClosure: (() -> [LeaveSpaceRoom])?
|
||||
|
||||
open override func rooms() -> [LeaveSpaceRoom] {
|
||||
roomsCallsCount += 1
|
||||
if let roomsClosure = roomsClosure {
|
||||
return roomsClosure()
|
||||
} else {
|
||||
return roomsReturnValue
|
||||
}
|
||||
}
|
||||
}
|
||||
open class MediaFileHandleSDKMock: MatrixRustSDK.MediaFileHandle, @unchecked Sendable {
|
||||
init() {
|
||||
super.init(noPointer: .init())
|
||||
@@ -22227,6 +22349,81 @@ open class SpaceServiceSDKMock: MatrixRustSDK.SpaceService, @unchecked Sendable
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - leaveSpace
|
||||
|
||||
open var leaveSpaceSpaceIdThrowableError: Error?
|
||||
var leaveSpaceSpaceIdUnderlyingCallsCount = 0
|
||||
open var leaveSpaceSpaceIdCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return leaveSpaceSpaceIdUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = leaveSpaceSpaceIdUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
leaveSpaceSpaceIdUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
leaveSpaceSpaceIdUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var leaveSpaceSpaceIdCalled: Bool {
|
||||
return leaveSpaceSpaceIdCallsCount > 0
|
||||
}
|
||||
open var leaveSpaceSpaceIdReceivedSpaceId: String?
|
||||
open var leaveSpaceSpaceIdReceivedInvocations: [String] = []
|
||||
|
||||
var leaveSpaceSpaceIdUnderlyingReturnValue: LeaveSpaceHandle!
|
||||
open var leaveSpaceSpaceIdReturnValue: LeaveSpaceHandle! {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return leaveSpaceSpaceIdUnderlyingReturnValue
|
||||
} else {
|
||||
var returnValue: LeaveSpaceHandle? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = leaveSpaceSpaceIdUnderlyingReturnValue
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
leaveSpaceSpaceIdUnderlyingReturnValue = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
leaveSpaceSpaceIdUnderlyingReturnValue = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
open var leaveSpaceSpaceIdClosure: ((String) async throws -> LeaveSpaceHandle)?
|
||||
|
||||
open override func leaveSpace(spaceId: String) async throws -> LeaveSpaceHandle {
|
||||
if let error = leaveSpaceSpaceIdThrowableError {
|
||||
throw error
|
||||
}
|
||||
leaveSpaceSpaceIdCallsCount += 1
|
||||
leaveSpaceSpaceIdReceivedSpaceId = spaceId
|
||||
DispatchQueue.main.async {
|
||||
self.leaveSpaceSpaceIdReceivedInvocations.append(spaceId)
|
||||
}
|
||||
if let leaveSpaceSpaceIdClosure = leaveSpaceSpaceIdClosure {
|
||||
return try await leaveSpaceSpaceIdClosure(spaceId)
|
||||
} else {
|
||||
return leaveSpaceSpaceIdReturnValue
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - spaceRoomList
|
||||
|
||||
open var spaceRoomListSpaceIdThrowableError: Error?
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
extension LeaveSpaceHandleSDKMock {
|
||||
struct Configuration {
|
||||
var rooms: [LeaveSpaceRoom] = .mockRooms
|
||||
}
|
||||
|
||||
convenience init(_ configuration: Configuration) {
|
||||
self.init()
|
||||
|
||||
roomsClosure = { configuration.rooms }
|
||||
}
|
||||
}
|
||||
|
||||
extension [LeaveSpaceRoom] {
|
||||
static var mockAdminRooms: [LeaveSpaceRoom] {
|
||||
mockRooms.filter(\.isLastAdmin)
|
||||
}
|
||||
|
||||
static var mockRooms: [LeaveSpaceRoom] {
|
||||
[
|
||||
LeaveSpaceRoom(spaceRoom: SpaceRoom(id: "1",
|
||||
name: "Lighting",
|
||||
avatarURL: .mockMXCAvatar,
|
||||
isSpace: false,
|
||||
memberCount: 10,
|
||||
joinRule: .public),
|
||||
isLastAdmin: false),
|
||||
LeaveSpaceRoom(spaceRoom: SpaceRoom(id: "2",
|
||||
name: "Sound",
|
||||
isSpace: false,
|
||||
memberCount: 20,
|
||||
joinRule: .private),
|
||||
isLastAdmin: false),
|
||||
LeaveSpaceRoom(spaceRoom: SpaceRoom(id: "3",
|
||||
name: "Set & Costume",
|
||||
isSpace: false,
|
||||
memberCount: 25,
|
||||
joinRule: .restricted(rules: [])),
|
||||
isLastAdmin: false),
|
||||
LeaveSpaceRoom(spaceRoom: SpaceRoom(id: "4",
|
||||
name: "The Theatre",
|
||||
isSpace: true,
|
||||
memberCount: 100,
|
||||
joinRule: .private,
|
||||
childrenCount: 20),
|
||||
isLastAdmin: false),
|
||||
LeaveSpaceRoom(spaceRoom: SpaceRoom(id: "5",
|
||||
name: "Bookings",
|
||||
isSpace: false,
|
||||
memberCount: 200,
|
||||
joinRule: .private,
|
||||
childrenCount: 0),
|
||||
isLastAdmin: true),
|
||||
LeaveSpaceRoom(spaceRoom: SpaceRoom(id: "6",
|
||||
name: "Events",
|
||||
isSpace: false,
|
||||
memberCount: 65,
|
||||
joinRule: .restricted(rules: []),
|
||||
childrenCount: 0),
|
||||
isLastAdmin: true),
|
||||
LeaveSpaceRoom(spaceRoom: SpaceRoom(id: "7",
|
||||
name: "Mario Kart",
|
||||
isSpace: false,
|
||||
memberCount: 123,
|
||||
joinRule: .public),
|
||||
isLastAdmin: false),
|
||||
LeaveSpaceRoom(spaceRoom: SpaceRoom(id: "8",
|
||||
name: "Tetris",
|
||||
isSpace: false,
|
||||
memberCount: 95,
|
||||
joinRule: .public),
|
||||
isLastAdmin: false),
|
||||
LeaveSpaceRoom(spaceRoom: SpaceRoom(id: "9",
|
||||
name: "Minecraft",
|
||||
isSpace: false,
|
||||
memberCount: 39,
|
||||
joinRule: .public),
|
||||
isLastAdmin: false),
|
||||
LeaveSpaceRoom(spaceRoom: SpaceRoom(id: "10",
|
||||
name: "Lemmings",
|
||||
isSpace: false,
|
||||
memberCount: 67,
|
||||
joinRule: .public),
|
||||
isLastAdmin: true),
|
||||
LeaveSpaceRoom(spaceRoom: SpaceRoom(id: "11",
|
||||
name: "Rayman",
|
||||
isSpace: false,
|
||||
memberCount: 23,
|
||||
joinRule: .public),
|
||||
isLastAdmin: false),
|
||||
LeaveSpaceRoom(spaceRoom: SpaceRoom(id: "12",
|
||||
name: "Gaming",
|
||||
avatarURL: .mockMXCAvatar,
|
||||
isSpace: true,
|
||||
memberCount: 835,
|
||||
joinRule: .public,
|
||||
childrenCount: 15),
|
||||
isLastAdmin: true)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private extension SpaceRoom {
|
||||
init(id: String,
|
||||
canonicalAlias: String? = nil,
|
||||
name: String,
|
||||
topic: String? = nil,
|
||||
avatarURL: URL? = nil,
|
||||
isSpace: Bool,
|
||||
memberCount: UInt64,
|
||||
joinRule: JoinRule? = .public,
|
||||
isDirect: Bool? = false,
|
||||
childrenCount: UInt64 = 0,
|
||||
membership: Membership? = .joined,
|
||||
heroes: [RoomHero]? = [],
|
||||
via: [String] = []) {
|
||||
self.init(roomId: id,
|
||||
canonicalAlias: canonicalAlias,
|
||||
name: name,
|
||||
topic: topic,
|
||||
avatarUrl: avatarURL?.absoluteString,
|
||||
roomType: isSpace ? .space : .room,
|
||||
numJoinedMembers: memberCount,
|
||||
joinRule: joinRule,
|
||||
worldReadable: true,
|
||||
guestCanJoin: false,
|
||||
isDirect: isDirect,
|
||||
childrenCount: childrenCount,
|
||||
state: membership,
|
||||
heroes: heroes,
|
||||
via: via)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ extension SpaceServiceProxyMock {
|
||||
struct Configuration {
|
||||
var joinedSpaces: [SpaceRoomProxyProtocol] = []
|
||||
var spaceRoomLists: [String: SpaceRoomListProxyMock] = [:]
|
||||
var leaveSpaceRooms: [LeaveSpaceRoom] = []
|
||||
}
|
||||
|
||||
convenience init(_ configuration: Configuration) {
|
||||
@@ -26,6 +27,10 @@ extension SpaceServiceProxyMock {
|
||||
.failure(.sdkError(ClientProxyMockError.generic))
|
||||
}
|
||||
}
|
||||
leaveSpaceSpaceIDClosure = { spaceID in
|
||||
.success(LeaveSpaceHandleProxy(spaceID: spaceID,
|
||||
leaveHandle: LeaveSpaceHandleSDKMock(.init(rooms: configuration.leaveSpaceRooms))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ enum RoomAvatarSizeOnScreen {
|
||||
case chats
|
||||
case spaces
|
||||
case timeline
|
||||
case leaveSpace
|
||||
case messageForwarding
|
||||
case globalSearch
|
||||
case roomSelection
|
||||
@@ -148,7 +149,7 @@ enum RoomAvatarSizeOnScreen {
|
||||
switch self {
|
||||
case .chats, .spaces:
|
||||
return 52
|
||||
case .timeline:
|
||||
case .timeline, .leaveSpace:
|
||||
return 32
|
||||
case .notificationSettings:
|
||||
return 30
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
@@ -55,6 +56,18 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func backportSafeAreaBar(edge: VerticalEdge,
|
||||
alignment: HorizontalAlignment = .center,
|
||||
spacing: CGFloat? = nil,
|
||||
content: () -> some View) -> some View {
|
||||
if #available(iOS 26.0, *) {
|
||||
safeAreaBar(edge: edge, alignment: alignment, spacing: spacing, content: content)
|
||||
} else {
|
||||
safeAreaInset(edge: edge, alignment: alignment, spacing: spacing) { content().background(Color.compound.bgCanvasDefault.ignoresSafeArea()) }
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func backportScrollEdgeEffectHidden() -> some View {
|
||||
if #available(iOS 26, *) {
|
||||
scrollEdgeEffectHidden()
|
||||
|
||||
@@ -68,6 +68,7 @@ enum TestablePreviewsDictionary {
|
||||
"KnockRequestsBannerView_Previews" : KnockRequestsBannerView_Previews.self,
|
||||
"KnockRequestsListEmptyStateView_Previews" : KnockRequestsListEmptyStateView_Previews.self,
|
||||
"KnockRequestsListScreen_Previews" : KnockRequestsListScreen_Previews.self,
|
||||
"LeaveSpaceView_Previews" : LeaveSpaceView_Previews.self,
|
||||
"LegalInformationScreen_Previews" : LegalInformationScreen_Previews.self,
|
||||
"LoadableImage_Previews" : LoadableImage_Previews.self,
|
||||
"LocationMarkerView_Previews" : LocationMarkerView_Previews.self,
|
||||
|
||||
@@ -29,9 +29,14 @@ struct SpaceScreenViewState: BindableState {
|
||||
var spaceName: String { space.name ?? L10n.commonSpace }
|
||||
}
|
||||
|
||||
struct SpaceScreenViewStateBindings { }
|
||||
struct SpaceScreenViewStateBindings {
|
||||
var leaveHandle: LeaveSpaceHandleProxy?
|
||||
}
|
||||
|
||||
enum SpaceScreenViewAction {
|
||||
case spaceAction(SpaceRoomCell.Action)
|
||||
case leaveSpace
|
||||
case deselectAllLeaveRoomDetails
|
||||
case toggleLeaveSpaceRoomDetails(id: String)
|
||||
case confirmLeaveSpace
|
||||
}
|
||||
|
||||
@@ -89,14 +89,21 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
||||
case .spaceAction(.join(let spaceRoomProxy)):
|
||||
Task { await join(spaceRoomProxy) }
|
||||
case .leaveSpace:
|
||||
#if DEBUG
|
||||
Task { // Temporary implementation to make joining a space easier to test.
|
||||
if case let .joined(roomProxy) = await clientProxy.roomForIdentifier(spaceRoomListProxy.spaceRoomProxy.id),
|
||||
case .success = await roomProxy.leaveRoom() {
|
||||
actionsSubject.send(.leftSpace)
|
||||
}
|
||||
Task { await showLeaveSpaceConfirmation() }
|
||||
case .deselectAllLeaveRoomDetails:
|
||||
guard let leaveHandle = state.bindings.leaveHandle else { fatalError("The leave handle should be available.") }
|
||||
for room in leaveHandle.rooms {
|
||||
room.isSelected = false
|
||||
}
|
||||
#endif
|
||||
case .toggleLeaveSpaceRoomDetails(let spaceRoomID):
|
||||
guard let room = state.bindings.leaveHandle?.rooms.first(where: { $0.spaceRoomProxy.id == spaceRoomID }) else {
|
||||
fatalError("The space room to toggle is not in the list of rooms to leave.")
|
||||
}
|
||||
withTransaction(\.disablesAnimations, true) { // The button is adding an unwanted animation.
|
||||
room.isSelected.toggle()
|
||||
}
|
||||
case .confirmLeaveSpace:
|
||||
Task { await confirmLeaveSpace() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,10 +143,45 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
||||
}
|
||||
}
|
||||
|
||||
private func showLeaveSpaceConfirmation() async {
|
||||
guard case let .success(leaveHandle) = await spaceServiceProxy.leaveSpace(spaceID: spaceRoomListProxy.spaceRoomProxy.id) else {
|
||||
showFailureIndicator()
|
||||
return
|
||||
}
|
||||
|
||||
state.bindings.leaveHandle = leaveHandle
|
||||
}
|
||||
|
||||
private func confirmLeaveSpace() async {
|
||||
guard let leaveHandle = state.bindings.leaveHandle else { fatalError("Leaving without a handle is impossible.") }
|
||||
|
||||
showLeavingIndicator()
|
||||
defer { hideLeavingIndicator() }
|
||||
|
||||
switch await leaveHandle.leave() {
|
||||
case .success:
|
||||
state.bindings.leaveHandle = nil
|
||||
actionsSubject.send(.leftSpace)
|
||||
case .failure:
|
||||
showFailureIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Indicators
|
||||
|
||||
private static var leavingIndicatorID: String { "\(Self.self)-Leaving" }
|
||||
private static var failureIndicatorID: String { "\(Self.self)-Failure" }
|
||||
|
||||
private func showLeavingIndicator() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: Self.leavingIndicatorID,
|
||||
type: .modal(progress: .indeterminate, interactiveDismissDisabled: true, allowsInteraction: false),
|
||||
title: L10n.commonLeavingSpace))
|
||||
}
|
||||
|
||||
private func hideLeavingIndicator() {
|
||||
userIndicatorController.retractIndicatorWithId(Self.leavingIndicatorID)
|
||||
}
|
||||
|
||||
private func showFailureIndicator() {
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: Self.failureIndicatorID,
|
||||
type: .toast,
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
//
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct LeaveSpaceView: View {
|
||||
let context: SpaceScreenViewModel.Context
|
||||
|
||||
@State private var scrollViewHeight: CGFloat = .zero
|
||||
@State private var buttonsHeight: CGFloat = .zero
|
||||
private let topPadding = 19.0
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
rooms
|
||||
}
|
||||
.readHeight($scrollViewHeight)
|
||||
}
|
||||
.backportSafeAreaBar(edge: .bottom, spacing: 0) {
|
||||
buttons
|
||||
.readHeight($buttonsHeight)
|
||||
}
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.padding(.top, topPadding) // For the drag indicator
|
||||
.presentationDetents([.height(scrollViewHeight + buttonsHeight + topPadding)])
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationBackground(.compound.bgCanvasDefault)
|
||||
}
|
||||
|
||||
var header: some View {
|
||||
VStack(spacing: 16) {
|
||||
BigIcon(icon: \.errorSolid, style: .alertSolid)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text(L10n.screenLeaveSpaceTitle(context.viewState.spaceName))
|
||||
.font(.compound.headingMDBold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
switch context.leaveHandle?.mode {
|
||||
case .manyRooms:
|
||||
Text(L10n.screenLeaveSpaceSubtitle)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
case .onlyAdminRooms:
|
||||
Text(L10n.screenLeaveSpaceSubtitleOnlyLastAdmin)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
case .noRooms, nil:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var rooms: some View {
|
||||
if let leaveRooms = context.leaveHandle?.rooms, !leaveRooms.isEmpty {
|
||||
LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
|
||||
Section {
|
||||
ForEach(leaveRooms, id: \.spaceRoomProxy.id) { room in
|
||||
LeaveSpaceRoomDetailsCell(room: room, mediaProvider: context.mediaProvider) {
|
||||
context.send(viewAction: .toggleLeaveSpaceRoomDetails(id: room.spaceRoomProxy.id))
|
||||
}
|
||||
.disabled(room.isLastAdmin)
|
||||
}
|
||||
} header: {
|
||||
Button(L10n.commonDeselectAll) {
|
||||
context.send(viewAction: .deselectAllLeaveRoomDetails)
|
||||
}
|
||||
.buttonStyle(.compound(.textLink, size: .small))
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 8)
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var buttons: some View {
|
||||
VStack(spacing: 16) {
|
||||
Button(role: .destructive) {
|
||||
context.send(viewAction: .confirmLeaveSpace)
|
||||
} label: {
|
||||
Label(context.leaveHandle?.confirmationTitle ?? L10n.actionLeaveSpace, icon: \.leave)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
|
||||
Button(L10n.actionCancel) {
|
||||
context.leaveHandle = nil
|
||||
}
|
||||
.buttonStyle(.compound(.tertiary))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 16)
|
||||
}
|
||||
}
|
||||
|
||||
struct LeaveSpaceRoomDetailsCell: View {
|
||||
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
|
||||
|
||||
let room: LeaveSpaceRoomDetails
|
||||
let mediaProvider: MediaProviderProtocol?
|
||||
|
||||
let action: () -> Void
|
||||
|
||||
private var subtitle: String? {
|
||||
guard !room.spaceRoomProxy.isSpace else { return nil }
|
||||
let memberCount = L10n.commonMemberCount(room.spaceRoomProxy.joinedMembersCount)
|
||||
return room.isLastAdmin ? L10n.screenLeaveSpaceLastAdminInfo(memberCount) : memberCount
|
||||
}
|
||||
|
||||
var visibilityIcon: KeyPath<CompoundIcons, Image>? {
|
||||
switch room.spaceRoomProxy.visibility {
|
||||
case .public: \.public
|
||||
case .private: \.lockSolid
|
||||
case .restricted: nil
|
||||
case .none: \.lockSolid
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 16) {
|
||||
if dynamicTypeSize < .accessibility3 {
|
||||
RoomAvatarImage(avatar: room.spaceRoomProxy.avatar,
|
||||
avatarSize: .room(on: .leaveSpace),
|
||||
mediaProvider: mediaProvider)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(room.spaceRoomProxy.computedName)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.lineLimit(1)
|
||||
.padding(.vertical, 1)
|
||||
.padding(.vertical, subtitle == nil ? 10 : 0)
|
||||
|
||||
subtitleLabel
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.vertical, 8)
|
||||
|
||||
ListRowAccessory.multiSelection(room.isSelected)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.buttonStyle(SpaceRoomCellButtonStyle(isSelected: false))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var subtitleLabel: some View {
|
||||
if let subtitle {
|
||||
Label {
|
||||
Text(subtitle)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.lineLimit(1)
|
||||
.padding(.vertical, 1)
|
||||
} icon: {
|
||||
if let visibilityIcon {
|
||||
CompoundIcon(visibilityIcon,
|
||||
size: .xSmall,
|
||||
relativeTo: .compound.bodyMD)
|
||||
.foregroundStyle(.compound.iconTertiary)
|
||||
}
|
||||
}
|
||||
.labelStyle(.custom(spacing: 4))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = makeViewModel(mode: .manyRooms)
|
||||
static let onlyAdminRoomsViewModel = makeViewModel(mode: .onlyAdminRooms)
|
||||
static let noRoomsViewModel = makeViewModel(mode: .noRooms)
|
||||
|
||||
static var previews: some View {
|
||||
LeaveSpaceView(context: viewModel.context)
|
||||
.previewDisplayName("Many Rooms")
|
||||
.snapshotPreferences(expect: viewModel.context.observe(\.leaveHandle).map { $0 != nil }.eraseToStream())
|
||||
LeaveSpaceView(context: onlyAdminRoomsViewModel.context)
|
||||
.previewDisplayName("Only Admin Rooms")
|
||||
.snapshotPreferences(expect: viewModel.context.observe(\.leaveHandle).map { $0 != nil }.eraseToStream())
|
||||
LeaveSpaceView(context: noRoomsViewModel.context)
|
||||
.previewDisplayName("No Rooms")
|
||||
.snapshotPreferences(expect: viewModel.context.observe(\.leaveHandle).map { $0 != nil }.eraseToStream())
|
||||
}
|
||||
|
||||
static func makeViewModel(mode: LeaveSpaceHandleProxy.Mode) -> SpaceScreenViewModel {
|
||||
let spaceRoomProxy = SpaceRoomProxyMock(.init(id: "!eng-space:matrix.org",
|
||||
name: "Engineering Team",
|
||||
isSpace: true,
|
||||
parent: SpaceRoomProxyMock(.init(name: "MegaGroup", isSpace: true)),
|
||||
childrenCount: 30,
|
||||
joinedMembersCount: 76,
|
||||
heroes: [.mockDan, .mockBob, .mockCharlie, .mockVerbose],
|
||||
topic: "Description of the space goes right here. Lorem ipsum dolor sit amet consectetur. Leo viverra morbi habitant in.",
|
||||
joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")])))
|
||||
let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceRoomProxy: spaceRoomProxy,
|
||||
initialSpaceRooms: .mockSpaceList))
|
||||
|
||||
let rooms: [LeaveSpaceRoom] = switch mode {
|
||||
case .manyRooms: .mockRooms
|
||||
case .onlyAdminRooms: .mockAdminRooms
|
||||
case .noRooms: []
|
||||
}
|
||||
let spaceServiceProxy = SpaceServiceProxyMock(.init(leaveSpaceRooms: rooms))
|
||||
|
||||
let viewModel = SpaceScreenViewModel(spaceRoomListProxy: spaceRoomListProxy,
|
||||
spaceServiceProxy: spaceServiceProxy,
|
||||
selectedSpaceRoomPublisher: .init(nil),
|
||||
userSession: UserSessionMock(.init()),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
viewModel.context.send(viewAction: .leaveSpace)
|
||||
return viewModel
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,9 @@ struct SpaceScreen: View {
|
||||
.navigationTitle(context.viewState.spaceName)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
.sheet(item: $context.leaveHandle) { _ in
|
||||
LeaveSpaceView(context: context)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
class LeaveSpaceHandleProxy: Identifiable {
|
||||
let id: String
|
||||
let leaveHandle: LeaveSpaceHandleProtocol
|
||||
var rooms: [LeaveSpaceRoomDetails]
|
||||
|
||||
enum Mode { case manyRooms, onlyAdminRooms, noRooms }
|
||||
let mode: Mode
|
||||
|
||||
init(spaceID: String, leaveHandle: LeaveSpaceHandleProtocol) {
|
||||
id = spaceID
|
||||
self.leaveHandle = leaveHandle
|
||||
|
||||
rooms = leaveHandle.rooms()
|
||||
.compactMap { room in
|
||||
guard room.spaceRoom.state == .joined, // The SDK is going to do this but not yet.
|
||||
room.spaceRoom.isDirect != true,
|
||||
room.spaceRoom.roomId != spaceID else {
|
||||
return nil
|
||||
}
|
||||
return .init(spaceRoomProxy: SpaceRoomProxy(spaceRoom: room.spaceRoom, parent: nil),
|
||||
isLastAdmin: room.isLastAdmin,
|
||||
isSelected: !room.isLastAdmin)
|
||||
}
|
||||
|
||||
mode = if rooms.isEmpty {
|
||||
.noRooms
|
||||
} else if rooms.count(where: { !$0.isLastAdmin }) == 0 {
|
||||
.onlyAdminRooms
|
||||
} else {
|
||||
.manyRooms
|
||||
}
|
||||
}
|
||||
|
||||
func leave() async -> Result<Void, SpaceServiceProxyError> {
|
||||
let selectedRoomIDs = rooms.filter(\.isSelected).map(\.spaceRoomProxy.id)
|
||||
|
||||
do {
|
||||
return try await .success(leaveHandle.leave(roomIds: selectedRoomIDs + [id]))
|
||||
} catch {
|
||||
MXLog.error("Failed leaving space \(id): \(error)")
|
||||
rooms = rooms.filter { leaveRoom in
|
||||
leaveHandle.rooms().contains { $0.spaceRoom.roomId == leaveRoom.spaceRoomProxy.id }
|
||||
}
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
var selectedCount: Int { rooms.count { $0.isSelected } }
|
||||
|
||||
var confirmationTitle: String {
|
||||
let selectedCount = selectedCount
|
||||
return selectedCount > 0 ? L10n.screenLeaveSpaceSubmit(selectedCount) : L10n.actionLeaveSpace
|
||||
}
|
||||
}
|
||||
|
||||
@Observable class LeaveSpaceRoomDetails {
|
||||
let spaceRoomProxy: SpaceRoomProxyProtocol
|
||||
let isLastAdmin: Bool
|
||||
var isSelected: Bool
|
||||
|
||||
init(spaceRoomProxy: SpaceRoomProxyProtocol, isLastAdmin: Bool, isSelected: Bool) {
|
||||
self.spaceRoomProxy = spaceRoomProxy
|
||||
self.isLastAdmin = isLastAdmin
|
||||
self.isSelected = isSelected
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,15 @@ class SpaceServiceProxy: SpaceServiceProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func leaveSpace(spaceID: String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError> {
|
||||
do {
|
||||
return try await .success(.init(spaceID: spaceID, leaveHandle: spaceService.leaveSpace(spaceId: spaceID)))
|
||||
} catch {
|
||||
MXLog.error("Failed to get leave handle for \(spaceID): \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func handleUpdates(_ updates: [SpaceListUpdate]) {
|
||||
|
||||
@@ -17,4 +17,5 @@ protocol SpaceServiceProxyProtocol {
|
||||
var joinedSpacesPublisher: CurrentValuePublisher<[SpaceRoomProxyProtocol], Never> { get }
|
||||
|
||||
func spaceRoomList(spaceID: String, parent: SpaceRoomProxyProtocol?) async -> Result<SpaceRoomListProxyProtocol, SpaceServiceProxyError>
|
||||
func leaveSpace(spaceID: String) async -> Result<LeaveSpaceHandleProxy, SpaceServiceProxyError>
|
||||
}
|
||||
|
||||
@@ -365,6 +365,12 @@ extension PreviewTests {
|
||||
}
|
||||
}
|
||||
|
||||
func testLeaveSpaceView() async throws {
|
||||
for (index, preview) in LeaveSpaceView_Previews._allPreviews.enumerated() {
|
||||
try await assertSnapshots(matching: preview, step: index)
|
||||
}
|
||||
}
|
||||
|
||||
func testLegalInformationScreen() async throws {
|
||||
for (index, preview) in LegalInformationScreen_Previews._allPreviews.enumerated() {
|
||||
try await assertSnapshots(matching: preview, step: index)
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b8919d537dacbfa092cf8bb0bd088d3d9ce1c2acef978675e9d7a1ec9aae8ad1
|
||||
size 75096
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:43b9a0739c68efb744640f6a23ebb59aadd4bcc8c5244fa3c47cab7ab427949b
|
||||
size 75068
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7f62ab49653a17483a0b0ec3237c8aac8af7d7cf50a9f9b6e5def24a63e1c87c
|
||||
size 33417
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:53f544407b29a510cbfbe78f806a090c65554291018e8f6c202b968ac972105d
|
||||
size 35337
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:18cbed2b974bbbd0a0e16f707c1d0b9eb14d72d8d638e3e9ef9f91da48423e29
|
||||
size 85863
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:83083ca194cd87ed33d7a4246436ac2561481519ae7a440ffa2d4a50cb131381
|
||||
size 89816
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:24805390f21ccaccc00622e1ef99efbe3ce8a514a944fdc5b95060eaef247f32
|
||||
size 43837
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ce1e9ab48a3edb5d4322c04d4c1ccbe16f5188db6a00fd248cadf1d962a43645
|
||||
size 51116
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ec9c8975b0fa564d212ec2241804aaa287901a557ca93d5503d357073fd2a98c
|
||||
size 151507
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:bffa459bce40305f442d5babae093b6611dff474e173fe194ab0bb24e3b3c649
|
||||
size 163522
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3e74a92933da47909b7fd57525936e7042b73412d269d97b162bfaf92947a847
|
||||
size 100719
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:28db91dd1882c36c5dba6ac2c79727c06d60865eae40226fb4f16981d040a1fa
|
||||
size 119047
|
||||
@@ -17,6 +17,7 @@ class SpaceScreenViewModelTests: XCTestCase {
|
||||
let mockSpaceRooms = [SpaceRoomProxyProtocol].mockSpaceList
|
||||
var clientProxy: ClientProxyMock!
|
||||
var paginationStateSubject: CurrentValueSubject<SpaceRoomListPaginationState, Never> = .init(.idle(endReached: true))
|
||||
var rustLeaveHandle: LeaveSpaceHandleSDKMock!
|
||||
|
||||
var viewModel: SpaceScreenViewModelProtocol!
|
||||
|
||||
@@ -192,6 +193,37 @@ class SpaceScreenViewModelTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testLeavingSpace() async throws {
|
||||
setupViewModel()
|
||||
XCTAssertNil(context.leaveHandle)
|
||||
|
||||
let deferredHandle = deferFulfillment(context.observe(\.leaveHandle)) { $0 != nil }
|
||||
context.send(viewAction: .leaveSpace)
|
||||
try await deferredHandle.fulfill()
|
||||
XCTAssertNotNil(context.leaveHandle, "The leave action should show the leave view.")
|
||||
|
||||
let handle = try XCTUnwrap(context.leaveHandle)
|
||||
let selectedCount = handle.selectedCount
|
||||
let firstSelectedRoom = try XCTUnwrap(handle.rooms.first { $0.isSelected })
|
||||
XCTAssertGreaterThan(selectedCount, 0, "The leave view should have selected rooms to begin with")
|
||||
|
||||
context.send(viewAction: .deselectAllLeaveRoomDetails)
|
||||
XCTAssertEqual(handle.selectedCount, 0, "Deselecting all should result in no selected rooms.")
|
||||
|
||||
context.send(viewAction: .toggleLeaveSpaceRoomDetails(id: firstSelectedRoom.spaceRoomProxy.id))
|
||||
XCTAssertEqual(handle.selectedCount, 1, "Toggling a room should result in 1 selected room")
|
||||
|
||||
// Confirming the leave should leave the selected room and then the space.
|
||||
let deferredAction = deferFulfillment(viewModel.actionsPublisher) { $0.isLeftSpace }
|
||||
context.send(viewAction: .confirmLeaveSpace)
|
||||
try await deferredAction.fulfill()
|
||||
XCTAssertNil(context.leaveHandle)
|
||||
XCTAssertTrue(rustLeaveHandle.leaveRoomIdsCalled)
|
||||
XCTAssertEqual(rustLeaveHandle.leaveRoomIdsReceivedRoomIds,
|
||||
[firstSelectedRoom.spaceRoomProxy.id, spaceRoomListProxy.spaceRoomProxy.id],
|
||||
"Confirming the leave should first leave the selected room and then the space.")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func setupViewModel(paginationResponses: [[SpaceRoomProxyProtocol]] = []) {
|
||||
@@ -204,6 +236,11 @@ class SpaceScreenViewModelTests: XCTestCase {
|
||||
guard let spaceRoomProxy = mockSpaceRooms.first(where: { $0.id == spaceID }) else { return .failure(.missingSpace) }
|
||||
return .success(SpaceRoomListProxyMock(.init(spaceRoomProxy: spaceRoomProxy)))
|
||||
}
|
||||
let rustLeaveHandle = LeaveSpaceHandleSDKMock(.init())
|
||||
spaceServiceProxy.leaveSpaceSpaceIDClosure = { spaceID in
|
||||
.success(LeaveSpaceHandleProxy(spaceID: spaceID, leaveHandle: rustLeaveHandle))
|
||||
}
|
||||
self.rustLeaveHandle = rustLeaveHandle
|
||||
|
||||
clientProxy = ClientProxyMock(.init())
|
||||
|
||||
@@ -214,3 +251,12 @@ class SpaceScreenViewModelTests: XCTestCase {
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
}
|
||||
}
|
||||
|
||||
private extension SpaceScreenViewModelAction {
|
||||
var isLeftSpace: Bool {
|
||||
switch self {
|
||||
case .leftSpace: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ packages:
|
||||
# Element/Matrix dependencies
|
||||
MatrixRustSDK:
|
||||
url: https://github.com/element-hq/matrix-rust-components-swift
|
||||
exactVersion: 25.09.26
|
||||
exactVersion: 25.10.02
|
||||
# path: ../matrix-rust-sdk
|
||||
Compound:
|
||||
path: compound-ios
|
||||
|
||||
Reference in New Issue
Block a user