Skip to content

Commit

Permalink
Invite again user on direct chats (#1087)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
alfogrillo authored Jun 16, 2023
1 parent d2d626f commit 87b0d95
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 22 deletions.
11 changes: 9 additions & 2 deletions ElementX/Resources/Localizations/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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$@";
Expand Down Expand Up @@ -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";
Expand All @@ -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.";
Expand All @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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$@";
Expand Down Expand Up @@ -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.";
Expand Down Expand Up @@ -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";
Expand Down
4 changes: 3 additions & 1 deletion ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 17 additions & 3 deletions ElementX/Sources/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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") }
Expand All @@ -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!
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ struct RoomDetailsScreen: View {

if let recipient = context.viewState.dmRecipient {
ignoreUserSection(user: recipient)
} else {
leaveRoomSection
}

leaveRoomSection
}
.elementFormStyle()
.alert(item: $context.alertInfo)
Expand Down
79 changes: 78 additions & 1 deletion ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions ElementX/Sources/Services/Room/RoomProxyProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,9 @@ extension RoomProxyProtocol {
var isEncryptedOneToOneRoom: Bool {
isDirect && isEncrypted && activeMembersCount == 2
}

func members() async -> [RoomMemberProxyProtocol]? {
await updateMembers()
return await membersPublisher.values.first()
}
}
10 changes: 5 additions & 5 deletions ElementX/Sources/UITests/UITestsAppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 87b0d95

Please sign in to comment.