Invite again user on direct chats (#1087)

* Add leaveRoom section for DMs

* Add invite alert in RoomScreenViewModel

* Show alert on composer focus

* Add localisations

* Refine invite alert logics

* Amend tests

* Update project

* Fix local variable name

* Refactor show invite alert logic
This commit is contained in:
Alfonso Grillo
2023-06-16 15:36:27 +02:00
committed by GitHub
parent d2d626f724
commit 87b0d95e67
11 changed files with 127 additions and 22 deletions
@@ -22,6 +22,7 @@
"action_edit" = "Edit"; "action_edit" = "Edit";
"action_enable" = "Enable"; "action_enable" = "Enable";
"action_forgot_password" = "Forgot password?"; "action_forgot_password" = "Forgot password?";
"action_forward" = "Forward";
"action_invite" = "Invite"; "action_invite" = "Invite";
"action_invite_friends" = "Invite friends"; "action_invite_friends" = "Invite friends";
"action_invite_friends_to_app" = "Invite friends to %1$@"; "action_invite_friends_to_app" = "Invite friends to %1$@";
@@ -52,6 +53,7 @@
"action_start" = "Start"; "action_start" = "Start";
"action_start_chat" = "Start chat"; "action_start_chat" = "Start chat";
"action_start_verification" = "Start verification"; "action_start_verification" = "Start verification";
"action_static_map_load" = "Tap to load map";
"action_take_photo" = "Take photo"; "action_take_photo" = "Take photo";
"action_view_source" = "View Source"; "action_view_source" = "View Source";
"action_yes" = "Yes"; "action_yes" = "Yes";
@@ -71,6 +73,7 @@
"common_encryption_enabled" = "Encryption enabled"; "common_encryption_enabled" = "Encryption enabled";
"common_error" = "Error"; "common_error" = "Error";
"common_file" = "File"; "common_file" = "File";
"common_forward_message" = "Forward message";
"common_gif" = "GIF"; "common_gif" = "GIF";
"common_image" = "Image"; "common_image" = "Image";
"common_invite_unknown_profile" = "We can’t validate this user’s Matrix ID. The invite might not be received."; "common_invite_unknown_profile" = "We can’t validate this user’s Matrix ID. The invite might not be received.";
@@ -81,6 +84,7 @@
"common_message_layout" = "Message layout"; "common_message_layout" = "Message layout";
"common_message_removed" = "Message removed"; "common_message_removed" = "Message removed";
"common_modern" = "Modern"; "common_modern" = "Modern";
"common_mute" = "Mute";
"common_no_results" = "No results"; "common_no_results" = "No results";
"common_offline" = "Offline"; "common_offline" = "Offline";
"common_password" = "Password"; "common_password" = "Password";
@@ -111,6 +115,7 @@
"common_unable_to_decrypt" = "Unable to decrypt"; "common_unable_to_decrypt" = "Unable to decrypt";
"common_unable_to_invite_message" = "We were unable to successfully send invites to one or more users."; "common_unable_to_invite_message" = "We were unable to successfully send invites to one or more users.";
"common_unable_to_invite_title" = "Unable to send invite(s)"; "common_unable_to_invite_title" = "Unable to send invite(s)";
"common_unmute" = "Unmute";
"common_unsupported_event" = "Unsupported event"; "common_unsupported_event" = "Unsupported event";
"common_username" = "Username"; "common_username" = "Username";
"common_verification_cancelled" = "Verification cancelled"; "common_verification_cancelled" = "Verification cancelled";
@@ -146,9 +151,10 @@
"notification_inline_reply_failed" = "** Failed to send - please open room"; "notification_inline_reply_failed" = "** Failed to send - please open room";
"notification_invitation_action_join" = "Join"; "notification_invitation_action_join" = "Join";
"notification_invitation_action_reject" = "Reject"; "notification_invitation_action_reject" = "Reject";
"notification_invite_body" = "invited you"; "notification_invite_body" = "Invited you to chat";
"notification_new_messages" = "New Messages"; "notification_new_messages" = "New Messages";
"notification_room_action_mark_as_read" = "Mark as read"; "notification_room_action_mark_as_read" = "Mark as read";
"notification_room_invite_body" = "Invited you to join the room";
"notification_sender_me" = "Me"; "notification_sender_me" = "Me";
"notification_test_push_notification_content" = "You are viewing the notification! Click me!"; "notification_test_push_notification_content" = "You are viewing the notification! Click me!";
"notification_ticker_text_dm" = "%1$@: %2$@"; "notification_ticker_text_dm" = "%1$@: %2$@";
@@ -214,7 +220,6 @@
"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_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_subtitle" = "What is the address of your server?"; "screen_change_server_subtitle" = "What is the address of your server?";
"screen_create_room_action_create_room" = "New room"; "screen_create_room_action_create_room" = "New room";
"screen_create_room_action_invite_people" = "Invite friends to Element";
"screen_create_room_add_people_title" = "Invite people"; "screen_create_room_add_people_title" = "Invite people";
"screen_create_room_error_creating_room" = "An error occurred when creating the room"; "screen_create_room_error_creating_room" = "An error occurred when creating the room";
"screen_create_room_private_option_description" = "Messages in this room are encrypted. Encryption can’t be disabled afterwards."; "screen_create_room_private_option_description" = "Messages in this room are encrypted. Encryption can’t be disabled afterwards.";
@@ -265,6 +270,8 @@
"screen_room_details_share_room_title" = "Share room"; "screen_room_details_share_room_title" = "Share room";
"screen_room_details_updating_room" = "Updating room…"; "screen_room_details_updating_room" = "Updating room…";
"screen_room_error_failed_retrieving_user_details" = "Could not retrieve user details"; "screen_room_error_failed_retrieving_user_details" = "Could not retrieve user details";
"screen_room_invite_again_alert_message" = "Would you like to invite them back?";
"screen_room_invite_again_alert_title" = "You are alone in this chat";
"screen_room_member_details_block_alert_action" = "Block"; "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_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_block_user" = "Block user";
@@ -290,7 +290,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} }
private func dismissRoom(animated: Bool) { private func dismissRoom(animated: Bool) {
navigationStackCoordinator.popToRoot(animated: animated) // The room isn't in the same navigation stack of the home screen.
// Animating the popToRoot causes weird animations on when the room is left from room's details
navigationStackCoordinator.popToRoot(animated: false)
navigationSplitCoordinator.setDetailCoordinator(nil, animated: animated) navigationSplitCoordinator.setDetailCoordinator(nil, animated: animated)
roomProxy = nil roomProxy = nil
timelineController = nil timelineController = nil
+17 -3
View File
@@ -56,6 +56,8 @@ public enum L10n {
public static var actionEnable: String { return L10n.tr("Localizable", "action_enable") } public static var actionEnable: String { return L10n.tr("Localizable", "action_enable") }
/// Forgot password? /// Forgot password?
public static var actionForgotPassword: String { return L10n.tr("Localizable", "action_forgot_password") } public static var actionForgotPassword: String { return L10n.tr("Localizable", "action_forgot_password") }
/// Forward
public static var actionForward: String { return L10n.tr("Localizable", "action_forward") }
/// Invite /// Invite
public static var actionInvite: String { return L10n.tr("Localizable", "action_invite") } public static var actionInvite: String { return L10n.tr("Localizable", "action_invite") }
/// Invite friends /// Invite friends
@@ -118,6 +120,8 @@ public enum L10n {
public static var actionStartChat: String { return L10n.tr("Localizable", "action_start_chat") } public static var actionStartChat: String { return L10n.tr("Localizable", "action_start_chat") }
/// Start verification /// Start verification
public static var actionStartVerification: String { return L10n.tr("Localizable", "action_start_verification") } public static var actionStartVerification: String { return L10n.tr("Localizable", "action_start_verification") }
/// Tap to load map
public static var actionStaticMapLoad: String { return L10n.tr("Localizable", "action_static_map_load") }
/// Take photo /// Take photo
public static var actionTakePhoto: String { return L10n.tr("Localizable", "action_take_photo") } public static var actionTakePhoto: String { return L10n.tr("Localizable", "action_take_photo") }
/// View Source /// View Source
@@ -158,6 +162,8 @@ public enum L10n {
public static var commonError: String { return L10n.tr("Localizable", "common_error") } public static var commonError: String { return L10n.tr("Localizable", "common_error") }
/// File /// File
public static var commonFile: String { return L10n.tr("Localizable", "common_file") } public static var commonFile: String { return L10n.tr("Localizable", "common_file") }
/// Forward message
public static var commonForwardMessage: String { return L10n.tr("Localizable", "common_forward_message") }
/// GIF /// GIF
public static var commonGif: String { return L10n.tr("Localizable", "common_gif") } public static var commonGif: String { return L10n.tr("Localizable", "common_gif") }
/// Image /// Image
@@ -182,6 +188,8 @@ public enum L10n {
public static var commonMessageRemoved: String { return L10n.tr("Localizable", "common_message_removed") } public static var commonMessageRemoved: String { return L10n.tr("Localizable", "common_message_removed") }
/// Modern /// Modern
public static var commonModern: String { return L10n.tr("Localizable", "common_modern") } public static var commonModern: String { return L10n.tr("Localizable", "common_modern") }
/// Mute
public static var commonMute: String { return L10n.tr("Localizable", "common_mute") }
/// No results /// No results
public static var commonNoResults: String { return L10n.tr("Localizable", "common_no_results") } public static var commonNoResults: String { return L10n.tr("Localizable", "common_no_results") }
/// Offline /// Offline
@@ -244,6 +252,8 @@ public enum L10n {
public static var commonUnableToInviteMessage: String { return L10n.tr("Localizable", "common_unable_to_invite_message") } public static var commonUnableToInviteMessage: String { return L10n.tr("Localizable", "common_unable_to_invite_message") }
/// Unable to send invite(s) /// Unable to send invite(s)
public static var commonUnableToInviteTitle: String { return L10n.tr("Localizable", "common_unable_to_invite_title") } public static var commonUnableToInviteTitle: String { return L10n.tr("Localizable", "common_unable_to_invite_title") }
/// Unmute
public static var commonUnmute: String { return L10n.tr("Localizable", "common_unmute") }
/// Unsupported event /// Unsupported event
public static var commonUnsupportedEvent: String { return L10n.tr("Localizable", "common_unsupported_event") } public static var commonUnsupportedEvent: String { return L10n.tr("Localizable", "common_unsupported_event") }
/// Username /// Username
@@ -340,7 +350,7 @@ public enum L10n {
public static func notificationInvitations(_ p1: Int) -> String { public static func notificationInvitations(_ p1: Int) -> String {
return L10n.tr("Localizable", "notification_invitations", p1) return L10n.tr("Localizable", "notification_invitations", p1)
} }
/// invited you /// Invited you to chat
public static var notificationInviteBody: String { return L10n.tr("Localizable", "notification_invite_body") } public static var notificationInviteBody: String { return L10n.tr("Localizable", "notification_invite_body") }
/// New Messages /// New Messages
public static var notificationNewMessages: String { return L10n.tr("Localizable", "notification_new_messages") } public static var notificationNewMessages: String { return L10n.tr("Localizable", "notification_new_messages") }
@@ -352,6 +362,8 @@ public enum L10n {
public static var notificationRoomActionMarkAsRead: String { return L10n.tr("Localizable", "notification_room_action_mark_as_read") } public static var notificationRoomActionMarkAsRead: String { return L10n.tr("Localizable", "notification_room_action_mark_as_read") }
/// Quick reply /// Quick reply
public static var notificationRoomActionQuickReply: String { return L10n.tr("Localizable", "notification_room_action_quick_reply") } public static var notificationRoomActionQuickReply: String { return L10n.tr("Localizable", "notification_room_action_quick_reply") }
/// Invited you to join the room
public static var notificationRoomInviteBody: String { return L10n.tr("Localizable", "notification_room_invite_body") }
/// Me /// Me
public static var notificationSenderMe: String { return L10n.tr("Localizable", "notification_sender_me") } public static var notificationSenderMe: String { return L10n.tr("Localizable", "notification_sender_me") }
/// You are viewing the notification! Click me! /// You are viewing the notification! Click me!
@@ -538,8 +550,6 @@ public enum L10n {
public static var screenChangeServerTitle: String { return L10n.tr("Localizable", "screen_change_server_title") } public static var screenChangeServerTitle: String { return L10n.tr("Localizable", "screen_change_server_title") }
/// New room /// New room
public static var screenCreateRoomActionCreateRoom: String { return L10n.tr("Localizable", "screen_create_room_action_create_room") } public static var screenCreateRoomActionCreateRoom: String { return L10n.tr("Localizable", "screen_create_room_action_create_room") }
/// Invite friends to Element
public static var screenCreateRoomActionInvitePeople: String { return L10n.tr("Localizable", "screen_create_room_action_invite_people") }
/// Invite people /// Invite people
public static var screenCreateRoomAddPeopleTitle: String { return L10n.tr("Localizable", "screen_create_room_add_people_title") } public static var screenCreateRoomAddPeopleTitle: String { return L10n.tr("Localizable", "screen_create_room_add_people_title") }
/// An error occurred when creating the room /// An error occurred when creating the room
@@ -682,6 +692,10 @@ public enum L10n {
public static var screenRoomErrorFailedProcessingMedia: String { return L10n.tr("Localizable", "screen_room_error_failed_processing_media") } public static var screenRoomErrorFailedProcessingMedia: String { return L10n.tr("Localizable", "screen_room_error_failed_processing_media") }
/// Could not retrieve user details /// Could not retrieve user details
public static var screenRoomErrorFailedRetrievingUserDetails: String { return L10n.tr("Localizable", "screen_room_error_failed_retrieving_user_details") } public static var screenRoomErrorFailedRetrievingUserDetails: String { return L10n.tr("Localizable", "screen_room_error_failed_retrieving_user_details") }
/// Would you like to invite them back?
public static var screenRoomInviteAgainAlertMessage: String { return L10n.tr("Localizable", "screen_room_invite_again_alert_message") }
/// You are alone in this chat
public static var screenRoomInviteAgainAlertTitle: String { return L10n.tr("Localizable", "screen_room_invite_again_alert_title") }
/// Block /// Block
public static var screenRoomMemberDetailsBlockAlertAction: String { return L10n.tr("Localizable", "screen_room_member_details_block_alert_action") } public static var screenRoomMemberDetailsBlockAlertAction: String { return L10n.tr("Localizable", "screen_room_member_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. /// 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,9 +37,9 @@ struct RoomDetailsScreen: View {
if let recipient = context.viewState.dmRecipient { if let recipient = context.viewState.dmRecipient {
ignoreUserSection(user: recipient) ignoreUserSection(user: recipient)
} else {
leaveRoomSection
} }
leaveRoomSection
} }
.elementFormStyle() .elementFormStyle()
.alert(item: $context.alertInfo) .alert(item: $context.alertInfo)
@@ -117,6 +117,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
// MARK: - Private // MARK: - Private
// swiftlint:disable:next function_body_length
private func setupSubscriptions() { private func setupSubscriptions() {
timelineController.callbacks timelineController.callbacks
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
@@ -164,6 +165,31 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
} }
.weakAssign(to: \.state.members, on: self) .weakAssign(to: \.state.members, on: self)
.store(in: &cancellables) .store(in: &cancellables)
setupDirectRoomSubscriptionsIfNeeded()
}
private func setupDirectRoomSubscriptionsIfNeeded() {
guard roomProxy.isDirect else {
return
}
let shouldShowInviteAlert = context.$viewState
.map(\.bindings.composerFocused)
.removeDuplicates()
.map { [weak self] isFocused in
guard let self else { return false }
return isFocused && self.roomProxy.isUserAloneInDirectRoom
}
// We want to show the alert just once, so we are taking the first "true" emitted
.first { $0 }
shouldShowInviteAlert
.sink { [weak self] _ in
self?.showInviteAlert()
}
.store(in: &cancellables)
} }
private func paginateBackwards() async { private func paginateBackwards() async {
@@ -502,6 +528,57 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private func hideLoadingIndicator() { private func hideLoadingIndicator() {
userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
} }
// MARK: - Direct chats logics
private func showInviteAlert() {
userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenRoomInviteAgainAlertTitle,
message: L10n.screenRoomInviteAgainAlertMessage,
primaryButton: .init(title: L10n.actionInvite, action: { [weak self] in self?.inviteOtherDMUserBack() }),
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
}
private let inviteLoadingIndicatorID = UUID().uuidString
private func inviteOtherDMUserBack() {
guard roomProxy.isUserAloneInDirectRoom else {
userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError)
return
}
Task {
userIndicatorController.submitIndicator(.init(id: inviteLoadingIndicatorID, type: .toast, title: L10n.commonLoading))
defer {
userIndicatorController.retractIndicatorWithId(inviteLoadingIndicatorID)
}
guard
let members = await roomProxy.members(),
members.count == 2,
let otherPerson = members.first(where: { !$0.isAccountOwner && $0.membership == .leave })
else {
userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError)
return
}
switch await roomProxy.invite(userID: otherPerson.userID) {
case .success:
break
case .failure:
userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.commonUnableToInviteTitle,
message: L10n.commonUnableToInviteMessage)
}
}
}
}
private extension RoomProxyProtocol {
/// Checks if the other person left the room in a direct chat
var isUserAloneInDirectRoom: Bool {
isDirect && activeMembersCount == 1
}
} }
// MARK: - Mocks // MARK: - Mocks
@@ -167,4 +167,9 @@ extension RoomProxyProtocol {
var isEncryptedOneToOneRoom: Bool { var isEncryptedOneToOneRoom: Bool {
isDirect && isEncrypted && activeMembersCount == 2 isDirect && isEncrypted && activeMembersCount == 2
} }
func members() async -> [RoomMemberProxyProtocol]? {
await updateMembers()
return await membersPublisher.values.first()
}
} }
@@ -308,7 +308,7 @@ class MockScreen: Identifiable {
isEncrypted: true, isEncrypted: true,
members: members, members: members,
memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false), memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
joinedMembersCount: members.count)) activeMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com", let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator, navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy, roomProxy: roomProxy,
@@ -327,7 +327,7 @@ class MockScreen: Identifiable {
canonicalAlias: "#mock:room.org", canonicalAlias: "#mock:room.org",
members: members, members: members,
memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false), memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
joinedMembersCount: members.count)) activeMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com", let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator, navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy, roomProxy: roomProxy,
@@ -348,7 +348,7 @@ class MockScreen: Identifiable {
canonicalAlias: "#mock:room.org", canonicalAlias: "#mock:room.org",
members: members, members: members,
memberForID: owner, memberForID: owner,
joinedMembersCount: members.count)) activeMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com", let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator, navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy, roomProxy: roomProxy,
@@ -365,7 +365,7 @@ class MockScreen: Identifiable {
isEncrypted: true, isEncrypted: true,
members: members, members: members,
memberForID: owner, memberForID: owner,
joinedMembersCount: members.count)) activeMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com", let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator, navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy, roomProxy: roomProxy,
@@ -383,7 +383,7 @@ class MockScreen: Identifiable {
isEncrypted: true, isEncrypted: true,
members: members, members: members,
memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false), memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
joinedMembersCount: members.count)) activeMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com", let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator, navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy, roomProxy: roomProxy,
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:523804837d4ca7123635a59677e8e50abb4fc3e8bab0d17d817955ea736244b1 oid sha256:4ca554f9b25a8705b17082d5d8c251a83273b2cb021c0ac334ce0e002e76a076
size 101550 size 107039
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:99f9a46a131e99939687a82a67933d27e28aa6a2f0cbf43a7b445e132e914bf2 oid sha256:f13ecb7efed5879470031cc91dab1d8f09a4f468204589383902a4be4cf912dc
size 124688 size 134039
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4328de12a3aefba9bdc8bf52bcdc8c6587a1618452aa6499429d85a80634fe30 oid sha256:27c43edc3f2ee71699ea7159d845c9d78b5e1eac23bf9a999c9a81f2942e620d
size 114902 size 120590
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9aab159ec7d3a2abbf32e7beca660f56db0cc0aeaf5dd35f398633be6cbe8c19 oid sha256:3e706f3a8eb417b4d0fca086d612e5d2dcf362ae8ab49c299ea746d7047fdab9
size 152190 size 162199