Work on timeline

This commit is contained in:
ganfra 2020-01-06 18:44:04 +01:00
parent 99c523b710
commit f9487f8995
10 changed files with 112 additions and 109 deletions

View file

@ -41,7 +41,8 @@ data class RoomSummary(
val membership: Membership = Membership.NONE,
val versioningState: VersioningState = VersioningState.NONE,
val readMarkerId: String? = null,
val userDrafts: List<UserDraft> = emptyList()
val userDrafts: List<UserDraft> = emptyList(),
var isEncrypted: Boolean
) {
val isVersioned: Boolean

View file

@ -92,7 +92,7 @@ internal fun ChunkEntity.add(roomId: String,
}
}
val isUnlinked = isUnlinked
val isChunkUnlinked = isUnlinked
val localId = TimelineEventEntity.nextId(realm)
val eventId = event.eventId ?: ""
val senderId = event.senderId ?: ""
@ -121,7 +121,7 @@ internal fun ChunkEntity.add(roomId: String,
this.stateIndex = currentStateIndex
this.displayIndex = currentDisplayIndex
this.sendState = SendState.SYNCED
this.isUnlinked = isUnlinked
this.isUnlinked = isChunkUnlinked
}
val eventEntity = realm.createObject<TimelineEventEntity>().also {
it.localId = localId

View file

@ -70,7 +70,8 @@ internal class RoomSummaryMapper @Inject constructor(
readMarkerId = roomSummaryEntity.readMarkerId,
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
canonicalAlias = roomSummaryEntity.canonicalAlias,
aliases = roomSummaryEntity.aliases.toList()
aliases = roomSummaryEntity.aliases.toList(),
isEncrypted = roomSummaryEntity.isEncrypted
)
}
}

View file

