diff --git a/CHANGES.md b/CHANGES.md index 8261412b4c..5cf2e7f53b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ Improvements 🙌: - Use Hardware keyboard enter to send message (use shift-enter for new line) (#1881, #1440) - Edit and remove icons are now visible on image attachment preview screen (#2294) - Room profile: BigImageViewerActivity now only display the image. Use the room setting to change or delete the room Avatar + - Room member profile: Add action to create (or open) a DM (#2310) Bugfix 🐛: - Messages encrypted with no way to decrypt after SDK update from 0.18 to 1.0.0 (#2252) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 370b416f54..ec2b28a4da 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -285,14 +285,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { fun createDM(alice: Session, bob: Session): String { val roomId = mTestHelper.doSync { - alice.createRoom( - CreateRoomParams().apply { - invitedUserIds.add(bob.myUserId) - setDirectMessage() - enableEncryptionIfInvitedUsersSupportIt = true - }, - it - ) + alice.createDirectRoom(bob.myUserId, it) } mTestHelper.waitWithLatch { latch -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt index 965e7e23bb..b772225f51 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt @@ -35,6 +35,22 @@ interface RoomService { fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback): Cancelable + /** + * Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters + */ + fun createDirectRoom(otherUserId: String, + callback: MatrixCallback): Cancelable { + return createRoom( + CreateRoomParams() + .apply { + invitedUserIds.add(otherUserId) + setDirectMessage() + enableEncryptionIfInvitedUsersSupportIt = true + }, + callback + ) + } + /** * Join a room by id * @param roomIdOrAlias the roomId or the room alias of the room to join @@ -113,5 +129,16 @@ interface RoomService { */ fun getChangeMembershipsLive(): LiveData> - fun getExistingDirectRoomWithUser(otherUserId: String): Room? + /** + * Return the roomId of an existing DM with the other user, or null if such room does not exist + * A room is a DM if: + * - it is listed in the `m.direct` account data + * - the current user has joined the room + * - the other user is invited or has joined the room + * - it has exactly 2 members + * Note: + * - the returning room can be encrypted or not + * - the power level of the users are not taken into account. Normally in a DM, the 2 members are admins of the room + */ + fun getExistingDirectRoomWithUser(otherUserId: String): String? } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt index 171e90703c..d49c2f120c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt @@ -61,7 +61,7 @@ internal class DefaultRoomService @Inject constructor( return roomGetter.getRoom(roomId) } - override fun getExistingDirectRoomWithUser(otherUserId: String): Room? { + override fun getExistingDirectRoomWithUser(otherUserId: String): String? { return roomGetter.getDirectRoomWith(otherUserId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt index 2947518605..eb9cd9fcba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomGetter.kt @@ -25,13 +25,12 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.SessionScope -import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import javax.inject.Inject internal interface RoomGetter { fun getRoom(roomId: String): Room? - fun getDirectRoomWith(otherUserId: String): Room? + fun getDirectRoomWith(otherUserId: String): String? } @SessionScope @@ -46,16 +45,14 @@ internal class DefaultRoomGetter @Inject constructor( } } - override fun getDirectRoomWith(otherUserId: String): Room? { + override fun getDirectRoomWith(otherUserId: String): String? { return realmSessionProvider.withRealm { realm -> RoomSummaryEntity.where(realm) .equalTo(RoomSummaryEntityFields.IS_DIRECT, true) .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) .findAll() - .filter { dm -> dm.otherMemberIds.contains(otherUserId) } - .map { it.roomId } - .firstOrNull { roomId -> otherUserId in RoomMemberHelper(realm, roomId).getActiveRoomMemberIds() } - ?.let { roomId -> createRoom(realm, roomId) } + .firstOrNull { dm -> dm.otherMemberIds.size == 1 && dm.otherMemberIds.first() == otherUserId } + ?.roomId } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index 2720c20fb0..2d09974687 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -49,7 +49,6 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.events.model.LocalEcho -import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64 @@ -233,7 +232,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( override fun handle(action: VerificationAction) = withState { state -> val otherUserId = state.otherUserMxItem?.id ?: return@withState val roomId = state.roomId - ?: session.getExistingDirectRoomWithUser(otherUserId)?.roomId + ?: session.getExistingDirectRoomWithUser(otherUserId) when (action) { is VerificationAction.RequestVerificationByDM -> { @@ -245,14 +244,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( pendingRequest = Loading() ) } - val roomParams = CreateRoomParams() - .apply { - invitedUserIds.add(otherUserId) - setDirectMessage() - enableEncryptionIfInvitedUsersSupportIt = true - } - - session.createRoom(roomParams, object : MatrixCallback { + session.createDirectRoom(otherUserId, object : MatrixCallback { override fun onSuccess(data: String) { setState { copy( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 912967138b..99adc0bf83 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -88,5 +88,6 @@ sealed class RoomDetailAction : VectorViewModelAction { val userJustAccepted: Boolean, val grantedEvents: RoomDetailViewEvents) : RoomDetailAction() + data class OpenOrCreateDm(val userId: String) : RoomDetailAction() data class JumpToReadReceipt(val userId: String) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 24f3cabe12..9c6c473a7f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -363,6 +363,7 @@ class RoomDetailFragment @Inject constructor( RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView() RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView() is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it) + is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) }.exhaustive } @@ -371,6 +372,10 @@ class RoomDetailFragment @Inject constructor( } } + private fun handleOpenRoom(openRoom: RoomDetailViewEvents.OpenRoom) { + navigator.openRoom(requireContext(), openRoom.roomId, null) + } + private fun requestNativeWidgetPermission(it: RoomDetailViewEvents.RequestNativeWidgetPermission) { val tag = RoomWidgetPermissionBottomSheet::class.java.name val dFrag = childFragmentManager.findFragmentByTag(tag) as? RoomWidgetPermissionBottomSheet @@ -886,6 +891,8 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.JumpToReadReceipt(roomDetailPendingAction.userId)) is RoomDetailPendingAction.MentionUser -> insertUserDisplayNameInTextEditor(roomDetailPendingAction.userId) + is RoomDetailPendingAction.OpenOrCreateDm -> + roomDetailViewModel.handle(RoomDetailAction.OpenOrCreateDm(roomDetailPendingAction.userId)) }.exhaustive } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingAction.kt index 394d46ef8d..598ab9d056 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingAction.kt @@ -17,6 +17,7 @@ package im.vector.app.features.home.room.detail sealed class RoomDetailPendingAction { + data class OpenOrCreateDm(val userId: String) : RoomDetailPendingAction() data class JumpToReadReceipt(val userId: String) : RoomDetailPendingAction() data class MentionUser(val userId: String) : RoomDetailPendingAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index ee2d193473..b9e3e6b31d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -38,6 +38,8 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class ShowInfoOkDialog(val message: String) : RoomDetailViewEvents() data class ShowE2EErrorMessage(val withHeldCode: WithHeldCode?) : RoomDetailViewEvents() + data class OpenRoom(val roomId: String) : RoomDetailViewEvents() + data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents() data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index e721e0948d..9efad1081f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -273,10 +273,28 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) is RoomDetailAction.CancelSend -> handleCancel(action) + is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action) is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) }.exhaustive } + private fun handleOpenOrCreateDm(action: RoomDetailAction.OpenOrCreateDm) { + val existingDmRoomId = session.getExistingDirectRoomWithUser(action.userId) + if (existingDmRoomId == null) { + // First create a direct room + viewModelScope.launch(Dispatchers.IO) { + val roomId = awaitCallback { + session.createDirectRoom(action.userId, it) + } + _viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId)) + } + } else { + if (existingDmRoomId != initialState.roomId) { + _viewEvents.post(RoomDetailViewEvents.OpenRoom(existingDmRoomId)) + } + } + } + private fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) { room.getUserReadReceipt(action.userId) ?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index a3ffd80ade..2e91091443 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -45,6 +45,7 @@ class RoomMemberProfileController @Inject constructor( fun onTapVerify() fun onShowDeviceList() fun onShowDeviceListNoCrossSigning() + fun onOpenDmClicked() fun onJumpToReadReceiptClicked() fun onMentionClicked() fun onEditPowerLevel(currentRole: Role) @@ -173,6 +174,14 @@ class RoomMemberProfileController @Inject constructor( buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) + buildProfileAction( + id = "direct", + editable = false, + title = stringProvider.getString(R.string.room_member_open_or_create_dm), + dividerColor = dividerColor, + action = { callback?.onOpenDmClicked() } + ) + if (state.hasReadReceipt) { buildProfileAction( id = "read_receipt", diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt index 2f5b2d5387..d60b5580fa 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileFragment.kt @@ -278,6 +278,11 @@ class RoomMemberProfileFragment @Inject constructor( DeviceListBottomSheet.newInstance(it.userId).show(parentFragmentManager, "DEV_LIST") } + override fun onOpenDmClicked() { + roomDetailPendingActionStore.data = RoomDetailPendingAction.OpenOrCreateDm(fragmentArgs.userId) + vectorBaseActivity.finish() + } + override fun onJumpToReadReceiptClicked() { roomDetailPendingActionStore.data = RoomDetailPendingAction.JumpToReadReceipt(fragmentArgs.userId) vectorBaseActivity.finish() diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index dd461123cc..16ebca5f20 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2167,6 +2167,7 @@ Default in %1$s Custom (%1$d) in %2$s + Direct message Jump to read receipt "Element does not handle events of type '%1$s'"