diff --git a/CHANGES.md b/CHANGES.md index ec853d96ef..05d189ed12 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ Improvements 🙌: - Add a menu item in the timeline as a shortcut to invite user (#2171) - Drawer: move settings access and add sign out action (#2171) - Filter room member (and banned users) by name (#2184) + - Implement "Jump to read receipt" and "Mention" actions on the room member profile screen Bugfix 🐛: - Improve support for image/audio/video/file selection with intent changes (#1376) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt index 3aa9d60e6a..4fc13e9228 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt @@ -63,6 +63,14 @@ interface ReadService { */ fun getMyReadReceiptLive(): LiveData> + /** + * Get the eventId where the read receipt for the provided user is + * @param userId the id of the user to look for + * + * @return the eventId where the read receipt for the provided user is attached, or null if not found + */ + fun getUserReadReceipt(userId: String): String? + /** * Returns a live list of read receipts for a given event * @param eventId: the event diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt index a5520972b0..bea3d6624d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt @@ -107,6 +107,16 @@ internal class DefaultReadService @AssistedInject constructor( } } + override fun getUserReadReceipt(userId: String): String? { + var eventId: String? = null + monarchy.doWithRealm { + eventId = ReadReceiptEntity.where(it, roomId = roomId, userId = userId) + .findFirst() + ?.eventId + } + return eventId + } + override fun getEventReadReceiptsLive(eventId: String): LiveData> { val liveRealmData = monarchy.findAllMappedWithChanges( { ReadReceiptsSummaryEntity.where(it, eventId) }, diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index 4ba3d6ba13..28f3a52efa 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -36,6 +36,7 @@ import im.vector.app.features.crypto.verification.IncomingVerificationRequestHan import im.vector.app.features.grouplist.SelectedGroupDataSource import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.HomeRoomListDataSource +import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.VectorHtmlCompressor @@ -114,6 +115,8 @@ interface VectorComponent { fun selectedGroupStore(): SelectedGroupDataSource + fun roomDetailPendingActionStore(): RoomDetailPendingActionStore + fun activeSessionObservableStore(): ActiveSessionDataSource fun incomingVerificationRequestHandler(): IncomingVerificationRequestHandler diff --git a/vector/src/main/java/im/vector/app/core/utils/TemporaryStore.kt b/vector/src/main/java/im/vector/app/core/utils/TemporaryStore.kt index 32b995c004..63f80341eb 100644 --- a/vector/src/main/java/im/vector/app/core/utils/TemporaryStore.kt +++ b/vector/src/main/java/im/vector/app/core/utils/TemporaryStore.kt @@ -23,6 +23,7 @@ const val THREE_MINUTES = 3 * 60_000L /** * Store an object T for a specific period of time + * @param delay delay to keep the data, in millis */ open class TemporaryStore(private val delay: Long = THREE_MINUTES) { @@ -30,14 +31,16 @@ open class TemporaryStore(private val delay: Long = THREE_MINUTES) { var data: T? = null set(value) { - field = value timer?.cancel() - timer = Timer().also { - it.schedule(object : TimerTask() { - override fun run() { - field = null - } - }, delay) + field = value + if (value != null) { + timer = Timer().also { + it.schedule(object : TimerTask() { + override fun run() { + field = null + } + }, delay) + } } } } 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 5f851b8da7..03809d3c75 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,4 +88,6 @@ sealed class RoomDetailAction : VectorViewModelAction { data class EnsureNativeWidgetAllowed(val widget: Widget, val userJustAccepted: Boolean, val grantedEvents: RoomDetailViewEvents) : 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 66e49cf060..eadea299e6 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 @@ -216,7 +216,8 @@ class RoomDetailFragment @Inject constructor( private val notificationUtils: NotificationUtils, private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, private val matrixItemColorProvider: MatrixItemColorProvider, - private val imageContentRenderer: ImageContentRenderer + private val imageContentRenderer: ImageContentRenderer, + private val roomDetailPendingActionStore: RoomDetailPendingActionStore ) : VectorBaseFragment(), TimelineEventController.Callback, @@ -878,6 +879,17 @@ class RoomDetailFragment @Inject constructor( override fun onResume() { super.onResume() notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId) + roomDetailPendingActionStore.data?.let { handlePendingAction(it) } + roomDetailPendingActionStore.data = null + } + + private fun handlePendingAction(roomDetailPendingAction: RoomDetailPendingAction) { + when (roomDetailPendingAction) { + is RoomDetailPendingAction.JumpToReadReceipt -> + roomDetailViewModel.handle(RoomDetailAction.JumpToReadReceipt(roomDetailPendingAction.userId)) + is RoomDetailPendingAction.MentionUser -> + insertUserDisplayNameInTextEditor(roomDetailPendingAction.userId) + }.exhaustive } override fun onPause() { 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 new file mode 100644 index 0000000000..394d46ef8d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingAction.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail + +sealed class 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/RoomDetailPendingActionStore.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingActionStore.kt new file mode 100644 index 0000000000..9ffbb83a47 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailPendingActionStore.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail + +import im.vector.app.core.utils.TemporaryStore +import javax.inject.Inject +import javax.inject.Singleton + +// Store to keep a pending action from sub screen of a room detail +@Singleton +class RoomDetailPendingActionStore @Inject constructor() : TemporaryStore(10_000) 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 ebc265d935..009f8ec802 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 @@ -274,9 +274,15 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) is RoomDetailAction.CancelSend -> handleCancel(action) + is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action) }.exhaustive } + private fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) { + room.getUserReadReceipt(action.userId) + ?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) } + } + private fun handleSendSticker(action: RoomDetailAction.SendSticker) { room.sendEvent(EventType.STICKER, action.stickerContent.toContent()) } 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 ca9ecb62ef..a3ffd80ade 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 @@ -172,13 +172,16 @@ class RoomMemberProfileController @Inject constructor( val membership = state.asyncMembership() ?: return buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) - buildProfileAction( - id = "read_receipt", - editable = false, - title = stringProvider.getString(R.string.room_member_jump_to_read_receipt), - dividerColor = dividerColor, - action = { callback?.onJumpToReadReceiptClicked() } - ) + + if (state.hasReadReceipt) { + buildProfileAction( + id = "read_receipt", + editable = false, + title = stringProvider.getString(R.string.room_member_jump_to_read_receipt), + dividerColor = dividerColor, + action = { callback?.onJumpToReadReceiptClicked() } + ) + } val ignoreActionTitle = state.buildIgnoreActionTitle() 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 169cba09eb..2f5b2d5387 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 @@ -43,6 +43,8 @@ import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.detail.RoomDetailPendingAction +import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs import kotlinx.android.parcel.Parcelize @@ -61,7 +63,8 @@ data class RoomMemberProfileArgs( class RoomMemberProfileFragment @Inject constructor( val viewModelFactory: RoomMemberProfileViewModel.Factory, private val roomMemberProfileController: RoomMemberProfileController, - private val avatarRenderer: AvatarRenderer + private val avatarRenderer: AvatarRenderer, + private val roomDetailPendingActionStore: RoomDetailPendingActionStore ) : VectorBaseFragment(), RoomMemberProfileController.Callback { private val fragmentArgs: RoomMemberProfileArgs by args() @@ -276,11 +279,13 @@ class RoomMemberProfileFragment @Inject constructor( } override fun onJumpToReadReceiptClicked() { - vectorBaseActivity.notImplemented("Jump to read receipts") + roomDetailPendingActionStore.data = RoomDetailPendingAction.JumpToReadReceipt(fragmentArgs.userId) + vectorBaseActivity.finish() } override fun onMentionClicked() { - vectorBaseActivity.notImplemented("Mention") + roomDetailPendingActionStore.data = RoomDetailPendingAction.MentionUser(fragmentArgs.userId) + vectorBaseActivity.finish() } private fun handleShareRoomMemberProfile(permalink: String) { diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt index f2c85c96c7..78562ea351 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewModel.kt @@ -85,7 +85,8 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v setState { copy( isMine = session.myUserId == this.userId, - userMatrixItem = room?.getRoomMember(initialState.userId)?.toMatrixItem()?.let { Success(it) } ?: Uninitialized + userMatrixItem = room?.getRoomMember(initialState.userId)?.toMatrixItem()?.let { Success(it) } ?: Uninitialized, + hasReadReceipt = room?.getUserReadReceipt(initialState.userId) != null ) } observeIgnoredState() diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt index dec003b37d..f943a5cf08 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileViewState.kt @@ -39,6 +39,7 @@ data class RoomMemberProfileViewState( val allDevicesAreTrusted: Boolean = false, val allDevicesAreCrossSignedTrusted: Boolean = false, val asyncMembership: Async = Uninitialized, + val hasReadReceipt: Boolean = false, val actionPermissions: ActionPermissions = ActionPermissions() ) : MvRxState { diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt index 6bcb9f9c7f..734620e378 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileActivity.kt @@ -29,6 +29,7 @@ import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore import im.vector.app.features.room.RequireActiveMembershipViewEvents import im.vector.app.features.room.RequireActiveMembershipViewModel import im.vector.app.features.room.RequireActiveMembershipViewState @@ -61,6 +62,9 @@ class RoomProfileActivity : @Inject lateinit var requireActiveMembershipViewModelFactory: RequireActiveMembershipViewModel.Factory + @Inject + lateinit var roomDetailPendingActionStore: RoomDetailPendingActionStore + override fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel { return requireActiveMembershipViewModelFactory.create(initialState) } @@ -97,6 +101,13 @@ class RoomProfileActivity : } } + override fun onResume() { + super.onResume() + if (roomDetailPendingActionStore.data != null) { + finish() + } + } + private fun handleRoomLeft(roomLeft: RequireActiveMembershipViewEvents.RoomLeft) { if (roomLeft.leftMessage != null) { Toast.makeText(this, roomLeft.leftMessage, Toast.LENGTH_LONG).show()