@ -43,7 +43,8 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var canonicalAlias: String? = null,
var aliases: RealmList<String> = RealmList(),
// this is required for querying
var flatAliases: String = ""
var flatAliases: String = "",
var isEncrypted: Boolean = false
) : RealmObject() {
private var membershipStr: String = Membership.NONE.name

View file

@ -93,11 +93,13 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).prev()
val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
val encryptionEvent = EventEntity.where(realm, roomId, EventType.ENCRYPTION).prev()
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
// avoid this call if we are sure there are unread events
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic
@ -105,10 +107,12 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
?.canonicalAlias
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases ?: emptyList()
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases
?: emptyList()
roomSummaryEntity.aliases.clear()
roomSummaryEntity.aliases.addAll(roomAliases)
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
roomSummaryEntity.isEncrypted = encryptionEvent != null
if (updateMembers) {
val otherRoomMembers = RoomMembers(realm, roomId)

View file

@ -52,8 +52,8 @@ import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort
import timber.log.Timber
import java.util.Collections
import java.util.UUID
import java.util.*
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.ArrayList
@ -77,11 +77,9 @@ internal class DefaultTimeline(
private val hiddenReadReceipts: TimelineHiddenReadReceipts
) : Timeline, TimelineHiddenReadReceipts.Delegate {
private companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
}
val backgroundHandler = createBackgroundHandler("TIMELINE_DB_THREAD_${System.currentTimeMillis()}")
private val listeners = ArrayList<Timeline.Listener>()
private val listeners = CopyOnWriteArrayList<Timeline.Listener>()
private val isStarted = AtomicBoolean(false)
private val isReady = AtomicBoolean(false)
private val mainHandler = createUIHandler()
@ -137,7 +135,7 @@ internal class DefaultTimeline(
// Public methods ******************************************************************************
override fun paginate(direction: Timeline.Direction, count: Int) {
BACKGROUND_HANDLER.post {
backgroundHandler.post {
if (!canPaginate(direction)) {
return@post
}
@ -165,7 +163,7 @@ internal class DefaultTimeline(
override fun start() {
if (isStarted.compareAndSet(false, true)) {
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
BACKGROUND_HANDLER.post {
backgroundHandler.post {
eventDecryptor.start()
val realm = Realm.getInstance(realmConfiguration)
backgroundRealm.set(realm)
@ -199,8 +197,8 @@ internal class DefaultTimeline(
isReady.set(false)
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
cancelableBag.cancel()
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
BACKGROUND_HANDLER.post {
backgroundHandler.removeCallbacksAndMessages(null)
backgroundHandler.post {
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
if (this::eventRelations.isInitialized) {
eventRelations.removeAllChangeListeners()
@ -288,20 +286,20 @@ internal class DefaultTimeline(
return hasMoreInCache(direction) || !hasReachedEnd(direction)
}
override fun addListener(listener: Timeline.Listener) = synchronized(listeners) {
override fun addListener(listener: Timeline.Listener): Boolean {
if (listeners.contains(listener)) {
return false
}
listeners.add(listener).also {
return listeners.add(listener).also {
postSnapshot()
}
}
override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) {
listeners.remove(listener)
override fun removeListener(listener: Timeline.Listener): Boolean {
return listeners.remove(listener)
}
override fun removeAllListeners() = synchronized(listeners) {
override fun removeAllListeners() {
listeners.clear()
}
@ -516,7 +514,7 @@ internal class DefaultTimeline(
}
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
// Database won't be updated, so we force pagination request
BACKGROUND_HANDLER.post {
backgroundHandler.post {
executePaginationTask(direction, limit)
}
}
@ -649,19 +647,17 @@ internal class DefaultTimeline(
}
private fun postSnapshot() {
BACKGROUND_HANDLER.post {
backgroundHandler.post {
if (isReady.get().not()) {
return@post
}
updateLoadingStates(filteredEvents)
val snapshot = createSnapshot()
val runnable = Runnable {
synchronized(listeners) {
listeners.forEach {
it.onTimelineUpdated(snapshot)
}
}
}
debouncer.debounce("post_snapshot", runnable, 50)
}
}
@ -671,12 +667,10 @@ internal class DefaultTimeline(
return
}
val runnable = Runnable {
synchronized(listeners) {
listeners.forEach {
it.onTimelineFailure(throwable)
}
}
}
mainHandler.post(runnable)
}

View file

@ -40,6 +40,7 @@ import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.lifecycle.observe
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -71,6 +72,7 @@ import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
import im.vector.matrix.rx.rx
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
@ -225,6 +227,8 @@ class RoomDetailFragment @Inject constructor(
setupNotificationView()
setupJumpToReadMarkerView()
setupJumpToBottomView()
roomDetailViewModel.subscribe { renderState(it) }
textComposerViewModel.subscribe { renderTextComposerState(it) }
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
@ -325,15 +329,13 @@ class RoomDetailFragment @Inject constructor(
jumpToBottomView.setOnClickListener {
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
jumpToBottomView.visibility = View.INVISIBLE
withState(roomDetailViewModel) { state ->
if (state.timeline?.isLive == false) {
state.timeline.restartWithEventId(null)
if (!roomDetailViewModel.timeline.isLive) {
roomDetailViewModel.timeline.restartWithEventId(null)
} else {
layoutManager.scrollToPosition(0)
}
}
}
}
private fun setupJumpToReadMarkerView() {
jumpToReadMarkerView.callback = this
@ -431,7 +433,8 @@ class RoomDetailFragment @Inject constructor(
composerLayout.sendButton.setContentDescription(getString(descriptionRes))
avatarRenderer.render(
MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
MatrixItem.UserItem(event.root.senderId
?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
composerLayout.composerRelatedMessageAvatar
)
composerLayout.expand {
@ -481,6 +484,9 @@ class RoomDetailFragment @Inject constructor(
// PRIVATE METHODS *****************************************************************************
private fun setupRecyclerView() {
timelineEventController.callback = this
timelineEventController.timeline = roomDetailViewModel.timeline
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView)
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
@ -514,8 +520,6 @@ class RoomDetailFragment @Inject constructor(
}
})
timelineEventController.callback = this
if (vectorPreferences.swipeToReplyIsEnabled()) {
val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler {
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
@ -789,11 +793,12 @@ class RoomDetailFragment @Inject constructor(
}
private fun renderState(state: RoomDetailViewState) {
Timber.v("Render state summary complete: ${state.asyncRoomSummary.complete}")
renderRoomSummary(state)
val summary = state.asyncRoomSummary()
val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) {
scrollOnHighlightedEventCallback.timeline = state.timeline
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
timelineEventController.update(state)
inviteView.visibility = View.GONE
val uid = session.myUserId
@ -808,9 +813,10 @@ class RoomDetailFragment @Inject constructor(
} else if (state.asyncInviter.complete) {
vectorBaseActivity.finish()
}
val isRoomEncrypted = summary?.isEncrypted ?: false
if (state.tombstoneEvent == null) {
composerLayout.visibility = View.VISIBLE
composerLayout.setRoomEncrypted(state.isEncrypted)
composerLayout.setRoomEncrypted(isRoomEncrypted)
notificationAreaView.render(NotificationAreaView.State.Hidden)
} else {
composerLayout.visibility = View.GONE

View file

@ -20,14 +20,18 @@ import android.net.Uri
import androidx.annotation.IdRes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.airbnb.mvrx.*
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import com.jakewharton.rxrelay2.PublishRelay
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isImageMessage
@ -102,7 +106,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
private var timeline = room.createTimeline(eventId, timelineSettings)
var timeline = room.createTimeline(eventId, timelineSettings)
private set
private val _viewEvents = PublishDataSource<RoomDetailViewEvents>()
val viewEvents: DataSource<RoomDetailViewEvents> = _viewEvents
@ -138,18 +143,17 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
init {
timeline.start()
timeline.addListener(this)
observeRoomSummary()
observeSummaryState()
getUnreadState()
observeSyncState()
observeRoomSummary()
observeEventDisplayedActions()
observeSummaryState()
observeDrafts()
observeUnreadState()
room.getRoomSummaryLive()
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
timeline.addListener(this)
timeline.start()
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
// Inform the SDK that the room is displayed
session.onRoomDisplayed(initialState.roomId)
}
@ -310,7 +314,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
else -> false
}
// PRIVATE METHODS *****************************************************************************
// PRIVATE METHODS *****************************************************************************
private fun handleSendMessage(action: RoomDetailAction.SendMessage) {
withState { state ->
@ -791,10 +795,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
room.rx().liveRoomSummary()
.unwrap()
.execute { async ->
copy(
asyncRoomSummary = async,
isEncrypted = room.isEncrypted()
)
copy(asyncRoomSummary = async)
}
}
@ -880,7 +881,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
override fun onCleared() {
timeline.dispose()
timeline.removeListener(this)
timeline.removeAllListeners()
super.onCleared()
}
}

View file

@ -51,11 +51,9 @@ sealed class UnreadState {
data class RoomDetailViewState(
val roomId: String,
val eventId: String?,
val timeline: Timeline? = null,
val asyncInviter: Async<User> = Uninitialized,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val sendMode: SendMode = SendMode.REGULAR(""),
val isEncrypted: Boolean = false,
val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async<String> = Uninitialized,
val syncState: SyncState = SyncState.Idle,

View file

@ -95,12 +95,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val modelCache = arrayListOf<CacheItemData?>()
private var currentSnapshot: List<TimelineEvent> = emptyList()
private var inSubmitList: Boolean = false
private var timeline: Timeline? = null
private var unreadState: UnreadState = UnreadState.Unknown
private var positionOfReadMarker: Int? = null
private var eventIdToHighlight: String? = null
var callback: Callback? = null
var timeline: Timeline? = null
private val listUpdateCallback = object : ListUpdateCallback {
@ -176,10 +176,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
fun update(viewState: RoomDetailViewState) {
if (timeline?.timelineID != viewState.timeline?.timelineID) {
timeline = viewState.timeline
timeline?.addListener(this)
}
var requestModelBuild = false
if (eventIdToHighlight != viewState.highlightedEventId) {
// Clear cache to force a refresh
@ -205,6 +201,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
timeline?.addListener(this)
timelineMediaSizeProvider.recyclerView = recyclerView
}