DM Details View (#744)

* Basic implementation completed but it requires some bug fixing and polishing (+4 squashed commits)
Squashed commits:
[43f0064e] now the content of the room details reflects the expected dm details content when the conditions are met
[fe6f2b5e] replaced existing headers with a composable and reusable HeaderView
[3c00120a] now the members are fetched every time the room is opened
[421d539b] refactored the members fetching to be immediate in case the room could be a direct chat

* polished the code and fixed some bugs that prevented the updated state of the members to be displayed in the room details view (+1 squashed commit)
Squashed commits:
[6bf98473] fix for the button not being destructive in room details screen

* removed copy button and updated copy

* moved a file and renamed header view

* addressed all PR comments

* sdk bump

* UI tests

* unit tests

* changelog

* updated mocks

* removed a duplicated signature in a mock
This commit is contained in:
Mauro
2023-04-03 13:18:19 +02:00
committed by GitHub
parent 220138d492
commit 0f858d977f
35 changed files with 1241 additions and 241 deletions
@@ -163,7 +163,7 @@
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing.git",
"state" : {
"revision" : "cef5b3f6f11781dd4591bdd1dd0a3d22bd609334",
"version" : "1.11.0"
@@ -37,7 +37,7 @@
"action_save" = "Save";
"action_search" = "Search";
"action_send" = "Send";
"action_share_link" = "Share link";
"action_share_link" = "Share";
"action_skip" = "Skip";
"action_start" = "Start";
"action_start_chat" = "Start chat";
@@ -47,6 +47,7 @@
"common_about" = "About";
"common_audio" = "Audio";
"common_bubbles" = "Bubbles";
"common_creating_room" = "Creating room…";
"common_decryption_error" = "Decryption error";
"common_developer_options" = "Developer options";
"common_edited_suffix" = "(edited)";
@@ -104,6 +105,7 @@
"emoji_picker_category_symbols" = "Symbols";
"error_failed_creating_the_permalink" = "Failed creating the permalink";
"error_failed_loading_messages" = "Failed loading messages";
"error_no_compatible_app_found" = "No compatible app was found to handle this action.";
"error_some_messages_have_not_been_sent" = "Some messages have not been sent";
"error_unknown" = "Sorry, an error occurred";
"invite_friends_text" = "Hey, talk to me on %1$@: %2$@";
@@ -154,6 +156,12 @@
"screen_create_room_action_create_room" = "New room";
"screen_create_room_action_invite_people" = "Invite people";
"screen_create_room_add_people_title" = "Add people";
"screen_dm_details_block_alert_action" = "Block";
"screen_dm_details_block_alert_description" = "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.";
"screen_dm_details_block_user" = "Block user";
"screen_dm_details_unblock_alert_action" = "Unblock";
"screen_dm_details_unblock_alert_description" = "On unblocking the user, you will be able to see all messages by them again.";
"screen_dm_details_unblock_user" = "Unblock user";
"screen_login_error_deactivated_account" = "This account has been deactivated.";
"screen_login_error_invalid_credentials" = "Incorrect username and/or password";
"screen_login_error_invalid_user_id" = "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’";
@@ -174,6 +182,7 @@
"screen_room_details_leave_room_title" = "Leave room";
"screen_room_details_people_title" = "People";
"screen_room_details_security_title" = "Security";
"screen_room_details_share_room_title" = "Share room";
"screen_room_details_topic_title" = "Topic";
"screen_room_member_details_block_alert_action" = "Block";
"screen_room_member_details_block_alert_description" = "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.";
@@ -37,7 +37,7 @@
"action_save" = "Save";
"action_search" = "Search";
"action_send" = "Send";
"action_share_link" = "Share link";
"action_share_link" = "Share";
"action_skip" = "Skip";
"action_start" = "Start";
"action_start_chat" = "Start chat";
@@ -47,6 +47,7 @@
"common_about" = "About";
"common_audio" = "Audio";
"common_bubbles" = "Bubbles";
"common_creating_room" = "Creating room…";
"common_decryption_error" = "Decryption error";
"common_developer_options" = "Developer options";
"common_edited_suffix" = "(edited)";
@@ -104,6 +105,7 @@
"emoji_picker_category_symbols" = "Symbols";
"error_failed_creating_the_permalink" = "Failed creating the permalink";
"error_failed_loading_messages" = "Failed loading messages";
"error_no_compatible_app_found" = "No compatible app was found to handle this action.";
"error_some_messages_have_not_been_sent" = "Some messages have not been sent";
"error_unknown" = "Sorry, an error occurred";
"invite_friends_text" = "Hey, talk to me on %1$@: %2$@";
@@ -154,6 +156,12 @@
"screen_create_room_action_create_room" = "New room";
"screen_create_room_action_invite_people" = "Invite people";
"screen_create_room_add_people_title" = "Add people";
"screen_dm_details_block_alert_action" = "Block";
"screen_dm_details_block_alert_description" = "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.";
"screen_dm_details_block_user" = "Block user";
"screen_dm_details_unblock_alert_action" = "Unblock";
"screen_dm_details_unblock_alert_description" = "On unblocking the user, you will be able to see all messages by them again.";
"screen_dm_details_unblock_user" = "Unblock user";
"screen_login_error_deactivated_account" = "This account has been deactivated.";
"screen_login_error_invalid_credentials" = "Incorrect username and/or password";
"screen_login_error_invalid_user_id" = "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’";
@@ -174,6 +182,7 @@
"screen_room_details_leave_room_title" = "Leave room";
"screen_room_details_people_title" = "People";
"screen_room_details_security_title" = "Security";
"screen_room_details_share_room_title" = "Share room";
"screen_room_details_topic_title" = "Topic";
"screen_room_member_details_block_alert_action" = "Block";
"screen_room_member_details_block_alert_description" = "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.";
@@ -34,5 +34,21 @@
<string>%1$d room changes</string>
</dict>
</dict>
<key>screen_room_member_list_header_title</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>1 person</string>
<key>other</key>
<string>%1$d people</string>
</dict>
</dict>
</dict>
</plist>
@@ -37,7 +37,7 @@
"action_save" = "Save";
"action_search" = "Search";
"action_send" = "Send";
"action_share_link" = "Share link";
"action_share_link" = "Share";
"action_skip" = "Skip";
"action_start" = "Start";
"action_start_chat" = "Start chat";
@@ -47,6 +47,7 @@
"common_about" = "About";
"common_audio" = "Audio";
"common_bubbles" = "Bubbles";
"common_creating_room" = "Creating room…";
"common_decryption_error" = "Decryption error";
"common_developer_options" = "Developer options";
"common_edited_suffix" = "(edited)";
@@ -104,6 +105,7 @@
"emoji_picker_category_symbols" = "Symbols";
"error_failed_creating_the_permalink" = "Failed creating the permalink";
"error_failed_loading_messages" = "Failed loading messages";
"error_no_compatible_app_found" = "No compatible app was found to handle this action.";
"error_some_messages_have_not_been_sent" = "Some messages have not been sent";
"error_unknown" = "Sorry, an error occurred";
"invite_friends_text" = "Hey, talk to me on %1$@: %2$@";
@@ -154,6 +156,12 @@
"screen_create_room_action_create_room" = "New room";
"screen_create_room_action_invite_people" = "Invite people";
"screen_create_room_add_people_title" = "Add people";
"screen_dm_details_block_alert_action" = "Block";
"screen_dm_details_block_alert_description" = "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.";
"screen_dm_details_block_user" = "Block user";
"screen_dm_details_unblock_alert_action" = "Unblock";
"screen_dm_details_unblock_alert_description" = "On unblocking the user, you will be able to see all messages by them again.";
"screen_dm_details_unblock_user" = "Unblock user";
"screen_login_error_deactivated_account" = "This account has been deactivated.";
"screen_login_error_invalid_credentials" = "Incorrect username and/or password";
"screen_login_error_invalid_user_id" = "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’";
@@ -174,6 +182,7 @@
"screen_room_details_leave_room_title" = "Leave room";
"screen_room_details_people_title" = "People";
"screen_room_details_security_title" = "Security";
"screen_room_details_share_room_title" = "Share room";
"screen_room_details_topic_title" = "Topic";
"screen_room_member_details_block_alert_action" = "Block";
"screen_room_member_details_block_alert_description" = "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.";
@@ -0,0 +1,276 @@
"Notification" = "NOTIFICA";
"a11y_hide_password" = "Nascondi password";
"a11y_send_files" = "Send files";
"a11y_show_password" = "Show password";
"a11y_user_menu" = "User menu";
"action_back" = "Back";
"action_cancel" = "Cancel";
"action_clear" = "Clear";
"action_close" = "Close";
"action_complete_verification" = "Complete verification";
"action_confirm" = "Confirm";
"action_continue" = "Continue";
"action_copy" = "Copy";
"action_copy_link" = "Copy link";
"action_create_a_room" = "Create a room";
"action_disable" = "Disable";
"action_done" = "Done";
"action_edit" = "Edit";
"action_enable" = "Enable";
"action_invite" = "Invite";
"action_invite_friends_to_app" = "Invite friends to %1$@";
"action_learn_more" = "Learn more";
"action_leave" = "Leave";
"action_leave_room" = "Leave room";
"action_next" = "Next";
"action_no" = "No";
"action_not_now" = "Not now";
"action_ok" = "OK";
"action_quick_reply" = "Quick reply";
"action_quote" = "Quote";
"action_remove" = "Remove";
"action_reply" = "Reply";
"action_report_bug" = "Report bug";
"action_report_content" = "Report Content";
"action_retry" = "Retry";
"action_retry_decryption" = "Retry decryption";
"action_save" = "Save";
"action_search" = "Search";
"action_send" = "Send";
"action_share_link" = "Share";
"action_skip" = "Skip";
"action_start" = "Start";
"action_start_chat" = "Start chat";
"action_start_verification" = "Start verification";
"action_view_source" = "View Source";
"action_yes" = "Yes";
"common_about" = "About";
"common_audio" = "Audio";
"common_bubbles" = "Bubbles";
"common_creating_room" = "Creating room…";
"common_decryption_error" = "Decryption error";
"common_developer_options" = "Developer options";
"common_edited_suffix" = "(edited)";
"common_editing" = "Editing";
"common_encryption_enabled" = "Encryption enabled";
"common_error" = "Error";
"common_file" = "File";
"common_gif" = "GIF";
"common_image" = "Image";
"common_link_copied_to_clipboard" = "Link copied to clipboard";
"common_loading" = "Loading…";
"common_message" = "Message";
"common_message_layout" = "Message layout";
"common_message_removed" = "Message removed";
"common_modern" = "Modern";
"common_no_results" = "No results";
"common_offline" = "Offline";
"common_password" = "Password";
"common_people" = "People";
"common_permalink" = "Permalink";
"common_reactions" = "Reactions";
"common_replying_to" = "Replying to %1$@";
"common_report_a_bug" = "Report a bug";
"common_report_submitted" = "Report submitted";
"common_search_for_someone" = "Search for someone";
"common_security" = "Security";
"common_select_your_server" = "Select your server";
"common_sending" = "Sending…";
"common_server_not_supported" = "Server not supported";
"common_server_url" = "Server URL";
"common_settings" = "Settings";
"common_sticker" = "Sticker";
"common_success" = "Success";
"common_suggestions" = "Suggestions";
"common_topic" = "Topic";
"common_unable_to_decrypt" = "Unable to decrypt";
"common_unsupported_event" = "Unsupported event";
"common_username" = "Username";
"common_verification_cancelled" = "Verification cancelled";
"common_verification_complete" = "Verification complete";
"common_video" = "Video";
"common_waiting" = "Waiting…";
"crash_detection_dialog_content" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?";
"dialog_title_confirmation" = "Confirmation";
"dialog_title_error" = "Error";
"dialog_title_success" = "Success";
"dialog_title_warning" = "Warning";
"emoji_picker_category_activity" = "Activities";
"emoji_picker_category_flags" = "Flags";
"emoji_picker_category_foods" = "Food & Drink";
"emoji_picker_category_nature" = "Animals & Nature";
"emoji_picker_category_objects" = "Objects";
"emoji_picker_category_people" = "Smileys & People";
"emoji_picker_category_places" = "Travel & Places";
"emoji_picker_category_symbols" = "Symbols";
"error_failed_creating_the_permalink" = "Failed creating the permalink";
"error_failed_loading_messages" = "Failed loading messages";
"error_no_compatible_app_found" = "No compatible app was found to handle this action.";
"error_some_messages_have_not_been_sent" = "Some messages have not been sent";
"error_unknown" = "Sorry, an error occurred";
"invite_friends_text" = "Hey, talk to me on %1$@: %2$@";
"leave_room_alert_empty_subtitle" = "Are you sure that you want to leave this room? You are the only person here. If you leave, no one will be able to join in the future, including you.";
"leave_room_alert_private_subtitle" = "Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite.";
"leave_room_alert_subtitle" = "Are you sure that you want to leave the room?";
"login_initial_device_name_ios" = "%1$@ iOS";
"preference_rageshake" = "Rageshake to report bug";
"rageshake_detection_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?";
"rageshake_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?";
"report_content_explanation" = "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages.";
"report_content_hint" = "Reason for reporting this content";
"rich_text_editor_bullet_list" = "Toggle bullet list";
"rich_text_editor_code_block" = "Toggle code block";
"rich_text_editor_composer_placeholder" = "Message…";
"rich_text_editor_format_bold" = "Apply bold format";
"rich_text_editor_format_italic" = "Apply italic format";
"rich_text_editor_format_strikethrough" = "Apply strikethrough format";
"rich_text_editor_format_underline" = "Apply underline format";
"rich_text_editor_full_screen_toggle" = "Toggle full screen mode";
"rich_text_editor_indent" = "Indent";
"rich_text_editor_inline_code" = "Apply inline code format";
"rich_text_editor_link" = "Set link";
"rich_text_editor_numbered_list" = "Toggle numbered list";
"rich_text_editor_quote" = "Toggle quote";
"rich_text_editor_unindent" = "Unindent";
"room_timeline_beginning_of_room" = "This is the beginning of %1$@.";
"room_timeline_beginning_of_room_no_name" = "This is the beginning of this conversation.";
"room_timeline_read_marker_title" = "New";
"screen_bug_report_attach_screenshot" = "Attach screenshot";
"screen_bug_report_contact_me" = "You may contact me if you have any follow up questions";
"screen_bug_report_edit_screenshot" = "Edit screenshot";
"screen_bug_report_editor_description" = "Please describe the bug. What did you do? What did you expect to happen? What actually happened. Please go into as much detail as you can.";
"screen_bug_report_editor_placeholder" = "Describe the bug…";
"screen_bug_report_editor_supporting" = "If possible, please write the description in English.";
"screen_bug_report_include_crash_logs" = "Send crash logs";
"screen_bug_report_include_logs" = "Send logs to help";
"screen_bug_report_include_screenshot" = "Send screenshot";
"screen_bug_report_logs_description" = "To check things work as intended, logs will be sent with your message. These will be private. To just send your message, turn off this setting.";
"screen_bug_report_rash_logs_alert_title" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?";
"screen_change_server_error_invalid_homeserver" = "We couldn't reach this homeserver. Please check that you have entered the homeserver URL correctly. If the URL is correct, contact your homeserver administrator for further help.";
"screen_change_server_error_no_sliding_sync_message" = "This server currently doesn’t support sliding sync.";
"screen_change_server_form_header" = "Homeserver URL";
"screen_change_server_form_notice" = "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$@";
"screen_change_server_submit" = "Continue";
"screen_change_server_subtitle" = "What is the address of your server?";
"screen_change_server_title" = "Select your server";
"screen_create_room_action_create_room" = "New room";
"screen_create_room_action_invite_people" = "Invite people";
"screen_create_room_add_people_title" = "Add people";
"screen_dm_details_block_alert_action" = "Block";
"screen_dm_details_block_alert_description" = "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.";
"screen_dm_details_block_user" = "Block user";
"screen_dm_details_unblock_alert_action" = "Unblock";
"screen_dm_details_unblock_alert_description" = "On unblocking the user, you will be able to see all messages by them again.";
"screen_dm_details_unblock_user" = "Unblock user";
"screen_login_error_deactivated_account" = "This account has been deactivated.";
"screen_login_error_invalid_credentials" = "Incorrect username and/or password";
"screen_login_error_invalid_user_id" = "This is not a valid user identifier. Expected format: ‘@user:homeserver.org’";
"screen_login_error_unsupported_authentication" = "The selected homeserver doesn't support password or OIDC login. Please contact your admin or choose another homeserver.";
"screen_login_form_header" = "Enter your details";
"screen_login_password_hint" = "Password";
"screen_login_server_header" = "Where your conversations live";
"screen_login_submit" = "Continue";
"screen_login_title" = "Welcome back!";
"screen_login_username_hint" = "Username";
"screen_onboarding_welcome_subtitle" = "Welcome to the %1$@ Beta. Supercharged, for speed and simplicity.";
"screen_onboarding_welcome_title" = "Be in your Element";
"screen_report_content_block_user" = "Block user";
"screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user";
"screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them.";
"screen_room_details_encryption_enabled_title" = "Message encryption enabled";
"screen_room_details_invite_people_title" = "Invite people";
"screen_room_details_leave_room_title" = "Leave room";
"screen_room_details_people_title" = "People";
"screen_room_details_security_title" = "Security";
"screen_room_details_share_room_title" = "Share room";
"screen_room_details_topic_title" = "Topic";
"screen_room_member_details_block_alert_action" = "Block";
"screen_room_member_details_block_alert_description" = "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.";
"screen_room_member_details_block_user" = "Block user";
"screen_room_member_details_unblock_alert_action" = "Unblock";
"screen_room_member_details_unblock_alert_description" = "On unblocking the user, you will be able to see all messages by them again.";
"screen_room_member_details_unblock_user" = "Unblock user";
"screen_roomlist_a11y_create_message" = "Create a new conversation or room";
"screen_roomlist_main_space_title" = "All Chats";
"screen_session_verification_cancelled_subtitle" = "Something doesn’t seem right. Either the request timed out or the request was denied.";
"screen_session_verification_cancelled_title" = "Verification cancelled";
"screen_session_verification_compare_emojis_subtitle" = "Confirm that the emojis below match those shown on your other session.";
"screen_session_verification_compare_emojis_title" = "Compare emojis";
"screen_session_verification_complete_subtitle" = "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.";
"screen_session_verification_open_existing_session_subtitle" = "Prove it’s you in order to access your encrypted message history.";
"screen_session_verification_open_existing_session_title" = "Open an existing session";
"screen_session_verification_positive_button_canceled" = "Retry verification";
"screen_session_verification_positive_button_initial" = "I am ready";
"screen_session_verification_positive_button_ready" = "Start";
"screen_session_verification_positive_button_verifying_ongoing" = "Waiting to match";
"screen_session_verification_request_accepted_subtitle" = "Compare the unique emoji, ensuring they appear in the same order.";
"screen_session_verification_they_dont_match" = "They don’t match";
"screen_session_verification_they_match" = "They match";
"screen_session_verification_waiting_to_accept_subtitle" = "Accept the request to start the verification process in your other session to continue.";
"screen_session_verification_waiting_to_accept_title" = "Waiting to accept request";
"screen_signout_confirmation_dialog_content" = "Are you sure you want to sign out?";
"screen_signout_confirmation_dialog_submit" = "Sign out";
"screen_signout_confirmation_dialog_title" = "Sign out";
"screen_signout_in_progress_dialog_content" = "Signing out…";
"screen_signout_preference_item" = "Sign out";
"screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat";
"session_verification_banner_message" = "Looks like you’re using a new device. Verify it’s you to access your encrypted messages.";
"session_verification_banner_title" = "Access your message history";
"settings_rageshake" = "Rageshake";
"settings_rageshake_detection_threshold" = "Detection threshold";
"settings_title_general" = "General";
"settings_version_number" = "Version: %1$@ (%2$@)";
"state_event_avatar_changed_too" = "(avatar was changed too)";
"state_event_avatar_url_changed" = "%1$@ changed their avatar";
"state_event_avatar_url_changed_by_you" = "You changed your avatar";
"state_event_display_name_changed_from" = "%1$@ changed their display name from %2$@ to %3$@";
"state_event_display_name_changed_from_by_you" = "You changed your display name from %1$@ to %2$@";
"state_event_display_name_removed" = "%1$@ removed their display name (it was %2$@)";
"state_event_display_name_removed_by_you" = "You removed your display name (it was %1$@)";
"state_event_display_name_set" = "%1$@ set their display name to %2$@";
"state_event_display_name_set_by_you" = "You set your display name to %1$@";
"state_event_room_avatar_changed" = "%1$@ changed the room avatar";
"state_event_room_avatar_changed_by_you" = "You changed the room avatar";
"state_event_room_avatar_removed" = "%1$@ removed the room avatar";
"state_event_room_avatar_removed_by_you" = "You removed the room avatar";
"state_event_room_ban" = "%1$@ banned %2$@";
"state_event_room_ban_by_you" = "You banned %1$@";
"state_event_room_created" = "%1$@ created the room";
"state_event_room_created_by_you" = "You created the room";
"state_event_room_invite" = "%1$@ invited %2$@";
"state_event_room_invite_accepted" = "%1$@ accepted the invite";
"state_event_room_invite_accepted_by_you" = "You accepted the invite";
"state_event_room_invite_by_you" = "You invited %1$@";
"state_event_room_invite_you" = "%1$@ invited you";
"state_event_room_join" = "%1$@ joined the room";
"state_event_room_join_by_you" = "You joined the room";
"state_event_room_knock" = "%1$@ requested to join";
"state_event_room_knock_accepted" = "%1$@ allowed %2$@ to join";
"state_event_room_knock_accepted_by_you" = "%1$@ allowed you to join";
"state_event_room_knock_by_you" = "You requested to join";
"state_event_room_knock_denied" = "%1$@ rejected %2$@'s request to join";
"state_event_room_knock_denied_by_you" = "You rejected %1$@'s request to join";
"state_event_room_knock_denied_you" = "%1$@ rejected your request to join";
"state_event_room_knock_retracted" = "%1$@ is no longer interested in joining";
"state_event_room_knock_retracted_by_you" = "You cancelled your request to join";
"state_event_room_leave" = "%1$@ left the room";
"state_event_room_leave_by_you" = "You left the room";
"state_event_room_name_changed" = "%1$@ changed the room name to: %2$@";
"state_event_room_name_changed_by_you" = "You changed the room name to: %1$@";
"state_event_room_name_removed" = "%1$@ removed the room name";
"state_event_room_name_removed_by_you" = "You removed the room name";
"state_event_room_reject" = "%1$@ rejected the invitation";
"state_event_room_reject_by_you" = "You rejected the invitation";
"state_event_room_remove" = "%1$@ removed %2$@";
"state_event_room_remove_by_you" = "You removed %1$@";
"state_event_room_third_party_invite" = "%1$@ sent an invitation to %2$@ to join the room";
"state_event_room_third_party_invite_by_you" = "You sent an invitation to %1$@ to join the room";
"state_event_room_third_party_revoked_invite" = "%1$@ revoked the invitation for %2$@ to join the room";
"state_event_room_third_party_revoked_invite_by_you" = "You revoked the invitation for %1$@ to join the room";
"state_event_room_topic_changed" = "%1$@ changed the topic to: %2$@";
"state_event_room_topic_changed_by_you" = "You changed the topic to: %1$@";
"state_event_room_topic_removed" = "%1$@ removed the room topic";
"state_event_room_topic_removed_by_you" = "You removed the room topic";
"state_event_room_unban" = "%1$@ unbanned %2$@";
"state_event_room_unban_by_you" = "You unbanned %1$@";
"state_event_room_unknown_membership_change" = "%1$@ made an unknown change to their membership";
@@ -0,0 +1,276 @@
"Notification" = "Notificare";
"a11y_hide_password" = "Ascunde parola";
"a11y_send_files" = "Trimite fișiere";
"a11y_show_password" = "Arata parola";
"a11y_user_menu" = "Meniu utilizator";
"action_back" = "Înapoi";
"action_cancel" = "Anulează";
"action_clear" = "Șterge";
"action_close" = "Închide";
"action_complete_verification" = "Verificare completă";
"action_confirm" = "Confirmă";
"action_continue" = "Continua";
"action_copy" = "Copiază";
"action_copy_link" = "Copiază linkul";
"action_create_a_room" = "Crează o cameră";
"action_disable" = "Dezactivează";
"action_done" = "Gata";
"action_edit" = "Editează";
"action_enable" = "Activează";
"action_invite" = "Invită";
"action_invite_friends_to_app" = "Invită prieteni în %1$@";
"action_learn_more" = "Află mai multe";
"action_leave" = "Părăsește";
"action_leave_room" = "Părăsește camera";
"action_next" = "Următorul";
"action_no" = "Nu";
"action_not_now" = "Nu acum";
"action_ok" = "OK";
"action_quick_reply" = "Raspuns rapid";
"action_quote" = "Citează";
"action_remove" = "Șterge";
"action_reply" = "Răspunde";
"action_report_bug" = "Raportează o eroare";
"action_report_content" = "Raportează conținutul";
"action_retry" = "Reîncearcă";
"action_retry_decryption" = "Reîncearcă decriptarea";
"action_save" = "Salvează";
"action_search" = "Caută";
"action_send" = "Trimite";
"action_share_link" = "Partajează linkul";
"action_skip" = "Omite";
"action_start" = "Începe";
"action_start_chat" = "Începe discuția";
"action_start_verification" = "Începe verificarea";
"action_view_source" = "Vezi sursa";
"action_yes" = "Da";
"common_about" = "Despre";
"common_audio" = "Audio";
"common_bubbles" = "Baloane";
"common_creating_room" = "Se creează camera…";
"common_decryption_error" = "Eroare de decriptare";
"common_developer_options" = "Opțiuni programator";
"common_edited_suffix" = "(editat)";
"common_editing" = "Editare";
"common_encryption_enabled" = "Criptare activată";
"common_error" = "Eroare";
"common_file" = "Fişier";
"common_gif" = "GIF";
"common_image" = "Imagine";
"common_link_copied_to_clipboard" = "Linkul a fost copiat în clipboard";
"common_loading" = "Se încarcă…";
"common_message" = "Mesaj";
"common_message_layout" = "Aranjamentul mesajelor";
"common_message_removed" = "Mesaj sters";
"common_modern" = "Modern";
"common_no_results" = "Niciun rezultat";
"common_offline" = "Deconectat";
"common_password" = "Parola";
"common_people" = "Persoane";
"common_permalink" = "Permalink";
"common_reactions" = "Reacții";
"common_replying_to" = "Răspuns pentru %1$@";
"common_report_a_bug" = "Raporteaza o eroare";
"common_report_submitted" = "Raport trimis";
"common_search_for_someone" = "Caută pe cineva";
"common_security" = "Securitate";
"common_select_your_server" = "Selectează serverul";
"common_sending" = "Se trimite…";
"common_server_not_supported" = "Serverul nu este compatibil";
"common_server_url" = "Adresa URL a serverului";
"common_settings" = "Setări";
"common_sticker" = "Autocolant";
"common_success" = "Succes";
"common_suggestions" = "Sugestii";
"common_topic" = "Subiect";
"common_unable_to_decrypt" = "Nu s-a putut decripta";
"common_unsupported_event" = "Eveniment neacceptat";
"common_username" = "Utilizator";
"common_verification_cancelled" = "Verificare anulată";
"common_verification_complete" = "Verificare completă";
"common_video" = "Video";
"common_waiting" = "Se aşteaptă…";
"crash_detection_dialog_content" = "%1$@ s-a blocat ultima dată când a fost folosit. Dorești să ne trimiti un raport?";
"dialog_title_confirmation" = "Confirmare";
"dialog_title_error" = "Eroare";
"dialog_title_success" = "Succes";
"dialog_title_warning" = "Avertisment";
"emoji_picker_category_activity" = "Activități";
"emoji_picker_category_flags" = "Steaguri";
"emoji_picker_category_foods" = "Mâncare & Băutură";
"emoji_picker_category_nature" = "Animale și Natură";
"emoji_picker_category_objects" = "Obiecte";
"emoji_picker_category_people" = "Fețe zâmbitoare & Oameni";
"emoji_picker_category_places" = "Călătorii & Locuri";
"emoji_picker_category_symbols" = "Simboluri";
"error_failed_creating_the_permalink" = "Crearea permalink-ului a eșuat";
"error_failed_loading_messages" = "Încărcarea mesajelor a eșuat";
"error_no_compatible_app_found" = "Nu a fost găsită nicio aplicație capabilă să gestioneze această acțiune.";
"error_some_messages_have_not_been_sent" = "Unele mesaje nu au fost trimise";
"error_unknown" = "Ne pare rău, a apărut o eroare";
"invite_friends_text" = "Hei, vorbește cu mine pe %1$@: %2$@";
"leave_room_alert_empty_subtitle" = "Ești sigur că vrei să părăsești această cameră? Ești singura persoană de aici. Dacă o părasești, nimeni nu se va mai putea alătura în viitor, inclusiv tu.";
"leave_room_alert_private_subtitle" = "Ești sigur că vrei să părăsești această cameră? Această cameră nu este publică și nu te vei putea alătura din nou fără o invitație.";
"leave_room_alert_subtitle" = "Ești sigur că vrei să părăsești camera?";
"login_initial_device_name_ios" = "%1$@ iOS";
"preference_rageshake" = "Rageshake pentru a raporta erori";
"rageshake_detection_dialog_content" = "Se pare că scuturi telefonul de frustrare. Dorești să deschizi ecranul de raportare a unei erori?";
"rageshake_dialog_content" = "Se pare că scuturi telefonul de frustrare. Dorești să deschizi ecranul de raportare a unei erori?";
"report_content_explanation" = "Acest mesaj va fi raportat administratorilor homeserver-ului tau. Ei nu vor putea citi niciun mesaj criptat.";
"report_content_hint" = "Motivul raportării acestui conținut";
"rich_text_editor_bullet_list" = "Comutează lista cu puncte";
"rich_text_editor_code_block" = "Comutează blocul de cod";
"rich_text_editor_composer_placeholder" = "Mesaj…";
"rich_text_editor_format_bold" = "Aplică formatul aldin";
"rich_text_editor_format_italic" = "Aplică formatul italic";
"rich_text_editor_format_strikethrough" = "Aplică formatul barat";
"rich_text_editor_format_underline" = "Aplică formatul de subliniere";
"rich_text_editor_full_screen_toggle" = "Comută modul ecran intreg";
"rich_text_editor_indent" = "Indentare";
"rich_text_editor_inline_code" = "Aplică formatul de cod in linie";
"rich_text_editor_link" = "Setați linkul";
"rich_text_editor_numbered_list" = "Comută lista numerotată";
"rich_text_editor_quote" = "Aplică citatul";
"rich_text_editor_unindent" = "Dez-identare";
"room_timeline_beginning_of_room" = "Acesta este începutul conversației %1$@.";
"room_timeline_beginning_of_room_no_name" = "Acesta este începutul acestei conversații.";
"room_timeline_read_marker_title" = "Nou";
"screen_bug_report_attach_screenshot" = "Atașează o captură de ecran";
"screen_bug_report_contact_me" = "Puteți să mă contactați dacă aveți întrebări suplimentare";
"screen_bug_report_edit_screenshot" = "Editează captura de ecran";
"screen_bug_report_editor_description" = "Te rugăm să descrii eroarea. Ce-ai făcut? Ce te aşteptai să se întâmple? Ce s-a întâmplat de fapt. Te rugam să intri în cât mai multe detalii cu putință.";
"screen_bug_report_editor_placeholder" = "Descrie eroarea...";
"screen_bug_report_editor_supporting" = "Dacă posibil, te rugăm să scrii descrierea în engleză.";
"screen_bug_report_include_crash_logs" = "Trimite log-uri";
"screen_bug_report_include_logs" = "Trimite log-uri pentru a ajuta";
"screen_bug_report_include_screenshot" = "Trimite captură de ecran";
"screen_bug_report_logs_description" = "Pentru a verifica că lucrurile funcționează conform așteptărilor, log-uri vor fi trimise împreună cu mesajul. Acestea vor fi private. Pentru a trimite doar mesajul, dezactivați această setare.";
"screen_bug_report_rash_logs_alert_title" = "%1$@ s-a blocat ultima dată când a fost folosit. Dorești să ne trimiti un raport?";
"screen_change_server_error_invalid_homeserver" = "Nu am putut accesa acest homeserver. Te rugăm să verifici că ai introdus corect adresa URL a homeserver-ului. Dacă adresa URL este corectă, contactează administratorul homeserver-ului pentru ajutor suplimentar.";
"screen_change_server_error_no_sliding_sync_message" = "Momentan acest server nu oferă suport pentru sliding sync.";
"screen_change_server_form_header" = "Adresa URL a homeserver-ului";
"screen_change_server_form_notice" = "Te poți conecta numai la un server existent care oferă suport pentru sliding sync. Administratorul homeserver-ului tău va trebui să îl configureze. %1$@";
"screen_change_server_submit" = "Continuă";
"screen_change_server_subtitle" = "Care este adresa serverului tău?";
"screen_change_server_title" = "Selectează serverul";
"screen_create_room_action_create_room" = "Cameră nouă";
"screen_create_room_action_invite_people" = "Invită persoane";
"screen_create_room_add_people_title" = "Adaugă persoane";
"screen_dm_details_block_alert_action" = "Blochează";
"screen_dm_details_block_alert_description" = "Utilizatorii blocați nu iți vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Poți anula această acțiune oricând.";
"screen_dm_details_block_user" = "Blochează utilizatorul";
"screen_dm_details_unblock_alert_action" = "Deblochează";
"screen_dm_details_unblock_alert_description" = "La deblocarea utilizatorului, vei putea vedea din nou toate mesajele de la acesta.";
"screen_dm_details_unblock_user" = "Deblochează utilizatorul";
"screen_login_error_deactivated_account" = "Acest cont a fost dezactivat.";
"screen_login_error_invalid_credentials" = "Utilizator și/sau parolă incorecte";
"screen_login_error_invalid_user_id" = "Acesta nu este un identificator de utilizator valid. Format așteptat: „@user:homeserver.org”";
"screen_login_error_unsupported_authentication" = "Homeserver-ul selectat nu acceptă autentificarea prin parola sau OIDC. Te rugăm să contactezi administratorul sau să alegi un alt homeserver.";
"screen_login_form_header" = "Introdu-ți detaliile";
"screen_login_password_hint" = "Parolă";
"screen_login_server_header" = "Locul unde trăiesc conversațiile tale";
"screen_login_submit" = "Continuă";
"screen_login_title" = "Bine ai revenit!";
"screen_login_username_hint" = "Utilizator";
"screen_onboarding_welcome_subtitle" = "Bun venit la versiunea beta a %1$@. Supraalimentat, pentru viteză și simplitate.";
"screen_onboarding_welcome_title" = "Fii în Elementul tău";
"screen_report_content_block_user" = "Blochează utilizatorul";
"screen_report_content_block_user_hint" = "Verifică că dorești să ascunzi toate mesajele curente și viitoare de la acest utilizator";
"screen_room_details_encryption_enabled_subtitle" = "Mesajele sunt securizate cu încuietori. Doar tu și destinatarii aveți cheile unice pentru a le debloca.";
"screen_room_details_encryption_enabled_title" = "Criptarea mesajelor este activată";
"screen_room_details_invite_people_title" = "Invită persoane";
"screen_room_details_leave_room_title" = "Părăsește camera";
"screen_room_details_people_title" = "Persoane";
"screen_room_details_security_title" = "Securitate";
"screen_room_details_share_room_title" = "Partajează camera";
"screen_room_details_topic_title" = "Subiect";
"screen_room_member_details_block_alert_action" = "Blochează";
"screen_room_member_details_block_alert_description" = "Utilizatorii blocați nu iți vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Poți anula această acțiune oricând.";
"screen_room_member_details_block_user" = "Blocați utilizatorul";
"screen_room_member_details_unblock_alert_action" = "Deblochează";
"screen_room_member_details_unblock_alert_description" = "La deblocarea utilizatorului, vei putea vedea din nou toate mesajele de la acesta.";
"screen_room_member_details_unblock_user" = "Deblochează utilizatorul";
"screen_roomlist_a11y_create_message" = "Crează o conversație sau o cameră nouă";
"screen_roomlist_main_space_title" = "Toate conversatiile";
"screen_session_verification_cancelled_subtitle" = "Ceva nu este în regulă. Fie cererea a expirat, fie a fost respinsă.";
"screen_session_verification_cancelled_title" = "Verificare anulată";
"screen_session_verification_compare_emojis_subtitle" = "Confirmă că emoticoanele de mai jos se potrivesc cu cele afișate în cealaltă sesiune.";
"screen_session_verification_compare_emojis_title" = "Compară emoticoanele";
"screen_session_verification_complete_subtitle" = "Noua ta sesiune este acum verificată. Are acces la mesajele tale criptate, iar alți utilizatori îl te vor vedea ca fiind de încredere.";
"screen_session_verification_open_existing_session_subtitle" = "Demonstreaza-ți identitatea pentru a accesa istoricul mesajelor tale criptate.";
"screen_session_verification_open_existing_session_title" = "Deschide o sesiune existentă";
"screen_session_verification_positive_button_canceled" = "Reîncearcă verificarea";
"screen_session_verification_positive_button_initial" = "Sunt pregătit";
"screen_session_verification_positive_button_ready" = "Începe";
"screen_session_verification_positive_button_verifying_ongoing" = "Se așteaptă confirmarea";
"screen_session_verification_request_accepted_subtitle" = "Compară emoticoalene asigurându-te că apar în aceeași ordine.";
"screen_session_verification_they_dont_match" = "Nu se potrivesc";
"screen_session_verification_they_match" = "Se potrivesc";
"screen_session_verification_waiting_to_accept_subtitle" = "Acceptă solicitarea de a începe procesul de verificare în cealaltă sesiune pentru a continua.";
"screen_session_verification_waiting_to_accept_title" = "Se așteptă acceptarea cererii";
"screen_signout_confirmation_dialog_content" = "Ești sigur că vrei să te deconectezi?";
"screen_signout_confirmation_dialog_submit" = "Deconectează-te";
"screen_signout_confirmation_dialog_title" = "Deconectează-te";
"screen_signout_in_progress_dialog_content" = "Deconectare în curs...";
"screen_signout_preference_item" = "Deconectează-te";
"screen_start_chat_error_starting_chat" = "A apărut o eroare la încercarea începerii conversației";
"session_verification_banner_message" = "Se pare că folosești un dispozitiv nou. Verifică-ți identitatea pentru acces la mesajele tale criptate.";
"session_verification_banner_title" = "Accesează istoricul mesajelor";
"settings_rageshake" = "Rageshake";
"settings_rageshake_detection_threshold" = "Prag de detecție";
"settings_title_general" = "General";
"settings_version_number" = "Versiunea: %1$@ (%2$@)";
"state_event_avatar_changed_too" = "(s-a schimbat si avatarul)";
"state_event_avatar_url_changed" = "%1$@ și-a schimbat avatarul";
"state_event_avatar_url_changed_by_you" = "Ți-ai schimbat avatarul";
"state_event_display_name_changed_from" = "%1$@ și-a schimbat numele din %2$@ în %3$@";
"state_event_display_name_changed_from_by_you" = "Ți-ai schimbat numele din %1$@ în %2$@";
"state_event_display_name_removed" = "%1$@ și-a sters numele (era %2$@)";
"state_event_display_name_removed_by_you" = "Ți-ai sters numele (era %1$@)";
"state_event_display_name_set" = "%1$@ și-a schimbat numele %2$@";
"state_event_display_name_set_by_you" = "Ți-ai schimbat numele în %1$@";
"state_event_room_avatar_changed" = "%1$@ a schimbat avatarul camerei";
"state_event_room_avatar_changed_by_you" = "Ai schimbat avatarul camerei";
"state_event_room_avatar_removed" = "%1$@ a șters avatarul camerei";
"state_event_room_avatar_removed_by_you" = "Ai șters avatarul camerei";
"state_event_room_ban" = "%1$@ a adăugat o interdicție pentru %2$@";
"state_event_room_ban_by_you" = "Ai adăugat o interdicție pentru %1$@";
"state_event_room_created" = "%1$@ a creat camera";
"state_event_room_created_by_you" = "Ai creat camera";
"state_event_room_invite" = "%1$@ l-a invitat pe %2$@";
"state_event_room_invite_accepted" = "%1$@ a acceptat invitația";
"state_event_room_invite_accepted_by_you" = "Ai acceptat invitația";
"state_event_room_invite_by_you" = "L-ai invitat pe %1$@";
"state_event_room_invite_you" = "%1$@ te-a invitat";
"state_event_room_join" = "%1$@ a intrat în cameră";
"state_event_room_join_by_you" = "Ai intrat în cameră";
"state_event_room_knock" = "%1$@ a solicitat să se alăture camerei";
"state_event_room_knock_accepted" = "%1$@ i-a permis lui %2$@ să se alăture camerei";
"state_event_room_knock_accepted_by_you" = "%1$@ ți-a permis să te alături camerei";
"state_event_room_knock_by_you" = "Ai solicitat să te alături camerei";
"state_event_room_knock_denied" = "%1$@ a respins solicitarea de alăturare a lui %2$@";
"state_event_room_knock_denied_by_you" = "Ai respins solicitarea de alăturare a lui %1$@";
"state_event_room_knock_denied_you" = "%1$@ a respins cererea ta de alăturare";
"state_event_room_knock_retracted" = "%1$@ nu mai este interesat să se alăture camerei";
"state_event_room_knock_retracted_by_you" = "Ți-ai anulat cererea de alăturare";
"state_event_room_leave" = "%1$@ a părăsit camera";
"state_event_room_leave_by_you" = "Ai părăsit camera";
"state_event_room_name_changed" = "%1$@ a schimbat numele camerei în: %2$@";
"state_event_room_name_changed_by_you" = "Ai schimbat numele camerei în: %1$@";
"state_event_room_name_removed" = "%1$@ a sters numele camerei";
"state_event_room_name_removed_by_you" = "Ai șters numele camerei";
"state_event_room_reject" = "%1$@ a respins invitația";
"state_event_room_reject_by_you" = "Ai respins invitația";
"state_event_room_remove" = "%1$@ l-a îndepărtat pe %2$@";
"state_event_room_remove_by_you" = "L-ai îndepărtat pe %1$@";
"state_event_room_third_party_invite" = "%1$@ a trimis o invitație către %2$@ pentru a se alătura camerei";
"state_event_room_third_party_invite_by_you" = "Ai trimis o invitație către %1$@ pentru a se alătura camerei";
"state_event_room_third_party_revoked_invite" = "%1$@ a revocat invitația pentru %2$@ de a se alătura camerei";
"state_event_room_third_party_revoked_invite_by_you" = "Ai revocat invitația pentru %1$@ de a se alătura camerei";
"state_event_room_topic_changed" = "%1$@ a schimbat subiectul în: %2$@";
"state_event_room_topic_changed_by_you" = "Ai schimbat subiectul în: %1$@";
"state_event_room_topic_removed" = "%1$@ a șters subiectul camerei";
"state_event_room_topic_removed_by_you" = "Ai șters subiectul camerei";
"state_event_room_unban" = "%1$@ a anulat interdicția pentru %2$@";
"state_event_room_unban_by_you" = "Ai anulat interdicția pentru %1$@";
"state_event_room_unknown_membership_change" = "%1$@ a făcut o modificare necunoscută asupra calității sale de membru";
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>common_member_count</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>%1$d membru</string>
<key>few</key>
<string></string>
<key>other</key>
<string>%1$d membri</string>
</dict>
</dict>
<key>room_timeline_state_changes</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>%1$d schimbare a camerii</string>
<key>few</key>
<string></string>
<key>other</key>
<string>%1$d schimbări ale camerei</string>
</dict>
</dict>
<key>screen_room_member_list_header_title</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>o persoană</string>
<key>few</key>
<string></string>
<key>other</key>
<string>%1$d persoane</string>
</dict>
</dict>
</dict>
</plist>
+23 -1
View File
@@ -88,7 +88,7 @@ public enum L10n {
public static var actionSearch: String { return L10n.tr("Localizable", "action_search") }
/// Send
public static var actionSend: String { return L10n.tr("Localizable", "action_send") }
/// Share link
/// Share
public static var actionShareLink: String { return L10n.tr("Localizable", "action_share_link") }
/// Skip
public static var actionSkip: String { return L10n.tr("Localizable", "action_skip") }
@@ -108,6 +108,8 @@ public enum L10n {
public static var commonAudio: String { return L10n.tr("Localizable", "common_audio") }
/// Bubbles
public static var commonBubbles: String { return L10n.tr("Localizable", "common_bubbles") }
/// Creating room…
public static var commonCreatingRoom: String { return L10n.tr("Localizable", "common_creating_room") }
/// Decryption error
public static var commonDecryptionError: String { return L10n.tr("Localizable", "common_decryption_error") }
/// Developer options
@@ -230,6 +232,8 @@ public enum L10n {
public static var errorFailedCreatingThePermalink: String { return L10n.tr("Localizable", "error_failed_creating_the_permalink") }
/// Failed loading messages
public static var errorFailedLoadingMessages: String { return L10n.tr("Localizable", "error_failed_loading_messages") }
/// No compatible app was found to handle this action.
public static var errorNoCompatibleAppFound: String { return L10n.tr("Localizable", "error_no_compatible_app_found") }
/// Some messages have not been sent
public static var errorSomeMessagesHaveNotBeenSent: String { return L10n.tr("Localizable", "error_some_messages_have_not_been_sent") }
/// Sorry, an error occurred
@@ -346,6 +350,18 @@ public enum L10n {
public static var screenCreateRoomActionInvitePeople: String { return L10n.tr("Localizable", "screen_create_room_action_invite_people") }
/// Add people
public static var screenCreateRoomAddPeopleTitle: String { return L10n.tr("Localizable", "screen_create_room_add_people_title") }
/// Block
public static var screenDmDetailsBlockAlertAction: String { return L10n.tr("Localizable", "screen_dm_details_block_alert_action") }
/// Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.
public static var screenDmDetailsBlockAlertDescription: String { return L10n.tr("Localizable", "screen_dm_details_block_alert_description") }
/// Block user
public static var screenDmDetailsBlockUser: String { return L10n.tr("Localizable", "screen_dm_details_block_user") }
/// Unblock
public static var screenDmDetailsUnblockAlertAction: String { return L10n.tr("Localizable", "screen_dm_details_unblock_alert_action") }
/// On unblocking the user, you will be able to see all messages by them again.
public static var screenDmDetailsUnblockAlertDescription: String { return L10n.tr("Localizable", "screen_dm_details_unblock_alert_description") }
/// Unblock user
public static var screenDmDetailsUnblockUser: String { return L10n.tr("Localizable", "screen_dm_details_unblock_user") }
/// This account has been deactivated.
public static var screenLoginErrorDeactivatedAccount: String { return L10n.tr("Localizable", "screen_login_error_deactivated_account") }
/// Incorrect username and/or password
@@ -388,6 +404,8 @@ public enum L10n {
public static var screenRoomDetailsPeopleTitle: String { return L10n.tr("Localizable", "screen_room_details_people_title") }
/// Security
public static var screenRoomDetailsSecurityTitle: String { return L10n.tr("Localizable", "screen_room_details_security_title") }
/// Share room
public static var screenRoomDetailsShareRoomTitle: String { return L10n.tr("Localizable", "screen_room_details_share_room_title") }
/// Topic
public static var screenRoomDetailsTopicTitle: String { return L10n.tr("Localizable", "screen_room_details_topic_title") }
/// Block
@@ -402,6 +420,10 @@ public enum L10n {
public static var screenRoomMemberDetailsUnblockAlertDescription: String { return L10n.tr("Localizable", "screen_room_member_details_unblock_alert_description") }
/// Unblock user
public static var screenRoomMemberDetailsUnblockUser: String { return L10n.tr("Localizable", "screen_room_member_details_unblock_user") }
/// Plural format key: "%#@COUNT@"
public static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String {
return L10n.tr("Localizable", "screen_room_member_list_header_title", p1)
}
/// Create a new conversation or room
public static var screenRoomlistA11yCreateMessage: String { return L10n.tr("Localizable", "screen_roomlist_a11y_create_message") }
/// All Chats
@@ -166,6 +166,11 @@ class RoomProxyMock: RoomProxyProtocol {
var displayName: String?
var topic: String?
var avatarURL: URL?
var membersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never> {
get { return underlyingMembersPublisher }
set(value) { underlyingMembersPublisher = value }
}
var underlyingMembersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never>!
//MARK: - loadAvatarURLForUserId
@@ -410,23 +415,6 @@ class RoomProxyMock: RoomProxyProtocol {
return reportContentReasonReturnValue
}
}
//MARK: - members
var membersCallsCount = 0
var membersCalled: Bool {
return membersCallsCount > 0
}
var membersReturnValue: Result<[RoomMemberProxyProtocol], RoomProxyError>!
var membersClosure: (() async -> Result<[RoomMemberProxyProtocol], RoomProxyError>)?
func members() async -> Result<[RoomMemberProxyProtocol], RoomProxyError> {
membersCallsCount += 1
if let membersClosure = membersClosure {
return await membersClosure()
} else {
return membersReturnValue
}
}
//MARK: - ignoreUser
var ignoreUserCallsCount = 0
@@ -481,6 +469,18 @@ class RoomProxyMock: RoomProxyProtocol {
return leaveRoomReturnValue
}
}
//MARK: - updateMembers
var updateMembersCallsCount = 0
var updateMembersCalled: Bool {
return updateMembersCallsCount > 0
}
var updateMembersClosure: (() async -> Void)?
func updateMembers() async {
updateMembersCallsCount += 1
await updateMembersClosure?()
}
}
class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol {
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> {
+7 -5
View File
@@ -14,6 +14,7 @@
// limitations under the License.
//
import Combine
import Foundation
struct RoomProxyMockConfiguration {
@@ -51,11 +52,12 @@ extension RoomProxyMock {
alternativeAliases = configuration.alternativeAliases
hasUnreadNotifications = configuration.hasUnreadNotifications
membersClosure = {
if let members = configuration.members {
return .success(members)
}
return .failure(.failedRetrievingMembers)
if let members = configuration.members {
membersPublisher = Just(members).eraseToAnyPublisher()
} else {
membersPublisher = Just([]).eraseToAnyPublisher()
}
updateMembersClosure = { }
}
}
@@ -79,6 +79,7 @@ struct A11yIdentifiers {
struct RoomDetailsScreen {
let avatar = "room_details-avatar"
let dmAvatar = "room_details-dm_avatar"
let people = "room_details-people"
}
@@ -0,0 +1,72 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct AvatarHeaderView<Footer: View>: View {
let avatarUrl: URL?
let name: String?
let id: String
let avatarSize: AvatarSize
let imageProvider: ImageProviderProtocol?
let subtitle: String?
@ViewBuilder var footer: () -> Footer
var body: some View {
VStack(spacing: 8.0) {
LoadableAvatarImage(url: avatarUrl,
name: name,
contentID: id,
avatarSize: avatarSize,
imageProvider: imageProvider)
Text(name ?? id)
.foregroundColor(.element.primaryContent)
.font(.element.title1Bold)
.multilineTextAlignment(.center)
if let subtitle {
Text(subtitle)
.foregroundColor(.element.secondaryContent)
.font(.element.body)
.multilineTextAlignment(.center)
}
footer()
}
.frame(maxWidth: .infinity, alignment: .center)
.listRowBackground(Color.clear)
}
}
struct HeaderView_Previews: PreviewProvider {
static var previews: some View {
AvatarHeaderView(avatarUrl: URL.picturesDirectory,
name: "Test",
id: "test",
avatarSize: .room(on: .details),
imageProvider: MockMediaProvider(),
subtitle: "test") {
HStack(spacing: 32) {
ShareLink(item: "test") {
Image(systemName: "square.and.arrow.up")
}
.buttonStyle(FormActionButtonStyle(title: "Test"))
}
.padding(.top, 32)
}
}
}
@@ -38,19 +38,65 @@ struct RoomDetailsViewState: BindableState {
var topic: String?
var avatarURL: URL?
let permalink: URL?
var members: [RoomDetailsMember]
var members: [RoomMemberDetails] = []
var isProcessingIgnoreRequest = false
var isLoadingMembers: Bool {
members.isEmpty
}
var bindings: RoomDetailsViewStateBindings
var dmRecipient: RoomMemberDetails?
private var isDMRoom: Bool {
isEncrypted && isDirect && members.count == 2
}
}
struct RoomDetailsViewStateBindings {
struct IgnoreUserAlertItem: AlertItem, Equatable {
enum Action {
case ignore
case unignore
}
let action: Action
let cancelTitle = L10n.actionCancel
var title: String {
switch action {
case .ignore: return L10n.screenDmDetailsBlockUser
case .unignore: return L10n.screenDmDetailsUnblockUser
}
}
var confirmationTitle: String {
switch action {
case .ignore: return L10n.screenDmDetailsBlockAlertAction
case .unignore: return L10n.screenDmDetailsUnblockAlertAction
}
}
var description: String {
switch action {
case .ignore: return L10n.screenDmDetailsBlockAlertDescription
case .unignore: return L10n.screenDmDetailsUnblockAlertDescription
}
}
var viewAction: RoomDetailsViewAction {
switch action {
case .ignore: return .ignoreConfirmed
case .unignore: return .unignoreConfirmed
}
}
}
/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<RoomDetailsErrorType>?
var leaveRoomAlertItem: LeaveRoomAlertItem?
var ignoreUserRoomAlertItem: IgnoreUserAlertItem?
}
struct LeaveRoomAlertItem: AlertItem {
@@ -77,21 +123,11 @@ struct LeaveRoomAlertItem: AlertItem {
enum RoomDetailsViewAction {
case processTapPeople
case processTapLeave
case processTapIgnore
case processTapUnignore
case confirmLeave
case copyRoomLink
}
struct RoomDetailsMember: Identifiable, Equatable {
let id: String
let name: String?
let avatarURL: URL?
@MainActor
init(withProxy proxy: RoomMemberProxyProtocol) {
id = proxy.userID
name = proxy.displayName
avatarURL = proxy.avatarURL
}
case ignoreConfirmed
case unignoreConfirmed
}
enum RoomDetailsErrorType: Hashable {
@@ -14,6 +14,7 @@
// limitations under the License.
//
import Combine
import SwiftUI
typealias RoomDetailsViewModelType = StateStoreViewModel<RoomDetailsViewState, RoomDetailsViewAction>
@@ -22,12 +23,27 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
private let roomProxy: RoomProxyProtocol
private var members: [RoomMemberProxyProtocol] = [] {
didSet {
state.members = members.map { RoomDetailsMember(withProxy: $0) }
state.members = members.map { RoomMemberDetails(withProxy: $0) }
if roomProxy.isDirect, roomProxy.isEncrypted, members.count == 2 {
dmRecipient = members.first(where: { !$0.isAccountOwner })
} else {
dmRecipient = nil
}
}
}
private var dmRecipient: RoomMemberProxyProtocol? {
didSet {
if let dmRecipient {
state.dmRecipient = RoomMemberDetails(withProxy: dmRecipient)
} else {
state.dmRecipient = nil
}
}
}
var callback: ((RoomDetailsViewModelAction) -> Void)?
init(roomProxy: RoomProxyProtocol,
mediaProvider: MediaProviderProtocol) {
self.roomProxy = roomProxy
@@ -39,18 +55,16 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
topic: roomProxy.topic,
avatarURL: roomProxy.avatarURL,
permalink: roomProxy.permalink,
members: [],
bindings: .init()),
imageProvider: mediaProvider)
roomProxy.membersPublisher.sink { [weak self] members in
self?.members = members
}
.store(in: &cancellables)
Task {
switch await roomProxy.members() {
case .success(let members):
self.members = members
case .failure(let error):
MXLog.error("Failed retrieving room members: \(error)")
state.bindings.alertInfo = AlertInfo(id: .alert(L10n.errorUnknown))
}
await roomProxy.updateMembers()
}
}
@@ -60,8 +74,6 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
switch viewAction {
case .processTapPeople:
callback?(.requestMemberDetailsPresentation(members))
case .copyRoomLink:
copyRoomLink()
case .processTapLeave:
guard members.count > 1 else {
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(state: .empty)
@@ -70,21 +82,20 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(state: roomProxy.isPublic ? .public : .private)
case .confirmLeave:
await leaveRoom()
case .processTapIgnore:
state.bindings.ignoreUserRoomAlertItem = .init(action: .ignore)
case .processTapUnignore:
state.bindings.ignoreUserRoomAlertItem = .init(action: .unignore)
case .ignoreConfirmed:
await ignore()
case .unignoreConfirmed:
await unignore()
}
}
// MARK: - Private
private static let leaveRoomLoadingID = "LeaveRoomLoading"
private func copyRoomLink() {
if let roomLink = state.permalink {
UIPasteboard.general.url = roomLink
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(title: L10n.commonLinkCopiedToClipboard))
} else {
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
}
}
private func leaveRoom() async {
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.leaveRoomLoadingID, type: .modal, title: L10n.commonLoading, persistent: true))
@@ -97,4 +108,28 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
callback?(.leftRoom)
}
}
private func ignore() async {
state.isProcessingIgnoreRequest = true
let result = await dmRecipient?.ignoreUser()
state.isProcessingIgnoreRequest = false
switch result {
case .success:
state.dmRecipient?.isIgnored = true
case .failure, .none:
state.bindings.alertInfo = .init(id: .unknown)
}
}
private func unignore() async {
state.isProcessingIgnoreRequest = true
let result = await dmRecipient?.unignoreUser()
state.isProcessingIgnoreRequest = false
switch result {
case .success:
state.dmRecipient?.isIgnored = false
case .failure, .none:
state.bindings.alertInfo = .init(id: .unknown)
}
}
}
@@ -21,17 +21,27 @@ struct RoomDetailsScreen: View {
var body: some View {
Form {
headerSection
if let recipient = context.viewState.dmRecipient {
dmHeaderSection(recipient: recipient)
} else {
normalRoomHeaderSection
}
topicSection
aboutSection
if context.viewState.dmRecipient == nil {
aboutSection
}
if context.viewState.isEncrypted {
securitySection
}
leaveRoomSection
if let recipient = context.viewState.dmRecipient {
ignoreUserSection(user: recipient)
} else {
leaveRoomSection
}
}
.scrollContentBackground(.hidden)
.background(Color.element.formBackground.ignoresSafeArea())
@@ -39,48 +49,53 @@ struct RoomDetailsScreen: View {
.alert(item: $context.leaveRoomAlertItem,
actions: leaveRoomAlertActions,
message: leaveRoomAlertMessage)
.alert(item: $context.ignoreUserRoomAlertItem,
actions: blockUserAlertActions,
message: blockUserAlertMessage)
}
// MARK: - Private
private var headerSection: some View {
VStack(spacing: 8.0) {
LoadableAvatarImage(url: context.viewState.avatarURL,
name: context.viewState.title,
contentID: context.viewState.roomId,
avatarSize: .room(on: .details),
imageProvider: context.imageProvider)
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.avatar)
Text(context.viewState.title)
.foregroundColor(.element.primaryContent)
.font(.element.title1Bold)
.multilineTextAlignment(.center)
if let canonicalAlias = context.viewState.canonicalAlias {
Text(canonicalAlias)
.foregroundColor(.element.secondaryContent)
.font(.element.body)
.multilineTextAlignment(.center)
}
@ViewBuilder
private var normalRoomHeaderSection: some View {
AvatarHeaderView(avatarUrl: context.viewState.avatarURL,
name: context.viewState.title,
id: context.viewState.roomId,
avatarSize: .room(on: .details),
imageProvider: context.imageProvider,
subtitle: context.viewState.canonicalAlias) {
if let permalink = context.viewState.permalink {
HStack(spacing: 32) {
Button { context.send(viewAction: .copyRoomLink) } label: {
Image(systemName: "link")
}
.buttonStyle(FormActionButtonStyle(title: L10n.actionCopyLink))
ShareLink(item: permalink) {
Image(systemName: "square.and.arrow.up")
}
.buttonStyle(FormActionButtonStyle(title: L10n.actionInvite))
.buttonStyle(FormActionButtonStyle(title: L10n.actionShareLink))
}
.padding(.top, 32)
}
}
.frame(maxWidth: .infinity, alignment: .center)
.listRowBackground(Color.clear)
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.avatar)
}
@ViewBuilder
private func dmHeaderSection(recipient: RoomMemberDetails) -> some View {
AvatarHeaderView(avatarUrl: recipient.avatarURL,
name: recipient.name,
id: recipient.id,
avatarSize: .user(on: .memberDetails),
imageProvider: context.imageProvider,
subtitle: recipient.id) {
if let permalink = recipient.permalink {
HStack(spacing: 32) {
ShareLink(item: permalink) {
Image(systemName: "square.and.arrow.up")
}
.buttonStyle(FormActionButtonStyle(title: L10n.actionShareLink))
}
.padding(.top, 32)
}
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.dmAvatar)
}
@ViewBuilder
@@ -158,6 +173,21 @@ struct RoomDetailsScreen: View {
.formSectionStyle()
}
@ViewBuilder
private func ignoreUserSection(user: RoomMemberDetails) -> some View {
Section {
Button(role: user.isIgnored ? nil : .destructive) {
context.send(viewAction: user.isIgnored ? .processTapUnignore : .processTapIgnore)
} label: {
Label(user.isIgnored ? L10n.screenDmDetailsUnblockUser : L10n.screenDmDetailsBlockUser,
systemImage: "slash.circle")
}
.buttonStyle(FormButtonStyle(accessory: context.viewState.isProcessingIgnoreRequest ? .progressView : nil))
.disabled(context.viewState.isProcessingIgnoreRequest)
}
.formSectionStyle()
}
@ViewBuilder
private func leaveRoomAlertActions(_ item: LeaveRoomAlertItem) -> some View {
Button(item.cancelTitle, role: .cancel) { }
@@ -169,12 +199,25 @@ struct RoomDetailsScreen: View {
private func leaveRoomAlertMessage(_ item: LeaveRoomAlertItem) -> some View {
Text(item.subtitle)
}
@ViewBuilder
private func blockUserAlertActions(_ item: RoomDetailsViewStateBindings.IgnoreUserAlertItem) -> some View {
Button(item.cancelTitle, role: .cancel) { }
Button(item.confirmationTitle,
role: item.action == .ignore ? .destructive : nil) {
context.send(viewAction: item.viewAction)
}
}
private func blockUserAlertMessage(_ item: RoomDetailsViewStateBindings.IgnoreUserAlertItem) -> some View {
Text(item.description)
}
}
// MARK: - Previews
struct RoomDetails_Previews: PreviewProvider {
static let viewModel = {
static let genericRoomViewModel = {
let members: [RoomMemberProxyMock] = [
.mockAlice,
.mockBob,
@@ -190,8 +233,28 @@ struct RoomDetails_Previews: PreviewProvider {
return RoomDetailsViewModel(roomProxy: roomProxy,
mediaProvider: MockMediaProvider())
}()
static let dmRoomViewModel = {
let members: [RoomMemberProxyMock] = [
.mockMe,
.mockDan
]
let roomProxy = RoomProxyMock(with: .init(displayName: "DM Room",
topic: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
isDirect: true,
isEncrypted: true,
canonicalAlias: "#alias:domain.com",
members: members))
return RoomDetailsViewModel(roomProxy: roomProxy,
mediaProvider: MockMediaProvider())
}()
static var previews: some View {
RoomDetailsScreen(context: viewModel.context)
RoomDetailsScreen(context: genericRoomViewModel.context)
.previewDisplayName("Generic Room")
RoomDetailsScreen(context: dmRoomViewModel.context)
.previewDisplayName("DM Room")
}
}
@@ -19,64 +19,58 @@ import Foundation
enum RoomMemberDetailsViewModelAction { }
struct RoomMemberDetailsViewState: BindableState {
let userID: String
let name: String?
let avatarURL: URL?
let isAccountOwner: Bool
let permalink: URL?
var isIgnored: Bool
var details: RoomMemberDetails
var isProcessingIgnoreRequest = false
var bindings: RoomMemberDetailsViewStateBindings
}
struct RoomMemberDetailsViewStateBindings {
struct IgnoreUserAlertItem: AlertItem, Equatable {
enum Action {
case ignore
case unignore
}
let action: Action
let cancelTitle = L10n.actionCancel
var title: String {
switch action {
case .ignore: return L10n.screenRoomMemberDetailsBlockUser
case .unignore: return L10n.screenRoomMemberDetailsUnblockUser
}
}
var confirmationTitle: String {
switch action {
case .ignore: return L10n.screenRoomMemberDetailsBlockAlertAction
case .unignore: return L10n.screenRoomMemberDetailsUnblockAlertAction
}
}
var description: String {
switch action {
case .ignore: return L10n.screenRoomMemberDetailsBlockAlertDescription
case .unignore: return L10n.screenRoomMemberDetailsUnblockAlertDescription
}
}
var viewAction: RoomMemberDetailsViewAction {
switch action {
case .ignore: return .ignoreConfirmed
case .unignore: return .unignoreConfirmed
}
}
}
var ignoreUserAlert: IgnoreUserAlertItem?
var errorAlert: ErrorAlertItem?
}
struct IgnoreUserAlertItem: AlertItem, Equatable {
enum Action {
case ignore
case unignore
}
let action: Action
let cancelTitle = L10n.actionCancel
var title: String {
switch action {
case .ignore: return L10n.screenRoomMemberDetailsBlockUser
case .unignore: return L10n.screenRoomMemberDetailsUnblockUser
}
}
var confirmationTitle: String {
switch action {
case .ignore: return L10n.screenRoomMemberDetailsBlockAlertAction
case .unignore: return L10n.screenRoomMemberDetailsUnblockAlertAction
}
}
var description: String {
switch action {
case .ignore: return L10n.screenRoomMemberDetailsBlockAlertDescription
case .unignore: return L10n.screenRoomMemberDetailsUnblockAlertDescription
}
}
var viewAction: RoomMemberDetailsViewAction {
switch action {
case .ignore: return .ignoreConfirmed
case .unignore: return .unignoreConfirmed
}
}
}
enum RoomMemberDetailsViewAction {
case showUnignoreAlert
case showIgnoreAlert
case ignoreConfirmed
case unignoreConfirmed
case copyUserLink
}
@@ -25,12 +25,7 @@ class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDeta
init(roomMemberProxy: RoomMemberProxyProtocol, mediaProvider: MediaProviderProtocol) {
self.roomMemberProxy = roomMemberProxy
let initialViewState = RoomMemberDetailsViewState(userID: roomMemberProxy.userID,
name: roomMemberProxy.displayName,
avatarURL: roomMemberProxy.avatarURL,
isAccountOwner: roomMemberProxy.isAccountOwner,
permalink: roomMemberProxy.permalink,
isIgnored: roomMemberProxy.isIgnored,
let initialViewState = RoomMemberDetailsViewState(details: RoomMemberDetails(withProxy: roomMemberProxy),
bindings: .init())
super.init(initialViewState: initialViewState, imageProvider: mediaProvider)
}
@@ -43,8 +38,6 @@ class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDeta
state.bindings.ignoreUserAlert = .init(action: .unignore)
case .showIgnoreAlert:
state.bindings.ignoreUserAlert = .init(action: .ignore)
case .copyUserLink:
copyUserLink()
case .ignoreConfirmed:
await ignoreUser()
case .unignoreConfirmed:
@@ -54,22 +47,13 @@ class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDeta
// MARK: - Private
private func copyUserLink() {
if let userLink = state.permalink {
UIPasteboard.general.url = userLink
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(title: L10n.commonLinkCopiedToClipboard))
} else {
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
}
}
private func ignoreUser() async {
state.isProcessingIgnoreRequest = true
let result = await roomMemberProxy.ignoreUser()
state.isProcessingIgnoreRequest = false
switch result {
case .success:
state.isIgnored = true
state.details.isIgnored = true
case .failure:
state.bindings.errorAlert = .init()
}
@@ -81,7 +65,7 @@ class RoomMemberDetailsViewModel: RoomMemberDetailsViewModelType, RoomMemberDeta
state.isProcessingIgnoreRequest = false
switch result {
case .success:
state.isIgnored = false
state.details.isIgnored = false
case .failure:
state.bindings.errorAlert = .init()
}
@@ -23,7 +23,7 @@ struct RoomMemberDetailsScreen: View {
Form {
headerSection
if !context.viewState.isAccountOwner {
if !context.viewState.details.isAccountOwner {
blockUserSection
}
}
@@ -35,31 +35,16 @@ struct RoomMemberDetailsScreen: View {
// MARK: - Private
@ViewBuilder
private var headerSection: some View {
VStack(spacing: 8.0) {
LoadableAvatarImage(url: context.viewState.avatarURL,
name: context.viewState.name,
contentID: context.viewState.userID,
avatarSize: .user(on: .memberDetails),
imageProvider: context.imageProvider)
if let name = context.viewState.name {
Text(name)
.foregroundColor(.element.primaryContent)
.font(.element.title1Bold)
.multilineTextAlignment(.center)
}
Text(context.viewState.userID)
.foregroundColor(.element.secondaryContent)
.font(.element.body)
.multilineTextAlignment(.center)
if let permalink = context.viewState.permalink {
AvatarHeaderView(avatarUrl: context.viewState.details.avatarURL,
name: context.viewState.details.name,
id: context.viewState.details.id,
avatarSize: .user(on: .memberDetails),
imageProvider: context.imageProvider,
subtitle: context.viewState.details.id) {
if let permalink = context.viewState.details.permalink {
HStack(spacing: 32) {
Button { context.send(viewAction: .copyUserLink) } label: {
Image(systemName: "link")
}
.buttonStyle(FormActionButtonStyle(title: L10n.actionCopyLink))
ShareLink(item: permalink) {
Image(systemName: "square.and.arrow.up")
}
@@ -68,13 +53,11 @@ struct RoomMemberDetailsScreen: View {
.padding(.top, 32)
}
}
.frame(maxWidth: .infinity, alignment: .center)
.listRowBackground(Color.clear)
}
private var blockUserSection: some View {
Section {
Button {
Button(role: context.viewState.details.isIgnored ? nil : .destructive) {
context.send(viewAction: blockUserButtonAction)
} label: {
Label(blockUserButtonTitle, systemImage: "slash.circle")
@@ -87,19 +70,19 @@ struct RoomMemberDetailsScreen: View {
}
private var blockUserButtonAction: RoomMemberDetailsViewAction {
context.viewState.isIgnored ? .showUnignoreAlert : .showIgnoreAlert
context.viewState.details.isIgnored ? .showUnignoreAlert : .showIgnoreAlert
}
private var blockUserButtonTitle: String {
context.viewState.isIgnored ? L10n.screenRoomMemberDetailsUnblockUser : L10n.screenRoomMemberDetailsBlockUser
context.viewState.details.isIgnored ? L10n.screenRoomMemberDetailsUnblockUser : L10n.screenRoomMemberDetailsBlockUser
}
private var blockUserButtonAccessibilityIdentifier: String {
context.viewState.isIgnored ? A11yIdentifiers.roomMemberDetailsScreen.unignore : A11yIdentifiers.roomMemberDetailsScreen.ignore
context.viewState.details.isIgnored ? A11yIdentifiers.roomMemberDetailsScreen.unignore : A11yIdentifiers.roomMemberDetailsScreen.ignore
}
@ViewBuilder
private func blockUserAlertActions(_ item: IgnoreUserAlertItem) -> some View {
private func blockUserAlertActions(_ item: RoomMemberDetailsViewStateBindings.IgnoreUserAlertItem) -> some View {
Button(item.cancelTitle, role: .cancel) { }
Button(item.confirmationTitle,
role: item.action == .ignore ? .destructive : nil) {
@@ -107,7 +90,7 @@ struct RoomMemberDetailsScreen: View {
}
}
private func blockUserAlertMessage(_ item: IgnoreUserAlertItem) -> some View {
private func blockUserAlertMessage(_ item: RoomMemberDetailsViewStateBindings.IgnoreUserAlertItem) -> some View {
Text(item.description)
}
}
@@ -21,11 +21,11 @@ enum RoomMembersListViewModelAction {
}
struct RoomMembersListViewState: BindableState {
var members: [RoomDetailsMember]
var members: [RoomMemberDetails]
var bindings: RoomMembersListViewStateBindings
var visibleMembers: [RoomDetailsMember] {
var visibleMembers: [RoomMemberDetails] {
if bindings.searchQuery.isEmpty {
return members
}
@@ -28,7 +28,7 @@ class RoomMembersListViewModel: RoomMembersListViewModelType, RoomMembersListVie
members: [RoomMemberProxyProtocol]) {
self.mediaProvider = mediaProvider
self.members = members
super.init(initialViewState: .init(members: members.map { RoomDetailsMember(withProxy: $0) },
super.init(initialViewState: .init(members: members.map { RoomMemberDetails(withProxy: $0) },
bindings: .init()),
imageProvider: mediaProvider)
}
@@ -19,7 +19,7 @@ import SwiftUI
struct RoomMembersListMemberCell: View {
@ScaledMetric private var avatarSize = AvatarSize.user(on: .roomDetails).value
let member: RoomDetailsMember
let member: RoomMemberDetails
let context: RoomMembersListViewModel.Context
var body: some View {
+12 -8
View File
@@ -34,6 +34,11 @@ class RoomProxy: RoomProxyProtocol {
private(set) var displayName: String?
private var timelineObservationToken: TaskHandle?
private let membersSubject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
var membersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never> {
membersSubject.eraseToAnyPublisher()
}
init(slidingSyncRoom: SlidingSyncRoomProtocol,
room: RoomProtocol,
@@ -140,6 +145,7 @@ class RoomProxy: RoomProxyProtocol {
timelineObservationToken = result.taskHandle
Task {
await fetchMembers()
await updateMembers()
}
return .success(result.items)
} else {
@@ -274,18 +280,16 @@ class RoomProxy: RoomProxyProtocol {
}
}
}
func members() async -> Result<[RoomMemberProxyProtocol], RoomProxyError> {
func updateMembers() async {
do {
let members = try await Task.dispatch(on: .global()) {
let members = try self.room.members()
return members
let roomMembers = try await Task.dispatch(on: .global()) {
try self.room.members()
}
let proxiedMembers = buildRoomMemberProxies(members: members)
return .success(proxiedMembers)
membersSubject.value = buildRoomMemberProxies(members: roomMembers)
} catch {
return .failure(.failedRetrievingMembers)
return
}
}
@@ -54,7 +54,9 @@ protocol RoomProxyProtocol {
var topic: String? { get }
var avatarURL: URL? { get }
var membersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never> { get }
func loadAvatarURLForUserId(_ userId: String) async -> Result<URL?, RoomProxyError>
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError>
@@ -79,13 +81,13 @@ protocol RoomProxyProtocol {
func reportContent(_ eventID: String, reason: String?) async -> Result<Void, RoomProxyError>
func members() async -> Result<[RoomMemberProxyProtocol], RoomProxyError>
func ignoreUser(_ userID: String) async -> Result<Void, RoomProxyError>
func retryDecryption(for sessionID: String) async
func leaveRoom() async -> Result<Void, RoomProxyError>
func updateMembers() async
}
extension RoomProxyProtocol {
@@ -0,0 +1,36 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
struct RoomMemberDetails: Identifiable, Equatable {
let id: String
let name: String?
let avatarURL: URL?
let permalink: URL?
let isAccountOwner: Bool
var isIgnored: Bool
@MainActor
init(withProxy proxy: RoomMemberProxyProtocol) {
id = proxy.userID
name = proxy.displayName
avatarURL = proxy.avatarURL
permalink = proxy.permalink
isAccountOwner = proxy.isAccountOwner
isIgnored = proxy.isIgnored
}
}
@@ -337,6 +337,20 @@ class MockScreen: Identifiable {
let coordinator = RoomMemberDetailsCoordinator(parameters: .init(roomMemberProxy: RoomMemberProxyMock.mockIgnored, mediaProvider: MockMediaProvider()))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomDetailsScreenDmDetails:
let navigationStackCoordinator = NavigationStackCoordinator()
let members: [RoomMemberProxyMock] = [.mockMe, .mockDan]
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
displayName: "Room",
topic: "test",
isDirect: true,
isEncrypted: true,
members: members))
let coordinator = RoomDetailsCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy,
mediaProvider: MockMediaProvider()))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
}
}()
}
@@ -42,6 +42,7 @@ enum UITestsScreenIdentifier: String {
case userSessionScreen
case roomDetailsScreen
case roomDetailsScreenWithRoomAvatar
case roomDetailsScreenDmDetails
case roomMembersListScreen
case roomMemberDetailsAccountOwner
case roomMemberDetails
@@ -33,4 +33,12 @@ class RoomDetailsScreenUITests: XCTestCase {
XCTAssert(app.buttons[A11yIdentifiers.roomDetailsScreen.people].waitForExistence(timeout: 1))
app.assertScreenshot(.roomDetailsScreenWithRoomAvatar)
}
func testInitialStateComponentsDmDetails() {
let app = Application.launch(.roomDetailsScreenDmDetails)
XCTAssert(app.images[A11yIdentifiers.roomDetailsScreen.dmAvatar].waitForExistence(timeout: 1))
XCTAssertFalse(app.buttons[A11yIdentifiers.roomDetailsScreen.people].exists)
app.assertScreenshot(.roomDetailsScreenDmDetails)
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:523804837d4ca7123635a59677e8e50abb4fc3e8bab0d17d817955ea736244b1
size 101550
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5d36a079c35934f91cfa81e1f2a567571cd0c66ff32babb3f83d8aa15ff3afb
size 122338
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4328de12a3aefba9bdc8bf52bcdc8c6587a1618452aa6499429d85a80634fe30
size 114902
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cb47ad82f2952d130e17fe3c99871e83782bc573f01317fcd55cf1ffbf53f0f0
size 154477
@@ -82,4 +82,94 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
XCTAssertEqual(roomProxyMock.leaveRoomCallsCount, 1)
XCTAssertNotNil(context.alertInfo)
}
func testInitialDMDetailsState() async {
let recipient = RoomMemberProxyMock.mockDan
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
}
func testIgnoreSuccess() async throws {
let recipient = RoomMemberProxyMock.mockDan
recipient.ignoreUserClosure = {
try? await Task.sleep(for: .milliseconds(10))
return .success(())
}
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
context.send(viewAction: .ignoreConfirmed)
await Task.yield()
XCTAssertTrue(context.viewState.isProcessingIgnoreRequest)
try await Task.sleep(for: .milliseconds(10))
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
XCTAssert(context.viewState.dmRecipient?.isIgnored == true)
}
func testIgnoreFailure() async throws {
let recipient = RoomMemberProxyMock.mockDan
recipient.ignoreUserClosure = {
try? await Task.sleep(for: .milliseconds(10))
return .failure(.ignoreUserFailed)
}
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
context.send(viewAction: .ignoreConfirmed)
await Task.yield()
XCTAssertTrue(context.viewState.isProcessingIgnoreRequest)
try await Task.sleep(for: .milliseconds(10))
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
XCTAssert(context.viewState.dmRecipient?.isIgnored == false)
XCTAssertNotNil(context.alertInfo)
}
func testUnignoreSuccess() async throws {
let recipient = RoomMemberProxyMock.mockIgnored
recipient.unignoreUserClosure = {
try? await Task.sleep(for: .milliseconds(10))
return .success(())
}
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
context.send(viewAction: .unignoreConfirmed)
await Task.yield()
XCTAssertTrue(context.viewState.isProcessingIgnoreRequest)
try await Task.sleep(for: .milliseconds(10))
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
XCTAssert(context.viewState.dmRecipient?.isIgnored == false)
}
func testUnignoreFailure() async throws {
let recipient = RoomMemberProxyMock.mockIgnored
recipient.unignoreUserClosure = {
try? await Task.sleep(for: .milliseconds(10))
return .failure(.unignoreUserFailed)
}
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
context.send(viewAction: .unignoreConfirmed)
await Task.yield()
XCTAssertTrue(context.viewState.isProcessingIgnoreRequest)
try await Task.sleep(for: .milliseconds(10))
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
XCTAssert(context.viewState.dmRecipient?.isIgnored == true)
XCTAssertNotNil(context.alertInfo)
}
}
@@ -28,12 +28,7 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
roomMemberProxyMock = RoomMemberProxyMock.mockAlice
viewModel = RoomMemberDetailsViewModel(roomMemberProxy: roomMemberProxyMock, mediaProvider: MockMediaProvider())
XCTAssertEqual(context.viewState.name, "Alice")
XCTAssertFalse(context.viewState.isAccountOwner)
XCTAssertFalse(context.viewState.isIgnored)
XCTAssertEqual(context.viewState.userID, "@alice:matrix.org")
XCTAssertEqual(context.viewState.permalink, URL(string: "https://matrix.to/#/@alice:matrix.org"))
XCTAssertEqual(context.viewState.avatarURL, nil)
XCTAssertEqual(context.viewState.details, RoomMemberDetails(withProxy: roomMemberProxyMock))
XCTAssertNil(context.ignoreUserAlert)
XCTAssertNil(context.errorAlert)
}
@@ -48,15 +43,15 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
context.send(viewAction: .showIgnoreAlert)
await Task.yield()
XCTAssertEqual(context.ignoreUserAlert, IgnoreUserAlertItem(action: .ignore))
XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore))
context.send(viewAction: .ignoreConfirmed)
await Task.yield()
XCTAssertTrue(context.viewState.isProcessingIgnoreRequest)
XCTAssertFalse(context.viewState.isIgnored)
XCTAssertFalse(context.viewState.details.isIgnored)
try await Task.sleep(for: .milliseconds(10))
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
XCTAssertTrue(context.viewState.isIgnored)
XCTAssertTrue(context.viewState.details.isIgnored)
}
func testIgnoreFailure() async throws {
@@ -69,16 +64,16 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
context.send(viewAction: .showIgnoreAlert)
await Task.yield()
XCTAssertEqual(context.ignoreUserAlert, IgnoreUserAlertItem(action: .ignore))
XCTAssertEqual(context.ignoreUserAlert, .init(action: .ignore))
context.send(viewAction: .ignoreConfirmed)
await Task.yield()
XCTAssertTrue(context.viewState.isProcessingIgnoreRequest)
XCTAssertFalse(context.viewState.isIgnored)
XCTAssertFalse(context.viewState.details.isIgnored)
try await Task.sleep(for: .milliseconds(10))
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
XCTAssertNotNil(context.errorAlert)
XCTAssertFalse(context.viewState.isIgnored)
XCTAssertFalse(context.viewState.details.isIgnored)
}
func testUnignoreSuccess() async throws {
@@ -91,15 +86,15 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
context.send(viewAction: .showUnignoreAlert)
await Task.yield()
XCTAssertEqual(context.ignoreUserAlert, IgnoreUserAlertItem(action: .unignore))
XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore))
context.send(viewAction: .unignoreConfirmed)
await Task.yield()
XCTAssertTrue(context.viewState.isProcessingIgnoreRequest)
XCTAssertTrue(context.viewState.isIgnored)
XCTAssertTrue(context.viewState.details.isIgnored)
try await Task.sleep(for: .milliseconds(10))
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
XCTAssertFalse(context.viewState.isIgnored)
XCTAssertFalse(context.viewState.details.isIgnored)
}
func testUnignoreFailure() async throws {
@@ -112,15 +107,15 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
context.send(viewAction: .showUnignoreAlert)
await Task.yield()
XCTAssertEqual(context.ignoreUserAlert, IgnoreUserAlertItem(action: .unignore))
XCTAssertEqual(context.ignoreUserAlert, .init(action: .unignore))
context.send(viewAction: .unignoreConfirmed)
await Task.yield()
XCTAssertTrue(context.viewState.isProcessingIgnoreRequest)
XCTAssertTrue(context.viewState.isIgnored)
XCTAssertTrue(context.viewState.details.isIgnored)
try await Task.sleep(for: .milliseconds(10))
XCTAssertFalse(context.viewState.isProcessingIgnoreRequest)
XCTAssertTrue(context.viewState.isIgnored)
XCTAssertTrue(context.viewState.details.isIgnored)
XCTAssertNotNil(context.errorAlert)
}
@@ -128,12 +123,7 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
roomMemberProxyMock = RoomMemberProxyMock.mockMe
viewModel = RoomMemberDetailsViewModel(roomMemberProxy: roomMemberProxyMock, mediaProvider: MockMediaProvider())
XCTAssertEqual(context.viewState.name, "Me")
XCTAssertTrue(context.viewState.isAccountOwner)
XCTAssertFalse(context.viewState.isIgnored)
XCTAssertEqual(context.viewState.userID, "@me:matrix.org")
XCTAssertEqual(context.viewState.permalink, URL(string: "https://matrix.to/#/@me:matrix.org"))
XCTAssertEqual(context.viewState.avatarURL, URL.picturesDirectory)
XCTAssertEqual(context.viewState.details, RoomMemberDetails(withProxy: roomMemberProxyMock))
XCTAssertNil(context.ignoreUserAlert)
XCTAssertNil(context.errorAlert)
}
@@ -142,12 +132,7 @@ class RoomMemberDetailsViewModelTests: XCTestCase {
roomMemberProxyMock = RoomMemberProxyMock.mockIgnored
viewModel = RoomMemberDetailsViewModel(roomMemberProxy: roomMemberProxyMock, mediaProvider: MockMediaProvider())
XCTAssertEqual(context.viewState.name, "Ignored")
XCTAssertFalse(context.viewState.isAccountOwner)
XCTAssertTrue(context.viewState.isIgnored)
XCTAssertEqual(context.viewState.userID, "@ignored:matrix.org")
XCTAssertEqual(context.viewState.permalink, URL(string: "https://matrix.to/#/@ignored:matrix.org"))
XCTAssertEqual(context.viewState.avatarURL, nil)
XCTAssertEqual(context.viewState.details, RoomMemberDetails(withProxy: roomMemberProxyMock))
XCTAssertNil(context.ignoreUserAlert)
XCTAssertNil(context.errorAlert)
}
+1
View File
@@ -0,0 +1 @@
Added DM Details View.