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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 127 additions and 22 deletions

View File

@ -22,6 +22,7 @@
"action_edit" = "Edit";
"action_enable" = "Enable";
"action_forgot_password" = "Forgot password?";
"action_forward" = "Forward";
"action_invite" = "Invite";
"action_invite_friends" = "Invite friends";
"action_invite_friends_to_app" = "Invite friends to %1$@";
@ -52,6 +53,7 @@
"action_start" = "Start";
"action_start_chat" = "Start chat";
"action_start_verification" = "Start verification";
"action_static_map_load" = "Tap to load map";
"action_take_photo" = "Take photo";
"action_view_source" = "View Source";
"action_yes" = "Yes";
@ -71,6 +73,7 @@
"common_encryption_enabled" = "Encryption enabled";
"common_error" = "Error";
"common_file" = "File";
"common_forward_message" = "Forward message";
"common_gif" = "GIF";
"common_image" = "Image";
"common_invite_unknown_profile" = "We cant validate this users Matrix ID. The invite might not be received.";
@ -81,6 +84,7 @@
"common_message_layout" = "Message layout";
"common_message_removed" = "Message removed";
"common_modern" = "Modern";
"common_mute" = "Mute";
"common_no_results" = "No results";
"common_offline" = "Offline";
"common_password" = "Password";
@ -111,6 +115,7 @@
"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_title" = "Unable to send invite(s)";
"common_unmute" = "Unmute";
"common_unsupported_event" = "Unsupported event";
"common_username" = "Username";
"common_verification_cancelled" = "Verification cancelled";
@ -146,9 +151,10 @@
"notification_inline_reply_failed" = "** Failed to send - please open room";
"notification_invitation_action_join" = "Join";
"notification_invitation_action_reject" = "Reject";
"notification_invite_body" = "invited you";
"notification_invite_body" = "Invited you to chat";
"notification_new_messages" = "New Messages";
"notification_room_action_mark_as_read" = "Mark as read";
"notification_room_invite_body" = "Invited you to join the room";
"notification_sender_me" = "Me";
"notification_test_push_notification_content" = "You are viewing the notification! Click me!";
"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_subtitle" = "What is the address of your server?";
"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_error_creating_room" = "An error occurred when creating the room";
"screen_create_room_private_option_description" = "Messages in this room are encrypted. Encryption cant be disabled afterwards.";
@ -265,6 +270,8 @@
"screen_room_details_share_room_title" = "Share room";
"screen_room_details_updating_room" = "Updating room…";
"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_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";

View File

@ -290,7 +290,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
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)
roomProxy = nil
timelineController = nil

View File

@ -56,6 +56,8 @@ public enum L10n {
public static var actionEnable: String { return L10n.tr("Localizable", "action_enable") }
/// 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
public static var actionInvite: String { return L10n.tr("Localizable", "action_invite") }
/// Invite friends
@ -118,6 +120,8 @@ public enum L10n {
public static var actionStartChat: String { return L10n.tr("Localizable", "action_start_chat") }
/// 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
public static var actionTakePhoto: String { return L10n.tr("Localizable", "action_take_photo") }
/// View Source
@ -158,6 +162,8 @@ public enum L10n {
public static var commonError: String { return L10n.tr("Localizable", "common_error") }
/// 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
public static var commonGif: String { return L10n.tr("Localizable", "common_gif") }
/// Image
@ -182,6 +188,8 @@ public enum L10n {
public static var commonMessageRemoved: String { return L10n.tr("Localizable", "common_message_removed") }
/// 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
public static var commonNoResults: String { return L10n.tr("Localizable", "common_no_results") }
/// Offline
@ -244,6 +252,8 @@ public enum L10n {
public static var commonUnableToInviteMessage: String { return L10n.tr("Localizable", "common_unable_to_invite_message") }
/// Unable to send invite(s)
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
public static var commonUnsupportedEvent: String { return L10n.tr("Localizable", "common_unsupported_event") }
/// Username
@ -340,7 +350,7 @@ public enum L10n {
public static func notificationInvitations(_ p1: Int) -> String {
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") }
/// 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") }
/// 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
public static var notificationSenderMe: String { return L10n.tr("Localizable", "notification_sender_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") }
/// New 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
public static var screenCreateRoomAddPeopleTitle: String { return L10n.tr("Localizable", "screen_create_room_add_people_title") }
/// 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") }
/// Could not retrieve 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
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.

View File

@ -37,9 +37,9 @@ struct RoomDetailsScreen: View {
if let recipient = context.viewState.dmRecipient {
ignoreUserSection(user: recipient)
} else {
leaveRoomSection
}
leaveRoomSection
}
.elementFormStyle()
.alert(item: $context.alertInfo)

View File

@ -117,6 +117,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
// MARK: - Private
// swiftlint:disable:next function_body_length
private func setupSubscriptions() {
timelineController.callbacks
.receive(on: DispatchQueue.main)
@ -164,8 +165,33 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
.weakAssign(to: \.state.members, on: self)
.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 {
switch await timelineController.paginateBackwards(requestSize: Constants.backPaginationEventLimit, untilNumberOfItems: Constants.backPaginationPageSize) {
case .failure:
@ -502,6 +528,57 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private func hideLoadingIndicator() {
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

View File

@ -167,4 +167,9 @@ extension RoomProxyProtocol {
var isEncryptedOneToOneRoom: Bool {
isDirect && isEncrypted && activeMembersCount == 2
}
func members() async -> [RoomMemberProxyProtocol]? {
await updateMembers()
return await membersPublisher.values.first()
}
}

View File

@ -308,7 +308,7 @@ class MockScreen: Identifiable {
isEncrypted: true,
members: members,
memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
joinedMembersCount: members.count))
activeMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy,
@ -327,7 +327,7 @@ class MockScreen: Identifiable {
canonicalAlias: "#mock:room.org",
members: members,
memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
joinedMembersCount: members.count))
activeMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy,
@ -348,7 +348,7 @@ class MockScreen: Identifiable {
canonicalAlias: "#mock:room.org",
members: members,
memberForID: owner,
joinedMembersCount: members.count))
activeMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy,
@ -365,7 +365,7 @@ class MockScreen: Identifiable {
isEncrypted: true,
members: members,
memberForID: owner,
joinedMembersCount: members.count))
activeMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy,
@ -383,7 +383,7 @@ class MockScreen: Identifiable {
isEncrypted: true,
members: members,
memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
joinedMembersCount: members.count))
activeMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy,