Timeline: try to fix some issues with permalink [WIP]
This commit is contained in:
parent
f4ab770be9
commit
5d6d0202a9
@ -42,10 +42,11 @@ 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
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
private const val MIN_FETCHING_COUNT = 30
|
private const val MIN_FETCHING_COUNT = 30
|
||||||
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
|
|
||||||
|
|
||||||
internal class DefaultTimeline(
|
internal class DefaultTimeline(
|
||||||
private val roomId: String,
|
private val roomId: String,
|
||||||
@ -85,8 +86,8 @@ internal class DefaultTimeline(
|
|||||||
|
|
||||||
private var roomEntity: RoomEntity? = null
|
private var roomEntity: RoomEntity? = null
|
||||||
|
|
||||||
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
private var prevDisplayIndex: Int? = null
|
||||||
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
private var nextDisplayIndex: Int? = null
|
||||||
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||||
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
|
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
|
||||||
private val backwardsPaginationState = AtomicReference(PaginationState())
|
private val backwardsPaginationState = AtomicReference(PaginationState())
|
||||||
@ -222,6 +223,7 @@ internal class DefaultTimeline(
|
|||||||
if (isStarted.compareAndSet(true, false)) {
|
if (isStarted.compareAndSet(true, false)) {
|
||||||
eventDecryptor.destroy()
|
eventDecryptor.destroy()
|
||||||
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
|
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
|
||||||
|
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
|
||||||
BACKGROUND_HANDLER.post {
|
BACKGROUND_HANDLER.post {
|
||||||
cancelableBag.cancel()
|
cancelableBag.cancel()
|
||||||
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
|
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
|
||||||
@ -303,11 +305,8 @@ internal class DefaultTimeline(
|
|||||||
private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
|
private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
|
||||||
return Realm.getInstance(realmConfiguration).use { localRealm ->
|
return Realm.getInstance(realmConfiguration).use { localRealm ->
|
||||||
val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction)
|
val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction)
|
||||||
?: return false
|
?: return false
|
||||||
if (direction == Timeline.Direction.FORWARDS) {
|
if (direction == Timeline.Direction.FORWARDS) {
|
||||||
if (findCurrentChunk(localRealm)?.isLastForward == true) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val firstEvent = builtEvents.firstOrNull() ?: return true
|
val firstEvent = builtEvents.firstOrNull() ?: return true
|
||||||
firstEvent.displayIndex < timelineEventEntity.root!!.displayIndex
|
firstEvent.displayIndex < timelineEventEntity.root!!.displayIndex
|
||||||
} else {
|
} else {
|
||||||
@ -334,16 +333,17 @@ internal class DefaultTimeline(
|
|||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
* @return true if createSnapshot should be posted
|
* @return true if createSnapshot should be posted
|
||||||
*/
|
*/
|
||||||
private fun paginateInternal(startDisplayIndex: Int,
|
private fun paginateInternal(startDisplayIndex: Int?,
|
||||||
direction: Timeline.Direction,
|
direction: Timeline.Direction,
|
||||||
count: Int): Boolean {
|
count: Int,
|
||||||
|
strict: Boolean = false): Boolean {
|
||||||
updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) }
|
updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) }
|
||||||
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong())
|
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong(), strict)
|
||||||
val shouldFetchMore = builtCount < count && !hasReachedEnd(direction)
|
val shouldFetchMore = builtCount < count && !hasReachedEnd(direction)
|
||||||
if (shouldFetchMore) {
|
if (shouldFetchMore) {
|
||||||
val newRequestedCount = count - builtCount
|
val newRequestedCount = count - builtCount
|
||||||
updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) }
|
updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) }
|
||||||
val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount)
|
val fetchingCount = max(MIN_FETCHING_COUNT, newRequestedCount)
|
||||||
executePaginationTask(direction, fetchingCount)
|
executePaginationTask(direction, fetchingCount)
|
||||||
} else {
|
} else {
|
||||||
updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) }
|
updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) }
|
||||||
@ -404,20 +404,19 @@ internal class DefaultTimeline(
|
|||||||
.findFirst()
|
.findFirst()
|
||||||
shouldFetchInitialEvent = initialEvent == null
|
shouldFetchInitialEvent = initialEvent == null
|
||||||
initialEvent?.root?.displayIndex
|
initialEvent?.root?.displayIndex
|
||||||
} ?: DISPLAY_INDEX_UNKNOWN
|
}
|
||||||
|
|
||||||
prevDisplayIndex = initialDisplayIndex
|
prevDisplayIndex = initialDisplayIndex
|
||||||
nextDisplayIndex = initialDisplayIndex
|
nextDisplayIndex = initialDisplayIndex
|
||||||
val currentInitialEventId = initialEventId
|
val currentInitialEventId = initialEventId
|
||||||
if (currentInitialEventId != null && shouldFetchInitialEvent) {
|
if (currentInitialEventId != null && shouldFetchInitialEvent) {
|
||||||
fetchEvent(currentInitialEventId)
|
fetchEvent(currentInitialEventId)
|
||||||
} else {
|
} else {
|
||||||
val count = Math.min(settings.initialSize, liveEvents.size)
|
val count = min(settings.initialSize, liveEvents.size)
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
|
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count, strict = false)
|
||||||
} else {
|
} else {
|
||||||
paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2)
|
paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2, strict = false)
|
||||||
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2)
|
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2, strict = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
postSnapshot()
|
postSnapshot()
|
||||||
@ -429,9 +428,9 @@ internal class DefaultTimeline(
|
|||||||
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
|
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
|
||||||
val token = getTokenLive(direction) ?: return
|
val token = getTokenLive(direction) ?: 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
|
||||||
@ -479,14 +478,15 @@ internal class DefaultTimeline(
|
|||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it access realm live results
|
||||||
* @return number of items who have been added
|
* @return number of items who have been added
|
||||||
*/
|
*/
|
||||||
private fun buildTimelineEvents(startDisplayIndex: Int,
|
private fun buildTimelineEvents(startDisplayIndex: Int?,
|
||||||
direction: Timeline.Direction,
|
direction: Timeline.Direction,
|
||||||
count: Long): Int {
|
count: Long,
|
||||||
if (count < 1) {
|
strict: Boolean = false): Int {
|
||||||
|
if (count < 1 || startDisplayIndex == null) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val offsetResults = getOffsetResults(startDisplayIndex, direction, count)
|
val offsetResults = getOffsetResults(startDisplayIndex, direction, count, strict)
|
||||||
if (offsetResults.isEmpty()) {
|
if (offsetResults.isEmpty()) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@ -501,7 +501,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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -527,16 +527,23 @@ internal class DefaultTimeline(
|
|||||||
*/
|
*/
|
||||||
private fun getOffsetResults(startDisplayIndex: Int,
|
private fun getOffsetResults(startDisplayIndex: Int,
|
||||||
direction: Timeline.Direction,
|
direction: Timeline.Direction,
|
||||||
count: Long): RealmResults<TimelineEventEntity> {
|
count: Long,
|
||||||
|
strict: Boolean): RealmResults<TimelineEventEntity> {
|
||||||
val offsetQuery = liveEvents.where()
|
val offsetQuery = liveEvents.where()
|
||||||
if (direction == Timeline.Direction.BACKWARDS) {
|
if (direction == Timeline.Direction.BACKWARDS) {
|
||||||
offsetQuery
|
offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
|
||||||
.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
|
if (strict) {
|
||||||
.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
|
offsetQuery.lessThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
|
||||||
|
} else {
|
||||||
|
offsetQuery.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
offsetQuery
|
offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING)
|
||||||
.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING)
|
if (strict) {
|
||||||
.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
|
offsetQuery.greaterThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
|
||||||
|
} else {
|
||||||
|
offsetQuery.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return offsetQuery
|
return offsetQuery
|
||||||
.limit(count)
|
.limit(count)
|
||||||
@ -589,8 +596,8 @@ internal class DefaultTimeline(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun clearAllValues() {
|
private fun clearAllValues() {
|
||||||
prevDisplayIndex = DISPLAY_INDEX_UNKNOWN
|
prevDisplayIndex = null
|
||||||
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN
|
nextDisplayIndex = null
|
||||||
builtEvents.clear()
|
builtEvents.clear()
|
||||||
builtEventsIdMap.clear()
|
builtEventsIdMap.clear()
|
||||||
backwardsPaginationState.set(PaginationState())
|
backwardsPaginationState.set(PaginationState())
|
||||||
|
@ -20,10 +20,10 @@ import android.os.Handler
|
|||||||
import android.os.HandlerThread
|
import android.os.HandlerThread
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
|
||||||
fun createBackgroundHandler(name: String): Handler = Handler(
|
internal fun createBackgroundHandler(name: String): Handler = Handler(
|
||||||
HandlerThread(name).apply { start() }.looper
|
HandlerThread(name).apply { start() }.looper
|
||||||
)
|
)
|
||||||
|
|
||||||
fun createUIHandler(): Handler = Handler(
|
internal fun createUIHandler(): Handler = Handler(
|
||||||
Looper.getMainLooper()
|
Looper.getMainLooper()
|
||||||
)
|
)
|
44
vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt
Normal file
44
vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
* Copyright 2019 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.riotx.core.utils
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
|
||||||
|
internal class Debouncer(private val handler: Handler) {
|
||||||
|
|
||||||
|
private val runnables = HashMap<String, Runnable>()
|
||||||
|
|
||||||
|
fun debounce(identifier: String, millis: Long, r: Runnable): Boolean {
|
||||||
|
if (runnables.containsKey(identifier)) {
|
||||||
|
// debounce
|
||||||
|
val old = runnables[identifier]
|
||||||
|
handler.removeCallbacks(old)
|
||||||
|
}
|
||||||
|
insertRunnable(identifier, r, millis)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertRunnable(identifier: String, r: Runnable, millis: Long) {
|
||||||
|
val chained = Runnable {
|
||||||
|
handler.post(r)
|
||||||
|
runnables.remove(identifier)
|
||||||
|
}
|
||||||
|
runnables[identifier] = chained
|
||||||
|
handler.postDelayed(chained, millis)
|
||||||
|
}
|
||||||
|
}
|
31
vector/src/main/java/im/vector/riotx/core/utils/Handler.kt
Normal file
31
vector/src/main/java/im/vector/riotx/core/utils/Handler.kt
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
* Copyright 2019 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.riotx.core.utils
|
||||||
|
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.HandlerThread
|
||||||
|
import android.os.Looper
|
||||||
|
|
||||||
|
internal fun createBackgroundHandler(name: String): Handler = Handler(
|
||||||
|
HandlerThread(name).apply { start() }.looper
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun createUIHandler(): Handler = Handler(
|
||||||
|
Looper.getMainLooper()
|
||||||
|
)
|
@ -19,22 +19,17 @@ package im.vector.riotx.features.home.createdirect
|
|||||||
import com.airbnb.epoxy.EpoxyModel
|
import com.airbnb.epoxy.EpoxyModel
|
||||||
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
import com.airbnb.epoxy.paging.PagedListEpoxyController
|
||||||
import com.airbnb.mvrx.Async
|
import com.airbnb.mvrx.Async
|
||||||
import com.airbnb.mvrx.Fail
|
|
||||||
import com.airbnb.mvrx.Incomplete
|
import com.airbnb.mvrx.Incomplete
|
||||||
import com.airbnb.mvrx.Loading
|
|
||||||
import com.airbnb.mvrx.Success
|
|
||||||
import com.airbnb.mvrx.Uninitialized
|
import com.airbnb.mvrx.Uninitialized
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.user.model.User
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
import im.vector.matrix.android.internal.util.createUIHandler
|
|
||||||
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
|
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.epoxy.EmptyItem_
|
import im.vector.riotx.core.epoxy.EmptyItem_
|
||||||
import im.vector.riotx.core.epoxy.errorWithRetryItem
|
|
||||||
import im.vector.riotx.core.epoxy.loadingItem
|
import im.vector.riotx.core.epoxy.loadingItem
|
||||||
import im.vector.riotx.core.epoxy.noResultItem
|
import im.vector.riotx.core.epoxy.noResultItem
|
||||||
import im.vector.riotx.core.error.ErrorFormatter
|
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
import im.vector.riotx.core.utils.createUIHandler
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -202,6 +202,7 @@ class RoomDetailFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val roomDetailArgs: RoomDetailArgs by args()
|
private val roomDetailArgs: RoomDetailArgs by args()
|
||||||
private val glideRequests by lazy {
|
private val glideRequests by lazy {
|
||||||
GlideApp.with(this)
|
GlideApp.with(this)
|
||||||
@ -221,11 +222,13 @@ class RoomDetailFragment :
|
|||||||
@Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory
|
@Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory
|
||||||
@Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory
|
@Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory
|
||||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||||
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
|
||||||
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
|
|
||||||
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
|
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
|
||||||
@Inject lateinit var vectorPreferences: VectorPreferences
|
@Inject lateinit var vectorPreferences: VectorPreferences
|
||||||
|
|
||||||
|
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||||
|
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
|
||||||
|
private lateinit var endlessScrollListener: EndlessRecyclerViewScrollListener
|
||||||
|
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_room_detail
|
override fun getLayoutResId() = R.layout.fragment_room_detail
|
||||||
|
|
||||||
@ -374,17 +377,17 @@ class RoomDetailFragment :
|
|||||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||||
val parser = Parser.builder().build()
|
val parser = Parser.builder().build()
|
||||||
val document = parser.parse(messageContent.formattedBody
|
val document = parser.parse(messageContent.formattedBody
|
||||||
?: messageContent.body)
|
?: messageContent.body)
|
||||||
formattedBody = eventHtmlRenderer.render(document)
|
formattedBody = eventHtmlRenderer.render(document)
|
||||||
}
|
}
|
||||||
composerLayout.composerRelatedMessageContent.text = formattedBody
|
composerLayout.composerRelatedMessageContent.text = formattedBody
|
||||||
?: nonFormattedBody
|
?: nonFormattedBody
|
||||||
|
|
||||||
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
|
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
|
||||||
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
|
||||||
|
|
||||||
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
||||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||||
|
|
||||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||||
composerLayout.expand {
|
composerLayout.expand {
|
||||||
@ -413,9 +416,9 @@ class RoomDetailFragment :
|
|||||||
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
||||||
REACTION_SELECT_REQUEST_CODE -> {
|
REACTION_SELECT_REQUEST_CODE -> {
|
||||||
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
||||||
?: return
|
?: return
|
||||||
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
||||||
?: return
|
?: return
|
||||||
//TODO check if already reacted with that?
|
//TODO check if already reacted with that?
|
||||||
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
|
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
|
||||||
}
|
}
|
||||||
@ -430,7 +433,10 @@ class RoomDetailFragment :
|
|||||||
epoxyVisibilityTracker.attach(recyclerView)
|
epoxyVisibilityTracker.attach(recyclerView)
|
||||||
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
|
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
|
||||||
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
|
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
|
||||||
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
|
endlessScrollListener = EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction ->
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction))
|
||||||
|
}
|
||||||
|
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
|
||||||
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController)
|
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController)
|
||||||
recyclerView.layoutManager = layoutManager
|
recyclerView.layoutManager = layoutManager
|
||||||
recyclerView.itemAnimator = null
|
recyclerView.itemAnimator = null
|
||||||
@ -441,35 +447,32 @@ class RoomDetailFragment :
|
|||||||
it.dispatchTo(scrollOnHighlightedEventCallback)
|
it.dispatchTo(scrollOnHighlightedEventCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
recyclerView.addOnScrollListener(
|
recyclerView.addOnScrollListener(endlessScrollListener)
|
||||||
EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction ->
|
|
||||||
roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction))
|
|
||||||
})
|
|
||||||
recyclerView.setController(timelineEventController)
|
recyclerView.setController(timelineEventController)
|
||||||
timelineEventController.callback = this
|
timelineEventController.callback = this
|
||||||
|
|
||||||
if (vectorPreferences.swipeToReplyIsEnabled()) {
|
if (vectorPreferences.swipeToReplyIsEnabled()) {
|
||||||
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
|
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
|
||||||
R.drawable.ic_reply,
|
R.drawable.ic_reply,
|
||||||
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
|
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
|
||||||
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
|
||||||
(model as? AbsMessageItem)?.attributes?.informationData?.let {
|
(model as? AbsMessageItem)?.attributes?.informationData?.let {
|
||||||
val eventId = it.eventId
|
val eventId = it.eventId
|
||||||
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
|
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
|
||||||
return when (model) {
|
return when (model) {
|
||||||
is MessageFileItem,
|
is MessageFileItem,
|
||||||
is MessageImageVideoItem,
|
is MessageImageVideoItem,
|
||||||
is MessageTextItem -> {
|
is MessageTextItem -> {
|
||||||
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
|
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
|
||||||
}
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
val touchHelper = ItemTouchHelper(swipeCallback)
|
val touchHelper = ItemTouchHelper(swipeCallback)
|
||||||
touchHelper.attachToRecyclerView(recyclerView)
|
touchHelper.attachToRecyclerView(recyclerView)
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import im.vector.riotx.core.platform.DefaultListUpdateCallback
|
import im.vector.riotx.core.platform.DefaultListUpdateCallback
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager,
|
class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager,
|
||||||
@ -28,17 +29,16 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa
|
|||||||
|
|
||||||
override fun onChanged(position: Int, count: Int, tag: Any?) {
|
override fun onChanged(position: Int, count: Int, tag: Any?) {
|
||||||
val eventId = scheduledEventId.get() ?: return
|
val eventId = scheduledEventId.get() ?: return
|
||||||
|
|
||||||
val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
|
val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
|
||||||
|
|
||||||
if (positionToScroll != null) {
|
if (positionToScroll != null) {
|
||||||
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
|
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||||
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
|
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
|
||||||
|
|
||||||
// Do not scroll it item is already visible
|
// Do not scroll it item is already visible
|
||||||
if (positionToScroll !in firstVisibleItem..lastVisibleItem) {
|
if (positionToScroll !in firstVisibleItem..lastVisibleItem) {
|
||||||
|
Timber.v("Scroll to $positionToScroll")
|
||||||
// Note: Offset will be from the bottom, since the layoutManager is reversed
|
// Note: Offset will be from the bottom, since the layoutManager is reversed
|
||||||
layoutManager.scrollToPosition(position)
|
layoutManager.scrollToPosition(positionToScroll)
|
||||||
}
|
}
|
||||||
scheduledEventId.set(null)
|
scheduledEventId.set(null)
|
||||||
}
|
}
|
||||||
|
@ -18,11 +18,15 @@ package im.vector.riotx.features.home.room.detail
|
|||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import im.vector.riotx.core.platform.DefaultListUpdateCallback
|
import im.vector.riotx.core.platform.DefaultListUpdateCallback
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback {
|
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
|
||||||
|
private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback {
|
||||||
|
|
||||||
override fun onInserted(position: Int, count: Int) {
|
override fun onInserted(position: Int, count: Int) {
|
||||||
if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) {
|
Timber.v("On inserted $count count at position: $position")
|
||||||
|
if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) {
|
||||||
layoutManager.scrollToPosition(0)
|
layoutManager.scrollToPosition(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,12 +35,7 @@ import im.vector.riotx.features.home.AvatarRenderer
|
|||||||
import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
|
import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory
|
import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.*
|
import im.vector.riotx.features.home.room.detail.timeline.helper.*
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem
|
import im.vector.riotx.features.home.room.detail.timeline.item.*
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
|
||||||
import im.vector.riotx.features.media.ImageContentRenderer
|
import im.vector.riotx.features.media.ImageContentRenderer
|
||||||
import im.vector.riotx.features.media.VideoContentRenderer
|
import im.vector.riotx.features.media.VideoContentRenderer
|
||||||
import org.threeten.bp.LocalDateTime
|
import org.threeten.bp.LocalDateTime
|
||||||
@ -91,8 +86,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
fun onUrlLongClicked(url: String): Boolean
|
fun onUrlLongClicked(url: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var showingForwardLoader = false
|
||||||
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 timeline: Timeline? = null
|
||||||
@ -163,7 +158,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
synchronized(modelCache) {
|
synchronized(modelCache) {
|
||||||
for (i in 0 until modelCache.size) {
|
for (i in 0 until modelCache.size) {
|
||||||
if (modelCache[i]?.eventId == eventIdToHighlight
|
if (modelCache[i]?.eventId == eventIdToHighlight
|
||||||
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
|
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
|
||||||
modelCache[i] = null
|
modelCache[i] = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -182,17 +177,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun buildModels() {
|
override fun buildModels() {
|
||||||
val loaderAdded = LoadingItem_()
|
val timestamp = System.currentTimeMillis()
|
||||||
.id("forward_loading_item")
|
showingForwardLoader = LoadingItem_()
|
||||||
|
.id("forward_loading_item_$timestamp")
|
||||||
.addWhen(Timeline.Direction.FORWARDS)
|
.addWhen(Timeline.Direction.FORWARDS)
|
||||||
|
|
||||||
val timelineModels = getModels()
|
val timelineModels = getModels()
|
||||||
add(timelineModels)
|
add(timelineModels)
|
||||||
|
|
||||||
// Avoid displaying two loaders if there is no elements between them
|
// Avoid displaying two loaders if there is no elements between them
|
||||||
if (!loaderAdded || timelineModels.isNotEmpty()) {
|
if (!showingForwardLoader || timelineModels.isNotEmpty()) {
|
||||||
LoadingItem_()
|
LoadingItem_()
|
||||||
.id("backward_loading_item")
|
.id("backward_loading_item_$timestamp")
|
||||||
.addWhen(Timeline.Direction.BACKWARDS)
|
.addWhen(Timeline.Direction.BACKWARDS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -224,8 +220,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
|
||||||
// We then are sure we always have items up to date.
|
// We then are sure we always have items up to date.
|
||||||
if (modelCache[position] == null
|
if (modelCache[position] == null
|
||||||
|| modelCache[position]?.mergedHeaderModel != null
|
|| modelCache[position]?.mergedHeaderModel != null
|
||||||
|| modelCache[position]?.formattedDayModel != null) {
|
|| modelCache[position]?.formattedDayModel != null) {
|
||||||
modelCache[position] = buildItemModels(position, currentSnapshot)
|
modelCache[position] = buildItemModels(position, currentSnapshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -255,7 +251,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
it.id(event.localId)
|
it.id(event.localId)
|
||||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||||
}
|
}
|
||||||
val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent, items, addDaySeparator, currentPosition, callback){
|
val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent, items, addDaySeparator, currentPosition, callback) {
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
}
|
}
|
||||||
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
|
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
|
||||||
@ -284,6 +280,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) {
|
fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) {
|
||||||
// Search in the cache
|
// Search in the cache
|
||||||
var realPosition = 0
|
var realPosition = 0
|
||||||
|
if (showingForwardLoader) {
|
||||||
|
realPosition++
|
||||||
|
}
|
||||||
for (i in 0 until modelCache.size) {
|
for (i in 0 until modelCache.size) {
|
||||||
val itemCache = modelCache[i]
|
val itemCache = modelCache[i]
|
||||||
if (itemCache?.eventId == eventId) {
|
if (itemCache?.eventId == eventId) {
|
||||||
@ -319,6 +318,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
return modelCache.getOrNull(position - offsetValue)?.eventId
|
return modelCache.getOrNull(position - offsetValue)?.eventId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isLoadingForward() = showingForwardLoader
|
||||||
|
|
||||||
private data class CacheItemData(
|
private data class CacheItemData(
|
||||||
val localId: Long,
|
val localId: Long,
|
||||||
val eventId: String?,
|
val eventId: String?,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user