diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index c5a94ded1..d79c30175 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -479,6 +479,7 @@ Tap the + to start adding people."; "room_participants_invite_malformed_id" = "Malformed ID. Should be an email address or a Matrix ID like '@localpart:domain'"; "room_participants_invited_section" = "INVITED"; "room_participants_start_new_chat_error_using_user_email_without_identity_server" = "No identity server is configured so you cannot start a chat with a contact using an email."; +"room_participants_leave_not_allowed_for_last_owner_msg" = "You can't leave the room since you're the only owner of it."; "room_participants_online" = "Online"; "room_participants_offline" = "Offline"; diff --git a/Riot/Categories/MXRoom.swift b/Riot/Categories/MXRoom.swift new file mode 100644 index 000000000..02919c0a0 --- /dev/null +++ b/Riot/Categories/MXRoom.swift @@ -0,0 +1,33 @@ +// +// 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. +// + +@objc +extension MXRoom { + /// Returns true if the user is the last owner of the room, but not the last member. + func isLastOwner() async throws -> Bool { + let userID = mxSession.myUserId + let state = try await state() + + guard RoomPowerLevelHelper.roomPowerLevel(from: state.powerLevelOfUser(withUserID: userID)) == .owner else { + return false + } + + guard let joinedMembers = try await members()?.members(with: .join) else { + return false + } + + var isLastMember = true + for member in joinedMembers where member.userId != userID { + isLastMember = false + // If there are other owners they can leave + if RoomPowerLevelHelper.roomPowerLevel(from: state.powerLevelOfUser(withUserID: member.userId)) == .owner { + return false + } + } + return !isLastMember + } +} diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 790cbb1af..9c4bc1ee0 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -6447,6 +6447,10 @@ public class VectorL10n: NSObject { public static var roomParticipantsInvitedSection: String { return VectorL10n.tr("Vector", "room_participants_invited_section") } + /// You can't leave the room since you're the only owner of it + public static var roomParticipantsLeaveNotAllowedForLastOwnerMsg: String { + return VectorL10n.tr("Vector", "room_participants_leave_not_allowed_for_last_owner_msg") + } /// Leaving public static var roomParticipantsLeaveProcessing: String { return VectorL10n.tr("Vector", "room_participants_leave_processing") diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift index 383beac30..22f50ba99 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewController.swift @@ -75,6 +75,17 @@ final class RoomInfoListViewController: UIViewController { return controller }() + private lazy var isLastOwnerAlertController: UIAlertController = { + let title = VectorL10n.error + let message = VectorL10n.roomParticipantsLeaveNotAllowedForLastOwnerMsg + let controller = UIAlertController(title: title, message: message, preferredStyle: .alert) + + controller.addAction(UIAlertAction(title: VectorL10n.ok, style: .default, handler: nil)) + controller.mxk_setAccessibilityIdentifier("RoomSettingsVCLastOwnerAlert") + + return controller + }() + private enum RowType { case `default` case destructive @@ -216,7 +227,11 @@ final class RoomInfoListViewController: UIViewController { VectorL10n.roomParticipantsLeavePromptTitleForDm : VectorL10n.roomParticipantsLeavePromptTitle let rowLeave = Row(type: .destructive, icon: Asset.Images.roomActionLeave.image, text: leaveTitle, accessoryType: .none) { - self.present(self.leaveAlertController, animated: true, completion: nil) + if viewData.isLastOwner { + self.present(self.isLastOwnerAlertController, animated: true, completion: nil) + } else { + self.present(self.leaveAlertController, animated: true, completion: nil) + } } let sectionLeave = Section(header: nil, rows: [rowLeave], diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewData.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewData.swift index c217514b8..b8be0b49e 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewData.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewData.swift @@ -24,4 +24,5 @@ struct RoomInfoListViewData { let isEncrypted: Bool let isDirect: Bool let basicInfoViewData: RoomInfoBasicViewData + let isLastOwner: Bool } diff --git a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift index bf2c82e9b..c208647a4 100644 --- a/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift +++ b/Riot/Modules/Room/RoomInfo/RoomInfoList/RoomInfoListViewModel.swift @@ -26,6 +26,7 @@ final class RoomInfoListViewModel: NSObject, RoomInfoListViewModelType { private let session: MXSession private let room: MXRoom + private var isLastOwner = false // MARK: Public @@ -51,7 +52,8 @@ final class RoomInfoListViewModel: NSObject, RoomInfoListViewModelType { return RoomInfoListViewData(numberOfMembers: Int(room.summary.membersCount.joined), isEncrypted: room.summary.isEncrypted, isDirect: room.isDirect, - basicInfoViewData: basicInfoViewData) + basicInfoViewData: basicInfoViewData, + isLastOwner: isLastOwner) } // MARK: - Setup @@ -97,6 +99,9 @@ final class RoomInfoListViewModel: NSObject, RoomInfoListViewModelType { @objc private func roomSummaryUpdated(_ notification: Notification) { // force update view self.update(viewState: .loaded(viewData: viewData)) + Task { + isLastOwner = (try? await room.isLastOwner()) == true + } } private func loadData() {