refactor: manage member sheet in timeline

better implementation

updated tests

Update ElementX/Sources/Screens/Timeline/TimelineViewModel.swift

Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>

fix
This commit is contained in:
Mauro Romito
2025-04-08 15:23:40 +02:00
committed by Mauro
parent 56476f8bb5
commit 0e7533145a
44 changed files with 760 additions and 344 deletions
@@ -399,9 +399,26 @@
"screen_advanced_settings_hide_invite_avatars_toggle_title" = "Hide avatars in room invite requests";
"screen_advanced_settings_hide_timeline_media_toggle_title" = "Hide media previews in timeline";
"screen_advanced_settings_moderation_and_safety_section_title" = "Moderation and Safety";
"screen_advanced_settings_show_media_timeline_always_hide" = "Always hide";
"screen_advanced_settings_show_media_timeline_always_show" = "Always show";
"screen_advanced_settings_show_media_timeline_private_rooms" = "In private rooms";
"screen_advanced_settings_show_media_timeline_subtitle" = "A hidden media can always be shown by tapping on it";
"screen_advanced_settings_show_media_timeline_title" = "Show media in timeline";
"screen_bottom_sheet,manage_room_member_remove" = "Remove from room";
"screen_bottom_sheet_create_dm_confirmation_button_title" = "Send invite";
"screen_bottom_sheet_create_dm_message" = "Would you like to start a chat with %1$@?";
"screen_bottom_sheet_create_dm_title" = "Send invite?";
"screen_bottom_sheet_manage_room_member_ban" = "Ban from room";
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_action" = "Ban";
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_description" = "They won’t be able to join this room again if invited.";
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_title" = "Are you sure you want to ban this member?";
"screen_bottom_sheet_manage_room_member_banning_user" = "Banning %1$@";
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_action" = "Remove";
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_description" = "They will be able to join this room again if invited.";
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_title" = "Are you sure you want to remove this member?";
"screen_bottom_sheet_manage_room_member_member_user_info" = "View profile";
"screen_bottom_sheet_manage_room_member_remove_confirmation_title" = "Remove member and ban from joining in the future?";
"screen_bottom_sheet_manage_room_member_removing_user" = "Removing %1$@…";
"screen_create_room_room_access_section_anyone_option_description" = "Anyone can join this room";
"screen_create_room_room_access_section_anyone_option_title" = "Anyone";
"screen_create_room_room_access_section_header" = "Room Access";
@@ -886,26 +903,14 @@
"screen_room_member_details_unblock_user" = "Unblock user";
"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user.";
"screen_room_member_details_verify_button_title" = "Verify %1$@";
"screen_room_member_list_ban_member_confirmation_action" = "Ban";
"screen_room_member_list_ban_member_confirmation_description" = "They won’t be able to join this room again if invited.";
"screen_room_member_list_ban_member_confirmation_title" = "Are you sure you want to ban this member?";
"screen_room_member_list_banned_empty" = "There are no banned users in this room.";
"screen_room_member_list_banning_user" = "Banning %1$@";
"screen_room_member_list_kick_member_confirmation_action" = "Remove";
"screen_room_member_list_kick_member_confirmation_description" = "They will be able to join this room again if invited.";
"screen_room_member_list_kick_member_confirmation_title" = "Are you sure you want to remove this member?";
"screen_room_member_list_manage_member_ban" = "Remove and ban member";
"screen_room_member_list_manage_member_remove" = "Remove from room";
"screen_room_member_list_manage_member_remove_confirmation_kick" = "Only remove member";
"screen_room_member_list_manage_member_remove_confirmation_title" = "Remove member and ban from joining in the future?";
"screen_room_member_list_manage_member_unban_action" = "Unban";
"screen_room_member_list_manage_member_unban_message" = "They will be able to join this room again if invited.";
"screen_room_member_list_manage_member_unban_title" = "Unban user";
"screen_room_member_list_manage_member_user_info" = "View profile";
"screen_room_member_list_mode_banned" = "Banned";
"screen_room_member_list_mode_members" = "Members";
"screen_room_member_list_pending_header_title" = "Pending";
"screen_room_member_list_removing_user" = "Removing %1$@…";
"screen_room_member_list_role_administrator" = "Admin";
"screen_room_member_list_role_moderator" = "Moderator";
"screen_room_member_list_room_members_header_title" = "Room members";
@@ -1134,9 +1139,10 @@
"troubleshoot_notifications_test_current_push_provider_failure" = "No push providers selected.";
"troubleshoot_notifications_test_current_push_provider_success" = "Current push provider: %1$@.";
"troubleshoot_notifications_test_current_push_provider_title" = "Current push provider";
"troubleshoot_notifications_test_detect_push_provider_description" = "Ensure that the application has at least one push provider.";
"troubleshoot_notifications_test_detect_push_provider_failure" = "No push providers found.";
"troubleshoot_notifications_test_detect_push_provider_title" = "Detect push providers";
"troubleshoot_notifications_test_detect_push_provider_description" = "Ensure that the application supports at least one push provider.";
"troubleshoot_notifications_test_detect_push_provider_failure" = "No push provider support found.";
"troubleshoot_notifications_test_detect_push_provider_success_2" = "The application was built with support for: %1$@";
"troubleshoot_notifications_test_detect_push_provider_title" = "Push provider support";
"troubleshoot_notifications_test_display_notification_description" = "Check that the application can display notification.";
"troubleshoot_notifications_test_display_notification_failure" = "The notification has not been clicked.";
"troubleshoot_notifications_test_display_notification_permission_failure" = "Cannot display the notification.";
@@ -1237,7 +1243,7 @@
"screen_room_details_security_title" = "Security";
"screen_room_details_topic_title" = "Topic";
"screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again.";
"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member";
"screen_room_member_list_manage_member_remove_confirmation_ban" = "Ban from room";
"screen_room_notification_settings_mode_all_messages" = "All messages";
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only";
"screen_room_timeline_reactions_show_less" = "Show less";
@@ -399,9 +399,26 @@
"screen_advanced_settings_hide_invite_avatars_toggle_title" = "Hide avatars in room invite requests";
"screen_advanced_settings_hide_timeline_media_toggle_title" = "Hide media previews in timeline";
"screen_advanced_settings_moderation_and_safety_section_title" = "Moderation and Safety";
"screen_advanced_settings_show_media_timeline_always_hide" = "Always hide";
"screen_advanced_settings_show_media_timeline_always_show" = "Always show";
"screen_advanced_settings_show_media_timeline_private_rooms" = "In private rooms";
"screen_advanced_settings_show_media_timeline_subtitle" = "A hidden media can always be shown by tapping on it";
"screen_advanced_settings_show_media_timeline_title" = "Show media in timeline";
"screen_bottom_sheet,manage_room_member_remove" = "Remove from room";
"screen_bottom_sheet_create_dm_confirmation_button_title" = "Send invite";
"screen_bottom_sheet_create_dm_message" = "Would you like to start a chat with %1$@?";
"screen_bottom_sheet_create_dm_title" = "Send invite?";
"screen_bottom_sheet_manage_room_member_ban" = "Ban from room";
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_action" = "Ban";
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_description" = "They won’t be able to join this room again if invited.";
"screen_bottom_sheet_manage_room_member_ban_member_confirmation_title" = "Are you sure you want to ban this member?";
"screen_bottom_sheet_manage_room_member_banning_user" = "Banning %1$@";
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_action" = "Remove";
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_description" = "They will be able to join this room again if invited.";
"screen_bottom_sheet_manage_room_member_kick_member_confirmation_title" = "Are you sure you want to remove this member?";
"screen_bottom_sheet_manage_room_member_member_user_info" = "View profile";
"screen_bottom_sheet_manage_room_member_remove_confirmation_title" = "Remove member and ban from joining in the future?";
"screen_bottom_sheet_manage_room_member_removing_user" = "Removing %1$@…";
"screen_create_room_room_access_section_anyone_option_description" = "Anyone can join this room";
"screen_create_room_room_access_section_anyone_option_title" = "Anyone";
"screen_create_room_room_access_section_header" = "Room Access";
@@ -886,26 +903,14 @@
"screen_room_member_details_unblock_user" = "Unblock user";
"screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user.";
"screen_room_member_details_verify_button_title" = "Verify %1$@";
"screen_room_member_list_ban_member_confirmation_action" = "Ban";
"screen_room_member_list_ban_member_confirmation_description" = "They won’t be able to join this room again if invited.";
"screen_room_member_list_ban_member_confirmation_title" = "Are you sure you want to ban this member?";
"screen_room_member_list_banned_empty" = "There are no banned users in this room.";
"screen_room_member_list_banning_user" = "Banning %1$@";
"screen_room_member_list_kick_member_confirmation_action" = "Remove";
"screen_room_member_list_kick_member_confirmation_description" = "They will be able to join this room again if invited.";
"screen_room_member_list_kick_member_confirmation_title" = "Are you sure you want to remove this member?";
"screen_room_member_list_manage_member_ban" = "Remove and ban member";
"screen_room_member_list_manage_member_remove" = "Remove from room";
"screen_room_member_list_manage_member_remove_confirmation_kick" = "Only remove member";
"screen_room_member_list_manage_member_remove_confirmation_title" = "Remove member and ban from joining in the future?";
"screen_room_member_list_manage_member_unban_action" = "Unban";
"screen_room_member_list_manage_member_unban_message" = "They will be able to join this room again if invited.";
"screen_room_member_list_manage_member_unban_title" = "Unban user";
"screen_room_member_list_manage_member_user_info" = "View profile";
"screen_room_member_list_mode_banned" = "Banned";
"screen_room_member_list_mode_members" = "Members";
"screen_room_member_list_pending_header_title" = "Pending";
"screen_room_member_list_removing_user" = "Removing %1$@…";
"screen_room_member_list_role_administrator" = "Admin";
"screen_room_member_list_role_moderator" = "Moderator";
"screen_room_member_list_room_members_header_title" = "Room members";
@@ -1134,9 +1139,10 @@
"troubleshoot_notifications_test_current_push_provider_failure" = "No push providers selected.";
"troubleshoot_notifications_test_current_push_provider_success" = "Current push provider: %1$@.";
"troubleshoot_notifications_test_current_push_provider_title" = "Current push provider";
"troubleshoot_notifications_test_detect_push_provider_description" = "Ensure that the application has at least one push provider.";
"troubleshoot_notifications_test_detect_push_provider_failure" = "No push providers found.";
"troubleshoot_notifications_test_detect_push_provider_title" = "Detect push providers";
"troubleshoot_notifications_test_detect_push_provider_description" = "Ensure that the application supports at least one push provider.";
"troubleshoot_notifications_test_detect_push_provider_failure" = "No push provider support found.";
"troubleshoot_notifications_test_detect_push_provider_success_2" = "The application was built with support for: %1$@";
"troubleshoot_notifications_test_detect_push_provider_title" = "Push provider support";
"troubleshoot_notifications_test_display_notification_description" = "Check that the application can display notification.";
"troubleshoot_notifications_test_display_notification_failure" = "The notification has not been clicked.";
"troubleshoot_notifications_test_display_notification_permission_failure" = "Cannot display the notification.";
@@ -1237,7 +1243,7 @@
"screen_room_details_security_title" = "Security";
"screen_room_details_topic_title" = "Topic";
"screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again.";
"screen_room_member_list_manage_member_remove_confirmation_ban" = "Remove and ban member";
"screen_room_member_list_manage_member_remove_confirmation_ban" = "Ban from room";
"screen_room_notification_settings_mode_all_messages" = "All messages";
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and Keywords only";
"screen_room_timeline_reactions_show_less" = "Show less";
+46 -32
View File
@@ -978,6 +978,16 @@ internal enum L10n {
internal static var screenAdvancedSettingsSharePresence: String { return L10n.tr("Localizable", "screen_advanced_settings_share_presence") }
/// If turned off, you won’t be able to send or receive read receipts or typing notifications.
internal static var screenAdvancedSettingsSharePresenceDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_share_presence_description") }
/// Always hide
internal static var screenAdvancedSettingsShowMediaTimelineAlwaysHide: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_always_hide") }
/// Always show
internal static var screenAdvancedSettingsShowMediaTimelineAlwaysShow: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_always_show") }
/// In private rooms
internal static var screenAdvancedSettingsShowMediaTimelinePrivateRooms: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_private_rooms") }
/// A hidden media can always be shown by tapping on it
internal static var screenAdvancedSettingsShowMediaTimelineSubtitle: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_subtitle") }
/// Show media in timeline
internal static var screenAdvancedSettingsShowMediaTimelineTitle: String { return L10n.tr("Localizable", "screen_advanced_settings_show_media_timeline_title") }
/// Enable option to view message source in the timeline.
internal static var screenAdvancedSettingsViewSourceDescription: String { return L10n.tr("Localizable", "screen_advanced_settings_view_source_description") }
/// We won't record or profile any personal data
@@ -1082,6 +1092,8 @@ internal enum L10n {
internal static var screenBlockedUsersUnblockAlertTitle: String { return L10n.tr("Localizable", "screen_blocked_users_unblock_alert_title") }
/// Unblocking…
internal static var screenBlockedUsersUnblocking: String { return L10n.tr("Localizable", "screen_blocked_users_unblocking") }
/// Remove from room
internal static var screenBottomSheetManageRoomMemberRemove: String { return L10n.tr("Localizable", "screen_bottom_sheet,manage_room_member_remove") }
/// Send invite
internal static var screenBottomSheetCreateDmConfirmationButtonTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_create_dm_confirmation_button_title") }
/// Would you like to start a chat with %1$@?
@@ -1090,6 +1102,32 @@ internal enum L10n {
}
/// Send invite?
internal static var screenBottomSheetCreateDmTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_create_dm_title") }
/// Ban from room
internal static var screenBottomSheetManageRoomMemberBan: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban") }
/// Ban
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_action") }
/// They won’t be able to join this room again if invited.
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_description") }
/// Are you sure you want to ban this member?
internal static var screenBottomSheetManageRoomMemberBanMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_ban_member_confirmation_title") }
/// Banning %1$@
internal static func screenBottomSheetManageRoomMemberBanningUser(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_banning_user", String(describing: p1))
}
/// Remove
internal static var screenBottomSheetManageRoomMemberKickMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_confirmation_action") }
/// They will be able to join this room again if invited.
internal static var screenBottomSheetManageRoomMemberKickMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_confirmation_description") }
/// Are you sure you want to remove this member?
internal static var screenBottomSheetManageRoomMemberKickMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_kick_member_confirmation_title") }
/// View profile
internal static var screenBottomSheetManageRoomMemberMemberUserInfo: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_member_user_info") }
/// Remove member and ban from joining in the future?
internal static var screenBottomSheetManageRoomMemberRemoveConfirmationTitle: String { return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_remove_confirmation_title") }
/// Removing %1$@…
internal static func screenBottomSheetManageRoomMemberRemovingUser(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_bottom_sheet_manage_room_member_removing_user", String(describing: p1))
}
/// Attach screenshot
internal static var screenBugReportAttachScreenshot: String { return L10n.tr("Localizable", "screen_bug_report_attach_screenshot") }
/// You may contact me if you have any follow up questions.
@@ -2066,56 +2104,28 @@ internal enum L10n {
internal static func screenRoomMemberDetailsVerifyButtonTitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_room_member_details_verify_button_title", String(describing: p1))
}
/// Ban
internal static var screenRoomMemberListBanMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_room_member_list_ban_member_confirmation_action") }
/// They won’t be able to join this room again if invited.
internal static var screenRoomMemberListBanMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_room_member_list_ban_member_confirmation_description") }
/// Are you sure you want to ban this member?
internal static var screenRoomMemberListBanMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_room_member_list_ban_member_confirmation_title") }
/// There are no banned users in this room.
internal static var screenRoomMemberListBannedEmpty: String { return L10n.tr("Localizable", "screen_room_member_list_banned_empty") }
/// Banning %1$@
internal static func screenRoomMemberListBanningUser(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_room_member_list_banning_user", String(describing: p1))
}
/// Plural format key: "%#@COUNT@"
internal static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String {
return L10n.tr("Localizable", "screen_room_member_list_header_title", p1)
}
/// Remove
internal static var screenRoomMemberListKickMemberConfirmationAction: String { return L10n.tr("Localizable", "screen_room_member_list_kick_member_confirmation_action") }
/// They will be able to join this room again if invited.
internal static var screenRoomMemberListKickMemberConfirmationDescription: String { return L10n.tr("Localizable", "screen_room_member_list_kick_member_confirmation_description") }
/// Are you sure you want to remove this member?
internal static var screenRoomMemberListKickMemberConfirmationTitle: String { return L10n.tr("Localizable", "screen_room_member_list_kick_member_confirmation_title") }
/// Remove and ban member
internal static var screenRoomMemberListManageMemberBan: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_ban") }
/// Remove from room
internal static var screenRoomMemberListManageMemberRemove: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove") }
/// Remove and ban member
/// Ban from room
internal static var screenRoomMemberListManageMemberRemoveConfirmationBan: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_ban") }
/// Only remove member
internal static var screenRoomMemberListManageMemberRemoveConfirmationKick: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_kick") }
/// Remove member and ban from joining in the future?
internal static var screenRoomMemberListManageMemberRemoveConfirmationTitle: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_remove_confirmation_title") }
/// Unban
internal static var screenRoomMemberListManageMemberUnbanAction: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_unban_action") }
/// They will be able to join this room again if invited.
internal static var screenRoomMemberListManageMemberUnbanMessage: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_unban_message") }
/// Unban user
internal static var screenRoomMemberListManageMemberUnbanTitle: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_unban_title") }
/// View profile
internal static var screenRoomMemberListManageMemberUserInfo: String { return L10n.tr("Localizable", "screen_room_member_list_manage_member_user_info") }
/// Banned
internal static var screenRoomMemberListModeBanned: String { return L10n.tr("Localizable", "screen_room_member_list_mode_banned") }
/// Members
internal static var screenRoomMemberListModeMembers: String { return L10n.tr("Localizable", "screen_room_member_list_mode_members") }
/// Pending
internal static var screenRoomMemberListPendingHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_pending_header_title") }
/// Removing %1$@…
internal static func screenRoomMemberListRemovingUser(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_room_member_list_removing_user", String(describing: p1))
}
/// Admin
internal static var screenRoomMemberListRoleAdministrator: String { return L10n.tr("Localizable", "screen_room_member_list_role_administrator") }
/// Moderator
@@ -2860,15 +2870,19 @@ internal enum L10n {
}
/// Current push provider
internal static var troubleshootNotificationsTestCurrentPushProviderTitle: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_current_push_provider_title") }
/// Ensure that the application has at least one push provider.
/// Ensure that the application supports at least one push provider.
internal static var troubleshootNotificationsTestDetectPushProviderDescription: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_description") }
/// No push providers found.
/// No push provider support found.
internal static var troubleshootNotificationsTestDetectPushProviderFailure: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_failure") }
/// Plural format key: "%#@COUNT@"
internal static func troubleshootNotificationsTestDetectPushProviderSuccess(_ p1: Int) -> String {
return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_success", p1)
}
/// Detect push providers
/// The application was built with support for: %1$@
internal static func troubleshootNotificationsTestDetectPushProviderSuccess2(_ p1: Any) -> String {
return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_success_2", String(describing: p1))
}
/// Push provider support
internal static var troubleshootNotificationsTestDetectPushProviderTitle: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_detect_push_provider_title") }
/// Check that the application can display notification.
internal static var troubleshootNotificationsTestDisplayNotificationDescription: String { return L10n.tr("Localizable", "troubleshoot_notifications_test_display_notification_description") }
@@ -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.
//
enum ManageRoomMemberSheetViewModelAction: Equatable {
case dismiss(shouldShowDetails: Bool)
}
struct ManageRoomMemberSheetViewState: BindableState {
let member: RoomMemberDetails
let canKick: Bool
let canBan: Bool
var bindings = ManageRoomMemberSheetViewStateBindings()
}
struct ManageRoomMemberSheetViewStateBindings {
var alertInfo: AlertInfo<ManageRoomMemberSheetViewAlertType>?
}
enum ManageRoomMemberSheetViewAlertType {
case kick
case ban
}
enum ManageRoomMemberSheetViewAction {
case kick
case ban
case displayDetails
}
@@ -0,0 +1,125 @@
//
// 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 Combine
import Foundation
import SwiftUI
typealias ManageRoomMemberSheetViewModelType = StateStoreViewModel<ManageRoomMemberSheetViewState, ManageRoomMemberSheetViewAction>
class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, ManageRoomMemberSheetViewModelProtocol {
private let roomProxy: JoinedRoomProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let analyticsService: AnalyticsService
private var actionsSubject: PassthroughSubject<ManageRoomMemberSheetViewModelAction, Never> = .init()
var actions: AnyPublisher<ManageRoomMemberSheetViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(member: RoomMemberDetails,
canKick: Bool,
canBan: Bool,
roomProxy: JoinedRoomProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol,
analyticsService: AnalyticsService,
mediaProvider: MediaProviderProtocol) {
self.userIndicatorController = userIndicatorController
self.roomProxy = roomProxy
self.analyticsService = analyticsService
super.init(initialViewState: .init(member: member, canKick: canKick, canBan: canBan), mediaProvider: mediaProvider)
}
override func process(viewAction: ManageRoomMemberSheetViewAction) {
switch viewAction {
case .kick:
displayAlert(.kick)
case .ban:
displayAlert(.ban)
case .displayDetails:
actionsSubject.send(.dismiss(shouldShowDetails: true))
}
}
private func displayAlert(_ alertType: ManageRoomMemberSheetViewAlertType) {
let member = state.member
var reason: String?
let binding: Binding<String> = .init(get: { reason ?? "" },
set: { reason = $0.isBlank ? nil : $0 })
switch alertType {
case .kick:
state.bindings.alertInfo = .init(id: alertType,
title: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationTitle,
message: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationDescription,
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },
secondaryButton: .init(title: L10n.screenBottomSheetManageRoomMemberKickMemberConfirmationAction) { [weak self] in Task { await self?.kickMember(member, reason: reason) } },
textFields: [.init(placeholder: L10n.commonReason,
text: binding,
autoCapitalization: .sentences,
autoCorrectionDisabled: false)])
case .ban:
state.bindings.alertInfo = .init(id: alertType,
title: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationTitle,
message: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationDescription,
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },
secondaryButton: .init(title: L10n.screenBottomSheetManageRoomMemberBanMemberConfirmationAction) { [weak self] in Task { await self?.banMember(member, reason: reason) } },
textFields: [.init(placeholder: L10n.commonReason,
text: binding,
autoCapitalization: .sentences,
autoCorrectionDisabled: false)])
}
}
private func kickMember(_ member: RoomMemberDetails, reason: String?) async {
let indicatorTitle = L10n.screenBottomSheetManageRoomMemberRemovingUser(member.name ?? member.id)
showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.kickUser(member.id, reason: reason) {
case .success:
hideManageMemberIndicator(title: indicatorTitle)
analyticsService.trackRoomModeration(action: .KickMember, role: nil)
actionsSubject.send(.dismiss(shouldShowDetails: false))
case .failure:
showManageMemberFailure(title: indicatorTitle)
}
}
private func banMember(_ member: RoomMemberDetails, reason: String?) async {
let indicatorTitle = L10n.screenBottomSheetManageRoomMemberBanningUser(member.name ?? member.id)
showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.banUser(member.id, reason: reason) {
case .success:
hideManageMemberIndicator(title: indicatorTitle)
analyticsService.trackRoomModeration(action: .BanMember, role: nil)
actionsSubject.send(.dismiss(shouldShowDetails: false))
case .failure:
showManageMemberFailure(title: indicatorTitle)
}
}
private func showManageMemberIndicator(title: String) {
userIndicatorController.submitIndicator(UserIndicator(id: title,
type: .toast(progress: .indeterminate),
title: title,
persistent: true))
}
private func hideManageMemberIndicator(title: String) {
userIndicatorController.retractIndicatorWithId(title)
}
private func showManageMemberFailure(title: String) {
userIndicatorController.retractIndicatorWithId(title)
userIndicatorController.submitIndicator(UserIndicator(title: L10n.commonFailed, iconName: "xmark"))
}
}
extension ManageRoomMemberSheetViewModel: Identifiable {
var id: String { state.member.id }
}
@@ -0,0 +1,14 @@
//
// 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 Combine
@MainActor
protocol ManageRoomMemberSheetViewModelProtocol {
var actions: AnyPublisher<ManageRoomMemberSheetViewModelAction, Never> { get }
var context: ManageRoomMemberSheetViewModelType.Context { get }
}
@@ -0,0 +1,87 @@
//
// Copyright 2024 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 ManageRoomMemberSheetView: View {
@ObservedObject var context: ManageRoomMemberSheetViewModelType.Context
var body: some View {
Form {
AvatarHeaderView(member: context.viewState.member,
avatarSize: .user(on: .memberDetails),
mediaProvider: context.mediaProvider) {
EmptyView()
}
Section {
ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberMemberUserInfo,
icon: \.userProfileSolid),
kind: .navigationLink {
context.send(viewAction: .displayDetails)
})
}
Section {
if context.viewState.canKick {
ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberRemove,
icon: \.close,
role: .destructive),
kind: .button {
context.send(viewAction: .kick)
})
}
if context.viewState.canBan {
ListRow(label: .default(title: L10n.screenBottomSheetManageRoomMemberBan,
icon: \.block,
role: .destructive),
kind: .button {
context.send(viewAction: .ban)
})
}
}
}
.compoundList()
.scrollBounceBehavior(.basedOnSize)
.presentationDragIndicator(.visible)
.presentationDetents([.large, .fraction(0.67)]) // Maybe find a way to use the ideal height somehow?
.alert(item: $context.alertInfo)
}
}
struct ManageRoomMemberSheetView_Previews: PreviewProvider, TestablePreview {
static let allActionsViewModel = ManageRoomMemberSheetViewModel.mock()
static let kickOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canBan: false)
static let banOnlyViewModel = ManageRoomMemberSheetViewModel.mock(canKick: false)
static var previews: some View {
ManageRoomMemberSheetView(context: allActionsViewModel.context)
.previewDisplayName("All Actions")
ManageRoomMemberSheetView(context: kickOnlyViewModel.context)
.previewDisplayName("Kick Only")
ManageRoomMemberSheetView(context: banOnlyViewModel.context)
.previewDisplayName("Ban Only")
}
}
private extension ManageRoomMemberSheetViewModel {
static func mock(canKick: Bool = true,
canBan: Bool = true) -> ManageRoomMemberSheetViewModel {
let member = RoomMemberDetails(withProxy: RoomMemberProxyMock.mockDan)
return ManageRoomMemberSheetViewModel(member: member,
canKick: canKick,
canBan: canBan,
roomProxy: JoinedRoomProxyMock(.init()),
userIndicatorController: UserIndicatorControllerMock(),
analyticsService: ServiceLocator.shared.analytics,
mediaProvider: MediaProviderMock(configuration: .init()))
}
}
@@ -80,7 +80,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
self?.displayMediaPreview(mediaPreviewViewModel)
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
.tappedOnSenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
.displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
.composer, .hasScrolled, .viewInRoomTimeline:
break
}
@@ -102,7 +102,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType
self?.displayMediaPreview(mediaPreviewViewModel)
case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker,
.displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen,
.tappedOnSenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
.displaySenderDetails, .displayMessageForwarding, .displayLocation, .displayResolveSendFailure,
.composer, .hasScrolled, .viewInRoomTimeline:
break
}
@@ -78,7 +78,7 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
guard let self else { return }
switch action {
case .tappedOnSenderDetails(let userID):
case .displaySenderDetails(let userID):
actionsSubject.send(.displayUser(userID: userID))
case .displayMessageForwarding(let forwardingItem):
actionsSubject.send(.displayMessageForwarding(forwardingItem: forwardingItem))
@@ -28,6 +28,9 @@ struct PinnedEventsTimelineScreen: View {
.background(.compound.bgCanvasDefault)
.interactiveDismissDisabled()
.timelineMediaPreview(viewModel: $context.mediaPreviewViewModel)
.sheet(item: $timelineContext.manageMemberViewModel) {
ManageRoomMemberSheetView(context: $0.context)
}
.sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) }
.sheet(item: $timelineContext.actionMenuInfo) { info in
let actions = TimelineItemMenuActionProvider(timelineItem: info.item,
@@ -80,31 +80,15 @@ struct RoomMembersListScreenViewStateBindings {
var searchQuery = ""
/// The current mode the screen is in.
var mode: RoomMembersListScreenMode = .members
/// A selected member to kick, ban, promote etc.
var memberToManage: RoomMembersListScreenManagementDetails?
/// A sheet model for the selected member to kick, ban, promote etc.
var manageMemeberViewModel: ManageRoomMemberSheetViewModel?
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<RoomMembersListScreenAlertType>?
}
/// Information about managing a particular room member.
struct RoomMembersListScreenManagementDetails: Identifiable {
var id: String { member.id }
/// The member that is being managed.
let member: RoomMemberDetails
/// A management action that can be performed on the member.
enum Action { case kick, ban }
/// The management actions available for `member`.
let actions: [Action]
}
enum RoomMembersListScreenViewAction {
case selectMember(RoomMemberDetails)
case showMemberDetails(RoomMemberDetails)
case kickMember(RoomMemberDetails)
case banMember(RoomMemberDetails)
case unbanMember(RoomMemberDetails)
case invite
}
@@ -15,8 +15,10 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
private let roomProxy: JoinedRoomProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let analytics: AnalyticsService
private let mediaProvider: MediaProviderProtocol
private var members: [RoomMemberProxyProtocol] = []
private var currentUserProxy: RoomMemberProxyProtocol?
private var actionsSubject: PassthroughSubject<RoomMembersListScreenViewModelAction, Never> = .init()
@@ -34,6 +36,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
self.roomProxy = roomProxy
self.userIndicatorController = userIndicatorController
self.analytics = analytics
self.mediaProvider = mediaProvider
super.init(initialViewState: .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
bindings: .init(mode: initialMode)),
@@ -48,34 +51,6 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
switch viewAction {
case .selectMember(let member):
selectMember(member)
case .showMemberDetails(let member):
showMemberDetails(member)
case .kickMember(let member):
var reason: String?
let binding: Binding<String> = .init(get: { reason ?? "" },
set: { reason = $0.isBlank ? nil : $0 })
state.bindings.alertInfo = .init(id: .kickConfirmation,
title: L10n.screenRoomMemberListKickMemberConfirmationTitle,
message: L10n.screenRoomMemberListKickMemberConfirmationDescription,
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },
secondaryButton: .init(title: L10n.screenRoomMemberListKickMemberConfirmationAction) { [weak self] in Task { await self?.kickMember(member, reason: reason) } },
textFields: [.init(placeholder: L10n.commonReason,
text: binding,
autoCapitalization: .sentences,
autoCorrectionDisabled: false)])
case .banMember(let member):
var reason: String?
let binding: Binding<String> = .init(get: { reason ?? "" },
set: { reason = $0.isBlank ? nil : $0 })
state.bindings.alertInfo = .init(id: .banConfirmation,
title: L10n.screenRoomMemberListBanMemberConfirmationTitle,
message: L10n.screenRoomMemberListBanMemberConfirmationDescription,
primaryButton: .init(title: L10n.actionCancel, role: .cancel) { },
secondaryButton: .init(title: L10n.screenRoomMemberListBanMemberConfirmationAction) { [weak self] in Task { await self?.banMember(member, reason: reason) } },
textFields: [.init(placeholder: L10n.commonReason,
text: binding,
autoCapitalization: .sentences,
autoCorrectionDisabled: false)])
case .unbanMember(let member):
Task { await unbanMember(member) }
case .invite:
@@ -118,6 +93,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
let members = members.sorted()
let roomMembersDetails = await buildMembersDetails(members: members)
self.members = members
self.currentUserProxy = members.first { $0.userID == roomProxy.ownUserID }
self.state = .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
joinedMembers: roomMembersDetails.joinedMembers,
@@ -168,6 +144,12 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
}
private func selectMember(_ member: RoomMemberDetails) {
guard let currentUserProxy,
currentUserProxy.powerLevel > member.powerLevel else {
showMemberDetails(member)
return
}
if member.isBanned { // No need to check canBan here, banned users are only shown when it is true.
state.bindings.alertInfo = AlertInfo(id: .unbanConfirmation(member),
title: L10n.screenRoomMemberListManageMemberUnbanTitle,
@@ -179,16 +161,26 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
return
}
var actions = [RoomMembersListScreenManagementDetails.Action]()
if state.canKickUsers, member.role != .administrator {
actions.append(.kick)
}
if state.canBanUsers, member.role != .administrator {
actions.append(.ban)
}
if !actions.isEmpty {
state.bindings.memberToManage = .init(member: member, actions: actions)
if state.canKickUsers || state.canBanUsers {
let manageMemeberViewModel = ManageRoomMemberSheetViewModel(member: member,
canKick: state.canKickUsers,
canBan: state.canBanUsers,
roomProxy: roomProxy,
userIndicatorController: userIndicatorController,
analyticsService: analytics,
mediaProvider: mediaProvider)
manageMemeberViewModel.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .dismiss(let shouldShowDetails):
state.bindings.manageMemeberViewModel = nil
if shouldShowDetails {
showMemberDetails(member)
}
}
}
.store(in: &cancellables)
state.bindings.manageMemeberViewModel = manageMemeberViewModel
} else {
showMemberDetails(member)
}
@@ -200,46 +192,16 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
return
}
actionsSubject.send(.selectMember(member))
state.bindings.memberToManage = nil
}
// MARK: - Member Management
private func kickMember(_ member: RoomMemberDetails, reason: String?) async {
let indicatorTitle = L10n.screenRoomMemberListRemovingUser(member.name ?? member.id)
showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.kickUser(member.id, reason: reason) {
case .success:
state.bindings.memberToManage = nil
hideManageMemberIndicator(title: indicatorTitle)
analytics.trackRoomModeration(action: .KickMember, role: nil)
case .failure:
showManageMemberFailure(title: indicatorTitle)
}
}
private func banMember(_ member: RoomMemberDetails, reason: String?) async {
let indicatorTitle = L10n.screenRoomMemberListBanningUser(member.name ?? member.id)
showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.banUser(member.id, reason: reason) {
case .success:
state.bindings.memberToManage = nil
hideManageMemberIndicator(title: indicatorTitle)
analytics.trackRoomModeration(action: .BanMember, role: nil)
case .failure:
showManageMemberFailure(title: indicatorTitle)
}
}
private func unbanMember(_ member: RoomMemberDetails) async {
let indicatorTitle = L10n.screenRoomMemberListUnbanningUser(member.name ?? member.id)
showManageMemberIndicator(title: indicatorTitle)
switch await roomProxy.unbanUser(member.id) {
case .success:
state.bindings.memberToManage = nil
hideManageMemberIndicator(title: indicatorTitle)
analytics.trackRoomModeration(action: .UnbanMember, role: nil)
case .failure:
@@ -1,96 +0,0 @@
//
// Copyright 2024 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 RoomMembersListManageMemberSheet: View {
let member: RoomMemberDetails
let actions: [RoomMembersListScreenManagementDetails.Action]
@ObservedObject var context: RoomMembersListScreenViewModel.Context
var body: some View {
Form {
AvatarHeaderView(member: member,
avatarSize: .user(on: .memberDetails),
mediaProvider: context.mediaProvider) {
EmptyView()
}
Section {
ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberUserInfo,
icon: \.userProfileSolid),
kind: .button {
context.send(viewAction: .showMemberDetails(member))
})
if actions.contains(.kick) {
ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberRemove,
icon: \.close),
kind: .button {
context.send(viewAction: .kickMember(member))
})
}
if actions.contains(.ban) {
ListRow(label: .default(title: L10n.screenRoomMemberListManageMemberBan,
icon: \.block,
role: .destructive),
kind: .button {
context.send(viewAction: .banMember(member))
})
}
}
}
.compoundList()
.scrollBounceBehavior(.basedOnSize)
.presentationDragIndicator(.visible)
.presentationDetents([.large, .fraction(0.54)]) // Maybe find a way to use the ideal height somehow?
}
}
struct RoomMembersListManageMemberSheet_Previews: PreviewProvider, TestablePreview {
static let viewModel = RoomMembersListScreenViewModel.mock
static var previews: some View {
RoomMembersListManageMemberSheet(member: .init(withProxy: RoomMemberProxyMock.mockDan),
actions: [.kick, .ban],
context: viewModel.context)
.previewDisplayName("Joined")
RoomMembersListManageMemberSheet(member: .init(withProxy: RoomMemberProxyMock.mockBanned[3]),
actions: [],
context: viewModel.context)
.previewDisplayName("Banned")
}
}
struct RoomMembersListManageMemberSheetLive_Previews: PreviewProvider {
static let viewModel = RoomMembersListScreenViewModel.mock
static var previews: some View {
Color.clear
.sheet(isPresented: .constant(true)) {
RoomMembersListManageMemberSheet(member: .init(withProxy: RoomMemberProxyMock.mockDan),
actions: [.kick, .ban],
context: viewModel.context)
}
.previewDisplayName("Sheet")
}
}
private extension RoomMembersListScreenViewModel {
static var mock: RoomMembersListScreenViewModel {
RoomMembersListScreenViewModel(initialMode: .members,
clientProxy: ClientProxyMock(.init()),
roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)),
mediaProvider: MediaProviderMock(configuration: .init()),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
analytics: ServiceLocator.shared.analytics)
}
}
@@ -47,8 +47,8 @@ struct RoomMembersListScreen: View {
.autocorrectionDisabled()
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
.navigationTitle(L10n.commonPeople)
.sheet(item: $context.memberToManage) {
RoomMembersListManageMemberSheet(member: $0.member, actions: $0.actions, context: context)
.sheet(item: $context.manageMemeberViewModel) {
ManageRoomMemberSheetView(context: $0.context)
}
.alert(item: $context.alertInfo)
.toolbar { toolbar }
@@ -136,7 +136,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentPollForm(mode: mode))
case .displayMediaUploadPreviewScreen(let url):
actionsSubject.send(.presentMediaUploadPreviewScreen(url))
case .tappedOnSenderDetails(userID: let userID):
case .displaySenderDetails(userID: let userID):
actionsSubject.send(.presentRoomMemberDetails(userID: userID))
case .displayMessageForwarding(let forwardingItem):
actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem))
@@ -66,6 +66,9 @@ struct RoomScreen: View {
.toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background.
.overlay { loadingIndicator }
.alert(item: $timelineContext.alertInfo)
.sheet(item: $timelineContext.manageMemberViewModel) {
ManageRoomMemberSheetView(context: $0.context)
}
.sheet(item: $timelineContext.debugInfo) { TimelineItemDebugView(info: $0) }
.sheet(item: $timelineContext.actionMenuInfo) { info in
let actions = TimelineItemMenuActionProvider(timelineItem: info.item,
@@ -19,7 +19,7 @@ enum TimelineViewModelAction {
case displayLocationPicker
case displayPollForm(mode: PollFormMode)
case displayMediaUploadPreviewScreen(url: URL)
case tappedOnSenderDetails(userID: String)
case displaySenderDetails(userID: String)
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
case displayMediaPreview(TimelineMediaPreviewViewModel)
case displayLocation(body: String, geoURI: GeoURI, description: String?)
@@ -99,6 +99,8 @@ struct TimelineViewState: BindableState {
var canCurrentUserRedactOthers = false
var canCurrentUserRedactSelf = false
var canCurrentUserPin = false
var canCurrentUserKick = false
var canCurrentUserBan = false
var isViewSourceEnabled: Bool
var hideTimelineMedia: Bool
@@ -135,7 +137,7 @@ struct TimelineViewStateBindings {
/// Key is itemID, value is the collapsed state.
var reactionsCollapsed: [TimelineItemIdentifier: Bool]
var alertInfo: AlertInfo<RoomScreenAlertInfoType>?
var alertInfo: AlertInfo<TimelineAlertInfoType>?
var debugInfo: TimelineItemDebugInfo?
@@ -144,6 +146,8 @@ struct TimelineViewStateBindings {
var reactionSummaryInfo: ReactionSummaryInfo?
var readReceiptsSummaryInfo: ReadReceiptSummaryInfo?
var manageMemberViewModel: ManageRoomMemberSheetViewModel?
}
struct TimelineItemActionMenuInfo: Equatable, Identifiable {
@@ -172,7 +176,7 @@ struct ReadReceiptSummaryInfo: Identifiable {
let id: TimelineItemIdentifier
}
enum RoomScreenAlertInfoType: Hashable {
enum TimelineAlertInfoType: Hashable {
case audioRecodingPermissionError
case pollEndConfirmation(String)
case sendingFailed
@@ -42,6 +42,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
actionsSubject.eraseToAnyPublisher()
}
private var currentUserProxy: RoomMemberProxyProtocol?
private var paginateBackwardsTask: Task<Void, Never>?
private var paginateForwardsTask: Task<Void, Never>?
@@ -173,7 +175,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
case .handleTimelineItemMenuAction(let itemID, let action):
timelineInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID)
case .tappedOnSenderDetails(userID: let userID):
actionsSubject.send(.tappedOnSenderDetails(userID: userID))
handleTappedOnSenderDetails(userID: userID)
case .displayEmojiPicker(let itemID):
timelineInteractionHandler.displayEmojiPicker(for: itemID)
case .displayReactionSummary(let itemID, let key):
@@ -258,6 +260,41 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
// MARK: - Private
private func handleTappedOnSenderDetails(userID: String) {
// We also need to make sure the user is in the joined state, otherwise we just show the details.
guard let memberProxy = roomProxy.membersPublisher.value.first(where: { $0.userID == userID && $0.membership == .join }),
let currentUserProxy,
currentUserProxy.powerLevel > memberProxy.powerLevel else {
actionsSubject.send(.displaySenderDetails(userID: userID))
return
}
if state.canCurrentUserBan || state.canCurrentUserKick {
let member = RoomMemberDetails(withProxy: memberProxy)
let manageMemeberViewModel = ManageRoomMemberSheetViewModel(member: member,
canKick: state.canCurrentUserKick,
canBan: state.canCurrentUserBan,
roomProxy: roomProxy,
userIndicatorController: userIndicatorController,
analyticsService: analyticsService,
mediaProvider: mediaProvider)
manageMemeberViewModel.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .dismiss(let shouldShowDetails):
state.bindings.manageMemberViewModel = nil
if shouldShowDetails {
actionsSubject.send(.displaySenderDetails(userID: userID))
}
}
}
.store(in: &cancellables)
state.bindings.manageMemberViewModel = manageMemeberViewModel
} else {
actionsSubject.send(.displaySenderDetails(userID: userID))
}
}
private func focusLive() {
timelineController.focusLive()
}
@@ -348,6 +385,9 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
private func updateMembers(_ members: [RoomMemberProxyProtocol]) {
state.members = members.reduce(into: [String: RoomMemberState]()) { dictionary, member in
dictionary[member.userID] = RoomMemberState(displayName: member.displayName, avatarURL: member.avatarURL)
if member.userID == roomProxy.ownUserID {
currentUserProxy = member
}
}
}
@@ -369,6 +409,18 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
} else {
state.canCurrentUserPin = false
}
if case let .success(value) = await roomProxy.canUserKick(userID: roomProxy.ownUserID) {
state.canCurrentUserKick = value
} else {
state.canCurrentUserKick = false
}
if case let .success(value) = await roomProxy.canUserBan(userID: roomProxy.ownUserID) {
state.canCurrentUserBan = value
} else {
state.canCurrentUserBan = false
}
}
private func setupSubscriptions() {
@@ -898,7 +950,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
userIndicatorController.retractIndicatorWithId(Constants.focusTimelineToastIndicatorID)
}
private func displayAlert(_ type: RoomScreenAlertInfoType) {
private func displayAlert(_ type: TimelineAlertInfoType) {
switch type {
case .audioRecodingPermissionError:
state.bindings.alertInfo = .init(id: type,
@@ -20,6 +20,7 @@ struct RoomMemberDetails: Identifiable, Hashable {
enum Role { case administrator, moderator, user }
let role: Role
let powerLevel: Int
func matches(searchQuery: String) -> Bool {
guard !searchQuery.isEmpty else { return true }
@@ -38,6 +39,7 @@ extension RoomMemberDetails {
isIgnored = proxy.isIgnored
isBanned = proxy.membership == .ban
role = .init(proxy.role)
powerLevel = proxy.powerLevel
}
}