Merge pull request #2212 from vector-im/feature/bma/jump_to_read_receipt

Add "jump to read receipt" and "mention" actions from room member detail screen
This commit is contained in:
Benoit Marty 2020-10-08 15:17:48 +02:00 committed by GitHub
commit 50cf5b5322
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 132 additions and 19 deletions

View file

@ -15,6 +15,7 @@ Improvements 🙌:
- Add a menu item in the timeline as a shortcut to invite user (#2171) - Add a menu item in the timeline as a shortcut to invite user (#2171)
- Drawer: move settings access and add sign out action (#2171) - Drawer: move settings access and add sign out action (#2171)
- Filter room member (and banned users) by name (#2184) - Filter room member (and banned users) by name (#2184)
- Implement "Jump to read receipt" and "Mention" actions on the room member profile screen
Bugfix 🐛: Bugfix 🐛:
- Improve support for image/audio/video/file selection with intent changes (#1376) - Improve support for image/audio/video/file selection with intent changes (#1376)

View file

@ -63,6 +63,14 @@ interface ReadService {
*/ */
fun getMyReadReceiptLive(): LiveData<Optional<String>> fun getMyReadReceiptLive(): LiveData<Optional<String>>
/**
* 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 * Returns a live list of read receipts for a given event
* @param eventId: the event * @param eventId: the event

View file

@ -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<List<ReadReceipt>> { override fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>> {
val liveRealmData = monarchy.findAllMappedWithChanges( val liveRealmData = monarchy.findAllMappedWithChanges(
{ ReadReceiptsSummaryEntity.where(it, eventId) }, { ReadReceiptsSummaryEntity.where(it, eventId) },

View file

@ -36,6 +36,7 @@ import im.vector.app.features.crypto.verification.IncomingVerificationRequestHan
import im.vector.app.features.grouplist.SelectedGroupDataSource import im.vector.app.features.grouplist.SelectedGroupDataSource
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.HomeRoomListDataSource 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.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.html.VectorHtmlCompressor
@ -114,6 +115,8 @@ interface VectorComponent {
fun selectedGroupStore(): SelectedGroupDataSource fun selectedGroupStore(): SelectedGroupDataSource
fun roomDetailPendingActionStore(): RoomDetailPendingActionStore
fun activeSessionObservableStore(): ActiveSessionDataSource fun activeSessionObservableStore(): ActiveSessionDataSource
fun incomingVerificationRequestHandler(): IncomingVerificationRequestHandler fun incomingVerificationRequestHandler(): IncomingVerificationRequestHandler

View file

@ -23,6 +23,7 @@ const val THREE_MINUTES = 3 * 60_000L
/** /**
* Store an object T for a specific period of time * Store an object T for a specific period of time
* @param delay delay to keep the data, in millis
*/ */
open class TemporaryStore<T>(private val delay: Long = THREE_MINUTES) { open class TemporaryStore<T>(private val delay: Long = THREE_MINUTES) {
@ -30,14 +31,16 @@ open class TemporaryStore<T>(private val delay: Long = THREE_MINUTES) {
var data: T? = null var data: T? = null
set(value) { set(value) {
field = value
timer?.cancel() timer?.cancel()
timer = Timer().also { field = value
it.schedule(object : TimerTask() { if (value != null) {
override fun run() { timer = Timer().also {
field = null it.schedule(object : TimerTask() {
} override fun run() {
}, delay) field = null
}
}, delay)
}
} }
} }
} }

View file

@ -88,4 +88,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class EnsureNativeWidgetAllowed(val widget: Widget, data class EnsureNativeWidgetAllowed(val widget: Widget,
val userJustAccepted: Boolean, val userJustAccepted: Boolean,
val grantedEvents: RoomDetailViewEvents) : RoomDetailAction() val grantedEvents: RoomDetailViewEvents) : RoomDetailAction()
data class JumpToReadReceipt(val userId: String) : RoomDetailAction()
} }

View file

@ -216,7 +216,8 @@ class RoomDetailFragment @Inject constructor(
private val notificationUtils: NotificationUtils, private val notificationUtils: NotificationUtils,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
private val matrixItemColorProvider: MatrixItemColorProvider, private val matrixItemColorProvider: MatrixItemColorProvider,
private val imageContentRenderer: ImageContentRenderer private val imageContentRenderer: ImageContentRenderer,
private val roomDetailPendingActionStore: RoomDetailPendingActionStore
) : ) :
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
@ -878,6 +879,17 @@ class RoomDetailFragment @Inject constructor(
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
notificationDrawerManager.setCurrentRoom(roomDetailArgs.roomId) 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() { override fun onPause() {

View file

@ -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()
}

View file

@ -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<RoomDetailPendingAction>(10_000)

View file

@ -274,9 +274,15 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
is RoomDetailAction.CancelSend -> handleCancel(action) is RoomDetailAction.CancelSend -> handleCancel(action)
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
}.exhaustive }.exhaustive
} }
private fun handleJumpToReadReceipt(action: RoomDetailAction.JumpToReadReceipt) {
room.getUserReadReceipt(action.userId)
?.let { handleNavigateToEvent(RoomDetailAction.NavigateToEvent(it, true)) }
}
private fun handleSendSticker(action: RoomDetailAction.SendSticker) { private fun handleSendSticker(action: RoomDetailAction.SendSticker) {
room.sendEvent(EventType.STICKER, action.stickerContent.toContent()) room.sendEvent(EventType.STICKER, action.stickerContent.toContent())
} }

View file

@ -172,13 +172,16 @@ class RoomMemberProfileController @Inject constructor(
val membership = state.asyncMembership() ?: return val membership = state.asyncMembership() ?: return
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
buildProfileAction(
id = "read_receipt", if (state.hasReadReceipt) {
editable = false, buildProfileAction(
title = stringProvider.getString(R.string.room_member_jump_to_read_receipt), id = "read_receipt",
dividerColor = dividerColor, editable = false,
action = { callback?.onJumpToReadReceiptClicked() } title = stringProvider.getString(R.string.room_member_jump_to_read_receipt),
) dividerColor = dividerColor,
action = { callback?.onJumpToReadReceiptClicked() }
)
}
val ignoreActionTitle = state.buildIgnoreActionTitle() val ignoreActionTitle = state.buildIgnoreActionTitle()

View file

@ -43,6 +43,8 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.startSharePlainTextIntent import im.vector.app.core.utils.startSharePlainTextIntent
import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.crypto.verification.VerificationBottomSheet
import im.vector.app.features.home.AvatarRenderer 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.devices.DeviceListBottomSheet
import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs import im.vector.app.features.roommemberprofile.powerlevel.EditPowerLevelDialogs
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@ -61,7 +63,8 @@ data class RoomMemberProfileArgs(
class RoomMemberProfileFragment @Inject constructor( class RoomMemberProfileFragment @Inject constructor(
val viewModelFactory: RoomMemberProfileViewModel.Factory, val viewModelFactory: RoomMemberProfileViewModel.Factory,
private val roomMemberProfileController: RoomMemberProfileController, private val roomMemberProfileController: RoomMemberProfileController,
private val avatarRenderer: AvatarRenderer private val avatarRenderer: AvatarRenderer,
private val roomDetailPendingActionStore: RoomDetailPendingActionStore
) : VectorBaseFragment(), RoomMemberProfileController.Callback { ) : VectorBaseFragment(), RoomMemberProfileController.Callback {
private val fragmentArgs: RoomMemberProfileArgs by args() private val fragmentArgs: RoomMemberProfileArgs by args()
@ -276,11 +279,13 @@ class RoomMemberProfileFragment @Inject constructor(
} }
override fun onJumpToReadReceiptClicked() { override fun onJumpToReadReceiptClicked() {
vectorBaseActivity.notImplemented("Jump to read receipts") roomDetailPendingActionStore.data = RoomDetailPendingAction.JumpToReadReceipt(fragmentArgs.userId)
vectorBaseActivity.finish()
} }
override fun onMentionClicked() { override fun onMentionClicked() {
vectorBaseActivity.notImplemented("Mention") roomDetailPendingActionStore.data = RoomDetailPendingAction.MentionUser(fragmentArgs.userId)
vectorBaseActivity.finish()
} }
private fun handleShareRoomMemberProfile(permalink: String) { private fun handleShareRoomMemberProfile(permalink: String) {

View file

@ -85,7 +85,8 @@ class RoomMemberProfileViewModel @AssistedInject constructor(@Assisted private v
setState { setState {
copy( copy(
isMine = session.myUserId == this.userId, 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() observeIgnoredState()

View file

@ -39,6 +39,7 @@ data class RoomMemberProfileViewState(
val allDevicesAreTrusted: Boolean = false, val allDevicesAreTrusted: Boolean = false,
val allDevicesAreCrossSignedTrusted: Boolean = false, val allDevicesAreCrossSignedTrusted: Boolean = false,
val asyncMembership: Async<Membership> = Uninitialized, val asyncMembership: Async<Membership> = Uninitialized,
val hasReadReceipt: Boolean = false,
val actionPermissions: ActionPermissions = ActionPermissions() val actionPermissions: ActionPermissions = ActionPermissions()
) : MvRxState { ) : MvRxState {

View file

@ -29,6 +29,7 @@ import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity 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.RequireActiveMembershipViewEvents
import im.vector.app.features.room.RequireActiveMembershipViewModel import im.vector.app.features.room.RequireActiveMembershipViewModel
import im.vector.app.features.room.RequireActiveMembershipViewState import im.vector.app.features.room.RequireActiveMembershipViewState
@ -61,6 +62,9 @@ class RoomProfileActivity :
@Inject @Inject
lateinit var requireActiveMembershipViewModelFactory: RequireActiveMembershipViewModel.Factory lateinit var requireActiveMembershipViewModelFactory: RequireActiveMembershipViewModel.Factory
@Inject
lateinit var roomDetailPendingActionStore: RoomDetailPendingActionStore
override fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel { override fun create(initialState: RequireActiveMembershipViewState): RequireActiveMembershipViewModel {
return requireActiveMembershipViewModelFactory.create(initialState) 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) { private fun handleRoomLeft(roomLeft: RequireActiveMembershipViewEvents.RoomLeft) {
if (roomLeft.leftMessage != null) { if (roomLeft.leftMessage != null) {
Toast.makeText(this, roomLeft.leftMessage, Toast.LENGTH_LONG).show() Toast.makeText(this, roomLeft.leftMessage, Toast.LENGTH_LONG).show()