mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 18:35:40 +03:00
Work on timeline
This commit is contained in:
parent
99c523b710
commit
f9487f8995
10 changed files with 112 additions and 109 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue