From 87b0d95e67861a635c0264808b9f1f1a92e146fd Mon Sep 17 00:00:00 2001 From: Alfonso Grillo Date: Fri, 16 Jun 2023 15:36:27 +0200 Subject: [PATCH] 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 --- .../en.lproj/Localizable.strings | 11 ++- .../RoomFlowCoordinator.swift | 4 +- ElementX/Sources/Generated/Strings.swift | 20 ++++- .../View/RoomDetailsScreen.swift | 4 +- .../RoomScreen/RoomScreenViewModel.swift | 79 ++++++++++++++++++- .../Services/Room/RoomProxyProtocol.swift | 5 ++ .../UITests/UITestsAppCoordinator.swift | 10 +-- ...-generation.roomDetailsScreenDmDetails.png | 4 +- ...B-iPhone-14.roomDetailsScreenDmDetails.png | 4 +- ...-generation.roomDetailsScreenDmDetails.png | 4 +- ...o-iPhone-14.roomDetailsScreenDmDetails.png | 4 +- 11 files changed, 127 insertions(+), 22 deletions(-) diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index a6121c8a14..6e8371bc3e 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -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 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_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 can’t 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"; diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index f812a6af68..c37a64d69a 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -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 diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 1e2c9f17b3..ff41a3097d 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -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. diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift index 59200eda96..b9f4b83e4b 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift @@ -37,9 +37,9 @@ struct RoomDetailsScreen: View { if let recipient = context.viewState.dmRecipient { ignoreUserSection(user: recipient) - } else { - leaveRoomSection } + + leaveRoomSection } .elementFormStyle() .alert(item: $context.alertInfo) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 5dcd8f558c..c67592e9d6 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -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 diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index 03a0a3ed86..3d6f0b014b 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -167,4 +167,9 @@ extension RoomProxyProtocol { var isEncryptedOneToOneRoom: Bool { isDirect && isEncrypted && activeMembersCount == 2 } + + func members() async -> [RoomMemberProxyProtocol]? { + await updateMembers() + return await membersPublisher.values.first() + } } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 80507cf47c..4d76da8d29 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -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, diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenDmDetails.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenDmDetails.png index 282028d454..df8a52b799 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenDmDetails.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.roomDetailsScreenDmDetails.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:523804837d4ca7123635a59677e8e50abb4fc3e8bab0d17d817955ea736244b1 -size 101550 +oid sha256:4ca554f9b25a8705b17082d5d8c251a83273b2cb021c0ac334ce0e002e76a076 +size 107039 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenDmDetails.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenDmDetails.png index 98a21540b1..4aac6cbca9 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenDmDetails.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.roomDetailsScreenDmDetails.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99f9a46a131e99939687a82a67933d27e28aa6a2f0cbf43a7b445e132e914bf2 -size 124688 +oid sha256:f13ecb7efed5879470031cc91dab1d8f09a4f468204589383902a4be4cf912dc +size 134039 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenDmDetails.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenDmDetails.png index a6d719b9d7..611e35e0c2 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenDmDetails.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.roomDetailsScreenDmDetails.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4328de12a3aefba9bdc8bf52bcdc8c6587a1618452aa6499429d85a80634fe30 -size 114902 +oid sha256:27c43edc3f2ee71699ea7159d845c9d78b5e1eac23bf9a999c9a81f2942e620d +size 120590 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenDmDetails.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenDmDetails.png index cdc8d480f6..6a52732e1b 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenDmDetails.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.roomDetailsScreenDmDetails.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9aab159ec7d3a2abbf32e7beca660f56db0cc0aeaf5dd35f398633be6cbe8c19 -size 152190 +oid sha256:3e706f3a8eb417b4d0fca086d612e5d2dcf362ae8ab49c299ea746d7047fdab9 +size 162199