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
@@ -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
+17 -3
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.
@@ -37,9 +37,9 @@ struct RoomDetailsScreen: View {
if let recipient = context.viewState.dmRecipient {
ignoreUserSection(user: recipient)
} else {
leaveRoomSection
}
leaveRoomSection
}
.elementFormStyle()
.alert(item: $context.alertInfo)
@@ -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
@@ -167,4 +167,9 @@ extension RoomProxyProtocol {
var isEncryptedOneToOneRoom: Bool {
isDirect && isEncrypted && activeMembersCount == 2
}
func members() async -> [RoomMemberProxyProtocol]? {
await updateMembers()
return await membersPublisher.values.first()
}
}
@@ -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,