Typings: extract from room summary and use an in memory tracker

This commit is contained in:
ganfra 2020-06-16 17:50:08 +02:00
parent 8cef299878
commit 46378845e9
20 changed files with 246 additions and 74 deletions

View file

@ -42,6 +42,7 @@ import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.typing.TypingUsersTracker
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.widgets.WidgetService
@ -145,6 +146,11 @@ interface Session :
*/
fun contentUploadProgressTracker(): ContentUploadStateTracker
/**
* Returns the TypingUsersTracker associated with the session
*/
fun typingUsersTracker(): TypingUsersTracker
/**
* Returns the cryptoService associated with the session
*/
@ -190,4 +196,5 @@ interface Session :
}
val sharedSecretStorageService: SharedSecretStorageService
}

View file

@ -48,7 +48,6 @@ data class RoomSummary constructor(
val isEncrypted: Boolean,
val encryptionEventTs: Long?,
val inviterId: String? = null,
val typingRoomMemberIds: List<String> = emptyList(),
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
) {

View file

@ -0,0 +1,38 @@
/*
* 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.matrix.android.api.session.typing
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.room.sender.SenderInfo
/**
* Responsible for tracking typing users from each room.
* It's ephemeral data and it's only saved in memory.
*/
interface TypingUsersTracker {
/**
* Returns the sender information of all currently typing users in a room, excluding yourself.
*/
fun getTypingUsers(roomId: String): List<SenderInfo>
/**
* Returns a LiveData of the sender information of all currently typing users in a room, excluding yourself.
*/
fun getTypingUsersLive(roomId: String): LiveData<List<SenderInfo>>
}

View file

@ -54,7 +54,6 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
aliases = roomSummaryEntity.aliases.toList(),
isEncrypted = roomSummaryEntity.isEncrypted,
encryptionEventTs = roomSummaryEntity.encryptionEventTs,
typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList(),
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel,
inviterId = roomSummaryEntity.inviterId

View file

@ -49,7 +49,6 @@ internal open class RoomSummaryEntity(
var flatAliases: String = "",
var isEncrypted: Boolean = false,
var encryptionEventTs: Long? = 0,
var typingUserIds: RealmList<String> = RealmList(),
var roomEncryptionTrustLevelStr: String? = null,
var inviterId: String? = null
) : RealmObject() {

View file

@ -45,6 +45,7 @@ import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.session.typing.TypingUsersTracker
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.widgets.WidgetService
import im.vector.matrix.android.internal.auth.SessionParamsStore
@ -99,6 +100,7 @@ internal class DefaultSession @Inject constructor(
private val syncTokenStore: SyncTokenStore,
private val sessionParamsStore: SessionParamsStore,
private val contentUploadProgressTracker: ContentUploadStateTracker,
private val typingUsersTracker: TypingUsersTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>,
private val accountDataService: Lazy<AccountDataService>,
@ -237,6 +239,8 @@ internal class DefaultSession @Inject constructor(
override fun contentUploadProgressTracker() = contentUploadProgressTracker
override fun typingUsersTracker() = typingUsersTracker
override fun cryptoService(): CryptoService = cryptoService.get()
override fun identityService() = defaultIdentityService

View file

@ -36,6 +36,7 @@ import im.vector.matrix.android.api.session.accountdata.AccountDataService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
import im.vector.matrix.android.api.session.typing.TypingUsersTracker
import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService
import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver
import im.vector.matrix.android.internal.database.LiveEntityObserver
@ -66,6 +67,7 @@ import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLive
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver
import im.vector.matrix.android.internal.session.securestorage.DefaultSecureStorageService
import im.vector.matrix.android.internal.session.typing.DefaultTypingUsersTracker
import im.vector.matrix.android.internal.session.user.accountdata.DefaultAccountDataService
import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration
@ -269,4 +271,7 @@ internal abstract class SessionModule {
@Binds
abstract fun bindSharedSecretStorageService(service: DefaultSharedSecretStorageService): SharedSecretStorageService
@Binds
abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker
}

View file

@ -85,7 +85,6 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummary: RoomSyncSummary? = null,
unreadNotifications: RoomSyncUnreadNotifications? = null,
updateMembers: Boolean = false,
ephemeralResult: RoomSyncHandler.EphemeralResult? = null,
inviterId: String? = null) {
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
if (roomSummary != null) {
@ -137,8 +136,6 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
roomSummaryEntity.isEncrypted = encryptionEvent != null
roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs
roomSummaryEntity.typingUserIds.clear()
roomSummaryEntity.typingUserIds.addAll(ephemeralResult?.typingUserIds.orEmpty())
if (roomSummaryEntity.membership == Membership.INVITE && inviterId != null) {
roomSummaryEntity.inviterId = inviterId

View file

@ -69,6 +69,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val roomFullyReadHandler: RoomFullyReadHandler,
private val cryptoService: DefaultCryptoService,
private val roomMemberEventHandler: RoomMemberEventHandler,
private val roomTypingUsersHandler: RoomTypingUsersHandler,
@UserId private val userId: String,
private val eventBus: EventBus) {
@ -170,14 +171,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
it.type == EventType.STATE_ROOM_MEMBER
} != null
roomTypingUsersHandler.handle(realm, roomId, ephemeralResult)
roomSummaryUpdater.update(
realm,
roomId,
Membership.JOIN,
roomSync.summary,
roomSync.unreadNotifications,
updateMembers = hasRoomMember,
ephemeralResult = ephemeralResult)
updateMembers = hasRoomMember
)
return roomEntity
}

View file

@ -0,0 +1,43 @@
/*
* 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.matrix.android.internal.session.sync
import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
import im.vector.matrix.android.internal.session.typing.DefaultTypingUsersTracker
import io.realm.Realm
import javax.inject.Inject
internal class RoomTypingUsersHandler @Inject constructor(@UserId private val userId: String,
private val typingUsersTracker: DefaultTypingUsersTracker) {
fun handle(realm: Realm, roomId: String, ephemeralResult: RoomSyncHandler.EphemeralResult?) {
val roomMemberHelper = RoomMemberHelper(realm, roomId)
val typingIds = ephemeralResult?.typingUserIds?.filter { it != userId } ?: emptyList()
val senderInfo = typingIds.map { userId ->
val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(userId)
SenderInfo(
userId = userId,
displayName = roomMemberSummaryEntity?.displayName,
isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName),
avatarUrl = roomMemberSummaryEntity?.avatarUrl
)
}
typingUsersTracker.setTypingUsersFromRoom(roomId, senderInfo)
}
}

View file

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.failure.isTokenError
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.sync.SyncTask
import im.vector.matrix.android.internal.session.typing.DefaultTypingUsersTracker
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import im.vector.matrix.android.internal.util.Debouncer
import im.vector.matrix.android.internal.util.createUIHandler
@ -44,6 +45,7 @@ private const val RETRY_WAIT_TIME_MS = 10_000L
private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L
internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val typingUsersTracker: DefaultTypingUsersTracker,
private val networkConnectivityChecker: NetworkConnectivityChecker,
private val backgroundDetectionObserver: BackgroundDetectionObserver)
: Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
@ -196,7 +198,14 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private fun updateStateTo(newState: SyncState) {
Timber.v("Update state from $state to $newState")
if (newState == state) {
return
}
state = newState
// We clear typing users if the sync is not running
if (newState !is SyncState.Running) {
typingUsersTracker.clear()
}
debouncer.debounce("post_state", Runnable {
liveState.value = newState
}, 150)

View file

@ -0,0 +1,62 @@
/*
* 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.matrix.android.internal.session.typing
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.api.session.typing.TypingUsersTracker
import im.vector.matrix.android.internal.session.SessionScope
import javax.inject.Inject
@SessionScope
internal class DefaultTypingUsersTracker @Inject constructor() : TypingUsersTracker {
private val typingUsers = mutableMapOf<String, List<SenderInfo>>()
private val typingUsersLiveData = mutableMapOf<String, MutableLiveData<List<SenderInfo>>>()
/**
* Set all currently typing users for a room (excluding yourself)
*/
fun setTypingUsersFromRoom(roomId: String, senderInfoList: List<SenderInfo>) {
val hasNewValue = typingUsers[roomId] != senderInfoList
if (hasNewValue) {
typingUsers[roomId] = senderInfoList
typingUsersLiveData[roomId]?.postValue(senderInfoList)
}
}
/**
* Can be called when there is no sync so you don't get stuck with ephemeral data
*/
fun clear() {
val roomIds = typingUsers.keys
roomIds.forEach {
setTypingUsersFromRoom(it, emptyList())
}
}
override fun getTypingUsers(roomId: String): List<SenderInfo> {
return typingUsers[roomId] ?: emptyList()
}
override fun getTypingUsersLive(roomId: String): LiveData<List<SenderInfo>> {
return typingUsersLiveData.getOrPut(roomId) {
MutableLiveData(getTypingUsers(roomId))
}
}
}

View file

@ -16,6 +16,9 @@
package im.vector.riotx.core.epoxy
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import com.airbnb.epoxy.EpoxyModelWithHolder
import com.airbnb.epoxy.VisibilityState
import kotlinx.coroutines.CoroutineScope
@ -26,13 +29,22 @@ import kotlinx.coroutines.cancelChildren
/**
* EpoxyModelWithHolder which can listen to visibility state change
*/
abstract class VectorEpoxyModel<H : VectorEpoxyHolder> : EpoxyModelWithHolder<H>() {
abstract class VectorEpoxyModel<H : VectorEpoxyHolder> : EpoxyModelWithHolder<H>(), LifecycleOwner {
protected val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun getLifecycle() = lifecycleRegistry
private var onModelVisibilityStateChangedListener: OnVisibilityStateChangedListener? = null
override fun bind(holder: H) {
super.bind(holder)
lifecycleRegistry.currentState = Lifecycle.State.STARTED
}
override fun unbind(holder: H) {
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
coroutineScope.coroutineContext.cancelChildren()
super.unbind(holder)
}

View file

@ -64,7 +64,7 @@ class BreadcrumbsController @Inject constructor(
unreadNotificationCount(it.notificationCount)
showHighlighted(it.highlightCount > 0)
hasUnreadMessage(it.hasUnreadMessages)
hasTypingUsers(typingHelper.excludeCurrentUser(it.typingRoomMemberIds).isNotEmpty())
typingHelper(typingHelper)
hasDraft(it.userDrafts.isNotEmpty())
itemClickListener(
DebouncedClickListener(View.OnClickListener { _ ->

View file

@ -26,18 +26,20 @@ import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.observeNotNull
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
import im.vector.riotx.features.home.room.typing.TypingHelper
@EpoxyModelClass(layout = R.layout.item_breadcrumbs)
abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
@EpoxyAttribute lateinit var typingHelper: TypingHelper
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var unreadNotificationCount: Int = 0
@EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var hasUnreadMessage: Boolean = false
@EpoxyAttribute var hasTypingUsers: Boolean = false
@EpoxyAttribute var hasDraft: Boolean = false
@EpoxyAttribute var itemClickListener: View.OnClickListener? = null
@ -45,10 +47,12 @@ abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
super.bind(holder)
holder.rootView.setOnClickListener(itemClickListener)
holder.unreadIndentIndicator.isVisible = hasUnreadMessage
holder.typingIndicator.isVisible = hasTypingUsers
avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
holder.draftIndentIndicator.isVisible = hasDraft
typingHelper.hasTypingUsers(matrixItem.id).observeNotNull(this) { hasTypingUsers ->
holder.typingIndicator.isVisible = hasTypingUsers
}
}
class Holder : VectorEpoxyHolder() {

View file

@ -770,15 +770,15 @@ class RoomDetailFragment @Inject constructor(
private fun renderSubTitle(typingMessage: String?, topic: String) {
// TODO Temporary place to put typing data
roomToolbarSubtitleView.let {
it.setTextOrHide(typingMessage ?: topic)
if (typingMessage == null) {
it.setTextColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_toolbar_secondary_text_color))
it.setTypeface(null, Typeface.NORMAL)
val subtitle = typingMessage?.takeIf { it.isNotBlank() } ?: topic
roomToolbarSubtitleView.apply {
setTextOrHide(subtitle)
if (typingMessage.isNullOrBlank()) {
setTextColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_toolbar_secondary_text_color))
setTypeface(null, Typeface.NORMAL)
} else {
it.setTextColor(ContextCompat.getColor(requireContext(), R.color.riotx_accent))
it.setTypeface(null, Typeface.BOLD)
setTextColor(ContextCompat.getColor(requireContext(), R.color.riotx_accent))
setTypeface(null, Typeface.BOLD)
}
}
}

View file

@ -58,6 +58,8 @@ import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.extensions.observeK
import im.vector.matrix.rx.asObservable
import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap
import im.vector.riotx.R
@ -162,6 +164,7 @@ class RoomDetailViewModel @AssistedInject constructor(
observeSummaryState()
getUnreadState()
observeSyncState()
observeTypings()
observeEventDisplayedActions()
observeDrafts()
observeUnreadState()
@ -999,17 +1002,22 @@ class RoomDetailViewModel @AssistedInject constructor(
room.rx().liveRoomSummary()
.unwrap()
.execute { async ->
val typingRoomMembers =
typingHelper.toTypingRoomMembers(async.invoke()?.typingRoomMemberIds.orEmpty(), room)
copy(
asyncRoomSummary = async,
typingRoomMembers = typingRoomMembers,
typingMessage = typingHelper.toTypingMessage(typingRoomMembers)
typingRoomMembers = typingRoomMembers
)
}
}
private fun observeTypings(){
typingHelper.getTypingMessage(initialState.roomId)
.asObservable()
.subscribe {
setState { copy(typingMessage = it) }
}
.disposeOnClear()
}
private fun getUnreadState() {
Observable
.combineLatest<List<TimelineEvent>, RoomSummary, UnreadState>(

View file

@ -32,18 +32,21 @@ import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.observeK
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.features.crypto.util.toImageRes
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.typing.TypingHelper
import timber.log.Timber
@EpoxyModelClass(layout = R.layout.item_room)
abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
@EpoxyAttribute lateinit var typingHelper: TypingHelper
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute lateinit var lastFormattedEvent: CharSequence
@EpoxyAttribute lateinit var lastEventTime: CharSequence
@EpoxyAttribute var typingString: CharSequence? = null
@EpoxyAttribute var encryptionTrustLevel: RoomEncryptionTrustLevel? = null
@EpoxyAttribute var unreadNotificationCount: Int = 0
@EpoxyAttribute var hasUnreadMessage: Boolean = false
@ -63,8 +66,6 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
holder.titleView.text = matrixItem.getBestName()
holder.lastEventTimeView.text = lastEventTime
holder.lastEventView.text = lastFormattedEvent
holder.typingView.setTextOrHide(typingString)
holder.lastEventView.isInvisible = holder.typingView.isVisible
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
holder.unreadIndentIndicator.isVisible = hasUnreadMessage
holder.draftView.isVisible = hasDraft
@ -72,6 +73,11 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
holder.roomAvatarDecorationImageView.isVisible = encryptionTrustLevel != null
holder.roomAvatarDecorationImageView.setImageResource(encryptionTrustLevel.toImageRes())
renderSelection(holder, showSelected)
typingHelper.getTypingMessage(matrixItem.id).observeK(this) {
Timber.v("Observe typing for room ${matrixItem.id}: $it")
holder.typingView.setTextOrHide(it)
holder.lastEventView.isInvisible = holder.typingView.isVisible
}
}
private fun renderSelection(holder: Holder, isSelected: Boolean) {

View file

@ -17,7 +17,6 @@
package im.vector.riotx.features.home.room.list
import android.view.View
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.util.toMatrixItem
@ -37,7 +36,6 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
private val dateFormatter: VectorDateFormatter,
private val stringProvider: StringProvider,
private val typingHelper: TypingHelper,
private val session: Session,
private val avatarRenderer: AvatarRenderer) {
fun create(roomSummary: RoomSummary,
@ -104,23 +102,13 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
dateFormatter.formatMessageDay(date)
}
}
val typingString = typingHelper.excludeCurrentUser(roomSummary.typingRoomMemberIds)
.takeIf { it.isNotEmpty() }
?.let { typingMembers ->
// It's not ideal to get a Room and to fetch data from DB here, but let's keep it like this for the moment
val room = session.getRoom(roomSummary.roomId)
val typingRoomMembers = typingHelper.toTypingRoomMembers(typingMembers, room)
typingHelper.toTypingMessage(typingRoomMembers)
}
return RoomSummaryItem_()
.id(roomSummary.roomId)
.avatarRenderer(avatarRenderer)
.encryptionTrustLevel(roomSummary.roomEncryptionTrustLevel)
.matrixItem(roomSummary.toMatrixItem())
.lastEventTime(latestEventTime)
.typingString(typingString)
.typingHelper(typingHelper)
.lastFormattedEvent(latestFormattedEvent)
.showHighlighted(showHighlighted)
.showSelected(showSelected)

View file

@ -16,10 +16,9 @@
package im.vector.riotx.features.home.room.typing
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import javax.inject.Inject
@ -28,42 +27,33 @@ class TypingHelper @Inject constructor(
private val session: Session,
private val stringProvider: StringProvider
) {
/**
* Exclude current user from the list of typing users
* Return true if some users are currently typing in the room (excluding yourself).
*/
fun excludeCurrentUser(
typingUserIds: List<String>
): List<String> {
return typingUserIds
.filter { it != session.myUserId }
fun hasTypingUsers(roomId: String): LiveData<Boolean> {
val liveData = session.typingUsersTracker().getTypingUsersLive(roomId)
return Transformations.map(liveData) {
it.isNotEmpty()
}
}
/**
* Convert a list of userId to a list of maximum 3 UserItems
* Returns a human readable String of currently typing users in the room (excluding yourself).
*/
fun toTypingRoomMembers(
typingUserIds: List<String>,
membershipService: MembershipService?
): List<MatrixItem.UserItem> {
return excludeCurrentUser(typingUserIds)
.take(3)
.mapNotNull { membershipService?.getRoomMember(it) }
.map { it.toMatrixItem() }
}
/**
* Convert a list of typing UserItems to a human readable String
*/
fun toTypingMessage(typingUserItems: List<MatrixItem.UserItem>): String? {
return when {
typingUserItems.isEmpty() ->
null
typingUserItems.size == 1 ->
stringProvider.getString(R.string.room_one_user_is_typing, typingUserItems[0].getBestName())
typingUserItems.size == 2 ->
stringProvider.getString(R.string.room_two_users_are_typing, typingUserItems[0].getBestName(), typingUserItems[1].getBestName())
else ->
stringProvider.getString(R.string.room_many_users_are_typing, typingUserItems[0].getBestName(), typingUserItems[1].getBestName())
fun getTypingMessage(roomId: String): LiveData<String> {
val liveData = session.typingUsersTracker().getTypingUsersLive(roomId)
return Transformations.map(liveData) { typingUsers ->
when {
typingUsers.isEmpty() ->
""
typingUsers.size == 1 ->
stringProvider.getString(R.string.room_one_user_is_typing, typingUsers[0].disambiguatedDisplayName)
typingUsers.size == 2 ->
stringProvider.getString(R.string.room_two_users_are_typing, typingUsers[0].disambiguatedDisplayName, typingUsers[1].disambiguatedDisplayName)
else ->
stringProvider.getString(R.string.room_many_users_are_typing, typingUsers[0].disambiguatedDisplayName, typingUsers[1].disambiguatedDisplayName)
}
}
}
}