mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
Typings: extract from room summary and use an in memory tracker
This commit is contained in:
parent
8cef299878
commit
46378845e9
20 changed files with 246 additions and 74 deletions
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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>>
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 { _ ->
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue