Work on timeline
This commit is contained in:
parent
99c523b710
commit
f9487f8995
|
@ -41,7 +41,8 @@ data class RoomSummary(
|
||||||
val membership: Membership = Membership.NONE,
|
val membership: Membership = Membership.NONE,
|
||||||
val versioningState: VersioningState = VersioningState.NONE,
|
val versioningState: VersioningState = VersioningState.NONE,
|
||||||
val readMarkerId: String? = null,
|
val readMarkerId: String? = null,
|
||||||
val userDrafts: List<UserDraft> = emptyList()
|
val userDrafts: List<UserDraft> = emptyList(),
|
||||||
|
var isEncrypted: Boolean
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val isVersioned: 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 localId = TimelineEventEntity.nextId(realm)
|
||||||
val eventId = event.eventId ?: ""
|
val eventId = event.eventId ?: ""
|
||||||
val senderId = event.senderId ?: ""
|
val senderId = event.senderId ?: ""
|
||||||
|
@ -121,7 +121,7 @@ internal fun ChunkEntity.add(roomId: String,
|
||||||
this.stateIndex = currentStateIndex
|
this.stateIndex = currentStateIndex
|
||||||
this.displayIndex = currentDisplayIndex
|
this.displayIndex = currentDisplayIndex
|
||||||
this.sendState = SendState.SYNCED
|
this.sendState = SendState.SYNCED
|
||||||
this.isUnlinked = isUnlinked
|
this.isUnlinked = isChunkUnlinked
|
||||||
}
|
}
|
||||||
val eventEntity = realm.createObject<TimelineEventEntity>().also {
|
val eventEntity = realm.createObject<TimelineEventEntity>().also {
|
||||||
it.localId = localId
|
it.localId = localId
|
||||||
|
|
|
@ -70,7 +70,8 @@ internal class RoomSummaryMapper @Inject constructor(
|
||||||
readMarkerId = roomSummaryEntity.readMarkerId,
|
readMarkerId = roomSummaryEntity.readMarkerId,
|
||||||
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
|
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
|
||||||
canonicalAlias = roomSummaryEntity.canonicalAlias,
|
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 canonicalAlias: String? = null,
|
||||||
var aliases: RealmList<String> = RealmList(),
|
var aliases: RealmList<String> = RealmList(),
|
||||||
// this is required for querying
|
// this is required for querying
|
||||||
var flatAliases: String = ""
|
var flatAliases: String = "",
|
||||||
|
var isEncrypted: Boolean = false
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
private var membershipStr: String = Membership.NONE.name
|
private var membershipStr: String = Membership.NONE.name
|
||||||
|
|
|
@ -93,10 +93,12 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
|
||||||
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
|
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
|
||||||
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_CANONICAL_ALIAS).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 lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
|
||||||
|
val encryptionEvent = EventEntity.where(realm, roomId, EventType.ENCRYPTION).prev()
|
||||||
|
|
||||||
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
|
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
|
||||||
// avoid this call if we are sure there are unread events
|
// avoid this call if we are sure there are unread events
|
||||||
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
|
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
|
||||||
|
|
||||||
|
|
||||||
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
|
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
|
||||||
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
|
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
|
||||||
|
@ -105,10 +107,12 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
|
||||||
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
|
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
|
||||||
?.canonicalAlias
|
?.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.clear()
|
||||||
roomSummaryEntity.aliases.addAll(roomAliases)
|
roomSummaryEntity.aliases.addAll(roomAliases)
|
||||||
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
|
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
|
||||||
|
roomSummaryEntity.isEncrypted = encryptionEvent != null
|
||||||
|
|
||||||
if (updateMembers) {
|
if (updateMembers) {
|
||||||
val otherRoomMembers = RoomMembers(realm, roomId)
|
val otherRoomMembers = RoomMembers(realm, roomId)
|
||||||
|
|
|
@ -52,8 +52,8 @@ import io.realm.RealmQuery
|
||||||
import io.realm.RealmResults
|
import io.realm.RealmResults
|
||||||
import io.realm.Sort
|
import io.realm.Sort
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.Collections
|
import java.util.*
|
||||||
import java.util.UUID
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
@ -77,11 +77,9 @@ internal class DefaultTimeline(
|
||||||
private val hiddenReadReceipts: TimelineHiddenReadReceipts
|
private val hiddenReadReceipts: TimelineHiddenReadReceipts
|
||||||
) : Timeline, TimelineHiddenReadReceipts.Delegate {
|
) : Timeline, TimelineHiddenReadReceipts.Delegate {
|
||||||
|
|
||||||
private companion object {
|
val backgroundHandler = createBackgroundHandler("TIMELINE_DB_THREAD_${System.currentTimeMillis()}")
|
||||||
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
|
|
||||||
}
|
|
||||||
|
|
||||||
private val listeners = ArrayList<Timeline.Listener>()
|
private val listeners = CopyOnWriteArrayList<Timeline.Listener>()
|
||||||
private val isStarted = AtomicBoolean(false)
|
private val isStarted = AtomicBoolean(false)
|
||||||
private val isReady = AtomicBoolean(false)
|
private val isReady = AtomicBoolean(false)
|
||||||
private val mainHandler = createUIHandler()
|
private val mainHandler = createUIHandler()
|
||||||
|
@ -137,7 +135,7 @@ internal class DefaultTimeline(
|
||||||
// Public methods ******************************************************************************
|
// Public methods ******************************************************************************
|
||||||
|
|
||||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||||
BACKGROUND_HANDLER.post {
|
backgroundHandler.post {
|
||||||
if (!canPaginate(direction)) {
|
if (!canPaginate(direction)) {
|
||||||
return@post
|
return@post
|
||||||
}
|
}
|
||||||
|
@ -165,7 +163,7 @@ internal class DefaultTimeline(
|
||||||
override fun start() {
|
override fun start() {
|
||||||
if (isStarted.compareAndSet(false, true)) {
|
if (isStarted.compareAndSet(false, true)) {
|
||||||
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
|
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
|
||||||
BACKGROUND_HANDLER.post {
|
backgroundHandler.post {
|
||||||
eventDecryptor.start()
|
eventDecryptor.start()
|
||||||
val realm = Realm.getInstance(realmConfiguration)
|
val realm = Realm.getInstance(realmConfiguration)
|
||||||
backgroundRealm.set(realm)
|
backgroundRealm.set(realm)
|
||||||
|
@ -199,8 +197,8 @@ internal class DefaultTimeline(
|
||||||
isReady.set(false)
|
isReady.set(false)
|
||||||
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
|
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
|
||||||
cancelableBag.cancel()
|
cancelableBag.cancel()
|
||||||
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
|
backgroundHandler.removeCallbacksAndMessages(null)
|
||||||
BACKGROUND_HANDLER.post {
|
backgroundHandler.post {
|
||||||
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
|
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
|
||||||
if (this::eventRelations.isInitialized) {
|
if (this::eventRelations.isInitialized) {
|
||||||
eventRelations.removeAllChangeListeners()
|
eventRelations.removeAllChangeListeners()
|
||||||
|
@ -288,20 +286,20 @@ internal class DefaultTimeline(
|
||||||
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun addListener(listener: Timeline.Listener) = synchronized(listeners) {
|
override fun addListener(listener: Timeline.Listener): Boolean {
|
||||||
if (listeners.contains(listener)) {
|
if (listeners.contains(listener)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
listeners.add(listener).also {
|
return listeners.add(listener).also {
|
||||||
postSnapshot()
|
postSnapshot()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) {
|
override fun removeListener(listener: Timeline.Listener): Boolean {
|
||||||
listeners.remove(listener)
|
return listeners.remove(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeAllListeners() = synchronized(listeners) {
|
override fun removeAllListeners() {
|
||||||
listeners.clear()
|
listeners.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -497,9 +495,9 @@ internal class DefaultTimeline(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val params = PaginationTask.Params(roomId = roomId,
|
val params = PaginationTask.Params(roomId = roomId,
|
||||||
from = token,
|
from = token,
|
||||||
direction = direction.toPaginationDirection(),
|
direction = direction.toPaginationDirection(),
|
||||||
limit = limit)
|
limit = limit)
|
||||||
|
|
||||||
Timber.v("Should fetch $limit items $direction")
|
Timber.v("Should fetch $limit items $direction")
|
||||||
cancelableBag += paginationTask
|
cancelableBag += paginationTask
|
||||||
|
@ -516,7 +514,7 @@ internal class DefaultTimeline(
|
||||||
}
|
}
|
||||||
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
|
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
|
||||||
// Database won't be updated, so we force pagination request
|
// Database won't be updated, so we force pagination request
|
||||||
BACKGROUND_HANDLER.post {
|
backgroundHandler.post {
|
||||||
executePaginationTask(direction, limit)
|
executePaginationTask(direction, limit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -575,7 +573,7 @@ internal class DefaultTimeline(
|
||||||
val timelineEvent = buildTimelineEvent(eventEntity)
|
val timelineEvent = buildTimelineEvent(eventEntity)
|
||||||
|
|
||||||
if (timelineEvent.isEncrypted()
|
if (timelineEvent.isEncrypted()
|
||||||
&& timelineEvent.root.mxDecryptionResult == null) {
|
&& timelineEvent.root.mxDecryptionResult == null) {
|
||||||
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
|
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -649,17 +647,15 @@ internal class DefaultTimeline(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun postSnapshot() {
|
private fun postSnapshot() {
|
||||||
BACKGROUND_HANDLER.post {
|
backgroundHandler.post {
|
||||||
if (isReady.get().not()) {
|
if (isReady.get().not()) {
|
||||||
return@post
|
return@post
|
||||||
}
|
}
|
||||||
updateLoadingStates(filteredEvents)
|
updateLoadingStates(filteredEvents)
|
||||||
val snapshot = createSnapshot()
|
val snapshot = createSnapshot()
|
||||||
val runnable = Runnable {
|
val runnable = Runnable {
|
||||||
synchronized(listeners) {
|
listeners.forEach {
|
||||||
listeners.forEach {
|
it.onTimelineUpdated(snapshot)
|
||||||
it.onTimelineUpdated(snapshot)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debouncer.debounce("post_snapshot", runnable, 50)
|
debouncer.debounce("post_snapshot", runnable, 50)
|
||||||
|
@ -671,10 +667,8 @@ internal class DefaultTimeline(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val runnable = Runnable {
|
val runnable = Runnable {
|
||||||
synchronized(listeners) {
|
listeners.forEach {
|
||||||
listeners.forEach {
|
it.onTimelineFailure(throwable)
|
||||||
it.onTimelineFailure(throwable)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mainHandler.post(runnable)
|
mainHandler.post(runnable)
|
||||||
|
|
|
@ -40,6 +40,7 @@ import androidx.core.util.Pair
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.observe
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.MatrixItem
|
||||||
import im.vector.matrix.android.api.util.toMatrixItem
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
|
import im.vector.matrix.android.api.util.toRoomAliasMatrixItem
|
||||||
|
import im.vector.matrix.rx.rx
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.dialogs.withColoredButton
|
import im.vector.riotx.core.dialogs.withColoredButton
|
||||||
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
|
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
|
||||||
|
@ -225,6 +227,8 @@ class RoomDetailFragment @Inject constructor(
|
||||||
setupNotificationView()
|
setupNotificationView()
|
||||||
setupJumpToReadMarkerView()
|
setupJumpToReadMarkerView()
|
||||||
setupJumpToBottomView()
|
setupJumpToBottomView()
|
||||||
|
|
||||||
|
|
||||||
roomDetailViewModel.subscribe { renderState(it) }
|
roomDetailViewModel.subscribe { renderState(it) }
|
||||||
textComposerViewModel.subscribe { renderTextComposerState(it) }
|
textComposerViewModel.subscribe { renderTextComposerState(it) }
|
||||||
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
|
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
|
||||||
|
@ -325,12 +329,10 @@ class RoomDetailFragment @Inject constructor(
|
||||||
jumpToBottomView.setOnClickListener {
|
jumpToBottomView.setOnClickListener {
|
||||||
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
|
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
|
||||||
jumpToBottomView.visibility = View.INVISIBLE
|
jumpToBottomView.visibility = View.INVISIBLE
|
||||||
withState(roomDetailViewModel) { state ->
|
if (!roomDetailViewModel.timeline.isLive) {
|
||||||
if (state.timeline?.isLive == false) {
|
roomDetailViewModel.timeline.restartWithEventId(null)
|
||||||
state.timeline.restartWithEventId(null)
|
} else {
|
||||||
} else {
|
layoutManager.scrollToPosition(0)
|
||||||
layoutManager.scrollToPosition(0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -343,9 +345,9 @@ class RoomDetailFragment @Inject constructor(
|
||||||
AlertDialog.Builder(requireActivity())
|
AlertDialog.Builder(requireActivity())
|
||||||
.setTitle(R.string.dialog_title_error)
|
.setTitle(R.string.dialog_title_error)
|
||||||
.setMessage(getString(R.string.error_file_too_big,
|
.setMessage(getString(R.string.error_file_too_big,
|
||||||
error.filename,
|
error.filename,
|
||||||
TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes),
|
TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes),
|
||||||
TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes)
|
TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes)
|
||||||
))
|
))
|
||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
|
@ -431,7 +433,8 @@ class RoomDetailFragment @Inject constructor(
|
||||||
composerLayout.sendButton.setContentDescription(getString(descriptionRes))
|
composerLayout.sendButton.setContentDescription(getString(descriptionRes))
|
||||||
|
|
||||||
avatarRenderer.render(
|
avatarRenderer.render(
|
||||||
MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
|
MatrixItem.UserItem(event.root.senderId
|
||||||
|
?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
|
||||||
composerLayout.composerRelatedMessageAvatar
|
composerLayout.composerRelatedMessageAvatar
|
||||||
)
|
)
|
||||||
composerLayout.expand {
|
composerLayout.expand {
|
||||||
|
@ -449,7 +452,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
// Ignore update to avoid saving a draft
|
// Ignore update to avoid saving a draft
|
||||||
composerLayout.composerEditText.setText(text)
|
composerLayout.composerEditText.setText(text)
|
||||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length
|
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length
|
||||||
?: 0)
|
?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -481,6 +484,9 @@ class RoomDetailFragment @Inject constructor(
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
|
timelineEventController.callback = this
|
||||||
|
timelineEventController.timeline = roomDetailViewModel.timeline
|
||||||
|
|
||||||
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
|
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
|
||||||
epoxyVisibilityTracker.attach(recyclerView)
|
epoxyVisibilityTracker.attach(recyclerView)
|
||||||
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
|
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
|
||||||
|
@ -514,8 +520,6 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
timelineEventController.callback = this
|
|
||||||
|
|
||||||
if (vectorPreferences.swipeToReplyIsEnabled()) {
|
if (vectorPreferences.swipeToReplyIsEnabled()) {
|
||||||
val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler {
|
val quickReplyHandler = object : RoomMessageTouchHelperCallback.QuickReplayHandler {
|
||||||
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
||||||
|
@ -789,11 +793,12 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderState(state: RoomDetailViewState) {
|
private fun renderState(state: RoomDetailViewState) {
|
||||||
|
Timber.v("Render state summary complete: ${state.asyncRoomSummary.complete}")
|
||||||
renderRoomSummary(state)
|
renderRoomSummary(state)
|
||||||
val summary = state.asyncRoomSummary()
|
val summary = state.asyncRoomSummary()
|
||||||
val inviter = state.asyncInviter()
|
val inviter = state.asyncInviter()
|
||||||
if (summary?.membership == Membership.JOIN) {
|
if (summary?.membership == Membership.JOIN) {
|
||||||
scrollOnHighlightedEventCallback.timeline = state.timeline
|
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
|
||||||
timelineEventController.update(state)
|
timelineEventController.update(state)
|
||||||
inviteView.visibility = View.GONE
|
inviteView.visibility = View.GONE
|
||||||
val uid = session.myUserId
|
val uid = session.myUserId
|
||||||
|
@ -808,9 +813,10 @@ class RoomDetailFragment @Inject constructor(
|
||||||
} else if (state.asyncInviter.complete) {
|
} else if (state.asyncInviter.complete) {
|
||||||
vectorBaseActivity.finish()
|
vectorBaseActivity.finish()
|
||||||
}
|
}
|
||||||
|
val isRoomEncrypted = summary?.isEncrypted ?: false
|
||||||
if (state.tombstoneEvent == null) {
|
if (state.tombstoneEvent == null) {
|
||||||
composerLayout.visibility = View.VISIBLE
|
composerLayout.visibility = View.VISIBLE
|
||||||
composerLayout.setRoomEncrypted(state.isEncrypted)
|
composerLayout.setRoomEncrypted(isRoomEncrypted)
|
||||||
notificationAreaView.render(NotificationAreaView.State.Hidden)
|
notificationAreaView.render(NotificationAreaView.State.Hidden)
|
||||||
} else {
|
} else {
|
||||||
composerLayout.visibility = View.GONE
|
composerLayout.visibility = View.GONE
|
||||||
|
@ -1312,7 +1318,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
val startToCompose = composerLayout.composerEditText.text.isNullOrBlank()
|
val startToCompose = composerLayout.composerEditText.text.isNullOrBlank()
|
||||||
|
|
||||||
if (startToCompose
|
if (startToCompose
|
||||||
&& userId == session.myUserId) {
|
&& userId == session.myUserId) {
|
||||||
// Empty composer, current user: start an emote
|
// Empty composer, current user: start an emote
|
||||||
composerLayout.composerEditText.setText(Command.EMOTE.command + " ")
|
composerLayout.composerEditText.setText(Command.EMOTE.command + " ")
|
||||||
composerLayout.composerEditText.setSelection(Command.EMOTE.length)
|
composerLayout.composerEditText.setSelection(Command.EMOTE.length)
|
||||||
|
|
|
@ -20,14 +20,18 @@ import android.net.Uri
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
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.BehaviorRelay
|
||||||
import com.jakewharton.rxrelay2.PublishRelay
|
import com.jakewharton.rxrelay2.PublishRelay
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.MatrixPatterns
|
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.Session
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.isImageMessage
|
import im.vector.matrix.android.api.session.events.model.isImageMessage
|
||||||
|
@ -89,20 +93,21 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>()
|
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>()
|
||||||
private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
||||||
TimelineSettings(30,
|
TimelineSettings(30,
|
||||||
filterEdits = false,
|
filterEdits = false,
|
||||||
filterTypes = true,
|
filterTypes = true,
|
||||||
allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES,
|
allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES,
|
||||||
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
|
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
|
||||||
} else {
|
} else {
|
||||||
TimelineSettings(30,
|
TimelineSettings(30,
|
||||||
filterEdits = true,
|
filterEdits = true,
|
||||||
filterTypes = true,
|
filterTypes = true,
|
||||||
allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES,
|
allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES,
|
||||||
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
|
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
|
||||||
}
|
}
|
||||||
|
|
||||||
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
|
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>()
|
private val _viewEvents = PublishDataSource<RoomDetailViewEvents>()
|
||||||
val viewEvents: DataSource<RoomDetailViewEvents> = _viewEvents
|
val viewEvents: DataSource<RoomDetailViewEvents> = _viewEvents
|
||||||
|
@ -138,18 +143,17 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
timeline.start()
|
||||||
|
timeline.addListener(this)
|
||||||
|
observeRoomSummary()
|
||||||
|
observeSummaryState()
|
||||||
getUnreadState()
|
getUnreadState()
|
||||||
observeSyncState()
|
observeSyncState()
|
||||||
observeRoomSummary()
|
|
||||||
observeEventDisplayedActions()
|
observeEventDisplayedActions()
|
||||||
observeSummaryState()
|
|
||||||
observeDrafts()
|
observeDrafts()
|
||||||
observeUnreadState()
|
observeUnreadState()
|
||||||
|
room.getRoomSummaryLive()
|
||||||
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
|
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
|
||||||
timeline.addListener(this)
|
|
||||||
timeline.start()
|
|
||||||
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
|
||||||
|
|
||||||
// Inform the SDK that the room is displayed
|
// Inform the SDK that the room is displayed
|
||||||
session.onRoomDisplayed(initialState.roomId)
|
session.onRoomDisplayed(initialState.roomId)
|
||||||
}
|
}
|
||||||
|
@ -233,23 +237,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
copy(
|
copy(
|
||||||
// Create a sendMode from a draft and retrieve the TimelineEvent
|
// Create a sendMode from a draft and retrieve the TimelineEvent
|
||||||
sendMode = when (draft) {
|
sendMode = when (draft) {
|
||||||
is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
|
is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
|
||||||
is UserDraft.QUOTE -> {
|
is UserDraft.QUOTE -> {
|
||||||
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
|
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
|
||||||
SendMode.QUOTE(timelineEvent, draft.text)
|
SendMode.QUOTE(timelineEvent, draft.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is UserDraft.REPLY -> {
|
is UserDraft.REPLY -> {
|
||||||
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
|
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
|
||||||
SendMode.REPLY(timelineEvent, draft.text)
|
SendMode.REPLY(timelineEvent, draft.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is UserDraft.EDIT -> {
|
is UserDraft.EDIT -> {
|
||||||
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
|
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
|
||||||
SendMode.EDIT(timelineEvent, draft.text)
|
SendMode.EDIT(timelineEvent, draft.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} ?: SendMode.REGULAR("")
|
} ?: SendMode.REGULAR("")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -258,7 +262,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
|
|
||||||
private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) {
|
private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) {
|
||||||
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
|
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
|
||||||
?: return
|
?: return
|
||||||
|
|
||||||
val roomId = tombstoneContent.replacementRoom ?: ""
|
val roomId = tombstoneContent.replacementRoom ?: ""
|
||||||
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
|
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
|
||||||
|
@ -310,7 +314,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
private fun handleSendMessage(action: RoomDetailAction.SendMessage) {
|
private fun handleSendMessage(action: RoomDetailAction.SendMessage) {
|
||||||
withState { state ->
|
withState { state ->
|
||||||
|
@ -396,7 +400,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
is SendMode.EDIT -> {
|
is SendMode.EDIT -> {
|
||||||
// is original event a reply?
|
// is original event a reply?
|
||||||
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
|
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
|
||||||
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
|
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
|
||||||
if (inReplyTo != null) {
|
if (inReplyTo != null) {
|
||||||
// TODO check if same content?
|
// TODO check if same content?
|
||||||
room.getTimeLineEvent(inReplyTo)?.let {
|
room.getTimeLineEvent(inReplyTo)?.let {
|
||||||
|
@ -405,13 +409,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
} else {
|
} else {
|
||||||
val messageContent: MessageContent? =
|
val messageContent: MessageContent? =
|
||||||
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||||
val existingBody = messageContent?.body ?: ""
|
val existingBody = messageContent?.body ?: ""
|
||||||
if (existingBody != action.text) {
|
if (existingBody != action.text) {
|
||||||
room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "",
|
room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "",
|
||||||
messageContent?.type ?: MessageType.MSGTYPE_TEXT,
|
messageContent?.type ?: MessageType.MSGTYPE_TEXT,
|
||||||
action.text,
|
action.text,
|
||||||
action.autoMarkdown)
|
action.autoMarkdown)
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Same message content, do not send edition")
|
Timber.w("Same message content, do not send edition")
|
||||||
}
|
}
|
||||||
|
@ -422,7 +426,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
is SendMode.QUOTE -> {
|
is SendMode.QUOTE -> {
|
||||||
val messageContent: MessageContent? =
|
val messageContent: MessageContent? =
|
||||||
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
|
||||||
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
|
||||||
val textMsg = messageContent?.body
|
val textMsg = messageContent?.body
|
||||||
|
|
||||||
val finalText = legacyRiotQuoteText(textMsg, action.text.toString())
|
val finalText = legacyRiotQuoteText(textMsg, action.text.toString())
|
||||||
|
@ -538,7 +542,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
||||||
null -> room.sendMedias(attachments)
|
null -> room.sendMedias(attachments)
|
||||||
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name
|
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name
|
||||||
?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
|
?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -728,7 +732,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
.filter { it.isNotEmpty() }
|
.filter { it.isNotEmpty() }
|
||||||
.subscribeBy(onNext = { actions ->
|
.subscribeBy(onNext = { actions ->
|
||||||
val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event
|
val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event
|
||||||
?: return@subscribeBy
|
?: return@subscribeBy
|
||||||
val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent
|
val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent
|
||||||
if (trackUnreadMessages.get()) {
|
if (trackUnreadMessages.get()) {
|
||||||
if (globalMostRecentDisplayedEvent == null) {
|
if (globalMostRecentDisplayedEvent == null) {
|
||||||
|
@ -791,10 +795,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
room.rx().liveRoomSummary()
|
room.rx().liveRoomSummary()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.execute { async ->
|
.execute { async ->
|
||||||
copy(
|
copy(asyncRoomSummary = async)
|
||||||
asyncRoomSummary = async,
|
|
||||||
isEncrypted = room.isEncrypted()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -880,7 +881,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
timeline.dispose()
|
timeline.dispose()
|
||||||
timeline.removeListener(this)
|
timeline.removeAllListeners()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,11 +51,9 @@ sealed class UnreadState {
|
||||||
data class RoomDetailViewState(
|
data class RoomDetailViewState(
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String?,
|
val eventId: String?,
|
||||||
val timeline: Timeline? = null,
|
|
||||||
val asyncInviter: Async<User> = Uninitialized,
|
val asyncInviter: Async<User> = Uninitialized,
|
||||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||||
val sendMode: SendMode = SendMode.REGULAR(""),
|
val sendMode: SendMode = SendMode.REGULAR(""),
|
||||||
val isEncrypted: Boolean = false,
|
|
||||||
val tombstoneEvent: Event? = null,
|
val tombstoneEvent: Event? = null,
|
||||||
val tombstoneEventHandling: Async<String> = Uninitialized,
|
val tombstoneEventHandling: Async<String> = Uninitialized,
|
||||||
val syncState: SyncState = SyncState.Idle,
|
val syncState: SyncState = SyncState.Idle,
|
||||||
|
|
|
@ -95,12 +95,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
private val modelCache = arrayListOf<CacheItemData?>()
|
private val modelCache = arrayListOf<CacheItemData?>()
|
||||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||||
private var inSubmitList: Boolean = false
|
private var inSubmitList: Boolean = false
|
||||||
private var timeline: Timeline? = null
|
|
||||||
private var unreadState: UnreadState = UnreadState.Unknown
|
private var unreadState: UnreadState = UnreadState.Unknown
|
||||||
private var positionOfReadMarker: Int? = null
|
private var positionOfReadMarker: Int? = null
|
||||||
private var eventIdToHighlight: String? = null
|
private var eventIdToHighlight: String? = null
|
||||||
|
|
||||||
var callback: Callback? = null
|
var callback: Callback? = null
|
||||||
|
var timeline: Timeline? = null
|
||||||
|
|
||||||
private val listUpdateCallback = object : ListUpdateCallback {
|
private val listUpdateCallback = object : ListUpdateCallback {
|
||||||
|
|
||||||
|
@ -176,10 +176,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(viewState: RoomDetailViewState) {
|
fun update(viewState: RoomDetailViewState) {
|
||||||
if (timeline?.timelineID != viewState.timeline?.timelineID) {
|
|
||||||
timeline = viewState.timeline
|
|
||||||
timeline?.addListener(this)
|
|
||||||
}
|
|
||||||
var requestModelBuild = false
|
var requestModelBuild = false
|
||||||
if (eventIdToHighlight != viewState.highlightedEventId) {
|
if (eventIdToHighlight != viewState.highlightedEventId) {
|
||||||
// Clear cache to force a refresh
|
// Clear cache to force a refresh
|
||||||
|
@ -205,6 +201,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
|
|
||||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||||
super.onAttachedToRecyclerView(recyclerView)
|
super.onAttachedToRecyclerView(recyclerView)
|
||||||
|
timeline?.addListener(this)
|
||||||
timelineMediaSizeProvider.recyclerView = recyclerView
|
timelineMediaSizeProvider.recyclerView = recyclerView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue