Timeline\ReadMarker: continue fixing issues
|
@ -45,7 +45,7 @@ interface Timeline {
|
||||||
fun dispose()
|
fun dispose()
|
||||||
|
|
||||||
|
|
||||||
fun restartWithEventId(eventId: String)
|
fun restartWithEventId(eventId: String?)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -26,7 +26,6 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.find
|
import im.vector.matrix.android.internal.database.query.find
|
||||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
|
||||||
import im.vector.matrix.android.internal.database.query.latestEvent
|
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.network.executeRequest
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
|
@ -36,7 +35,6 @@ import im.vector.matrix.android.internal.session.sync.RoomFullyReadHandler
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
|
@ -114,7 +114,7 @@ internal class DefaultTimeline(
|
||||||
private val timelineID = UUID.randomUUID().toString()
|
private val timelineID = UUID.randomUUID().toString()
|
||||||
|
|
||||||
override val isLive
|
override val isLive
|
||||||
get() = initialEventId == null
|
get() = !hasMoreToLoad(Timeline.Direction.FORWARDS)
|
||||||
|
|
||||||
private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService)
|
private val eventDecryptor = TimelineEventDecryptor(realmConfiguration, timelineID, cryptoService)
|
||||||
|
|
||||||
|
@ -260,7 +260,7 @@ internal class DefaultTimeline(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun restartWithEventId(eventId: String) {
|
override fun restartWithEventId(eventId: String?) {
|
||||||
dispose()
|
dispose()
|
||||||
initialEventId = eventId
|
initialEventId = eventId
|
||||||
start()
|
start()
|
||||||
|
@ -415,7 +415,7 @@ internal class DefaultTimeline(
|
||||||
*/
|
*/
|
||||||
private fun handleInitialLoad() {
|
private fun handleInitialLoad() {
|
||||||
var shouldFetchInitialEvent = false
|
var shouldFetchInitialEvent = false
|
||||||
val initialDisplayIndex = if (isLive) {
|
val initialDisplayIndex = if (initialEventId == null) {
|
||||||
liveEvents.firstOrNull()?.root?.displayIndex
|
liveEvents.firstOrNull()?.root?.displayIndex
|
||||||
} else {
|
} else {
|
||||||
val initialEvent = liveEvents.where()
|
val initialEvent = liveEvents.where()
|
||||||
|
@ -431,7 +431,7 @@ internal class DefaultTimeline(
|
||||||
fetchEvent(currentInitialEventId)
|
fetchEvent(currentInitialEventId)
|
||||||
} else {
|
} else {
|
||||||
val count = min(settings.initialSize, liveEvents.size)
|
val count = min(settings.initialSize, liveEvents.size)
|
||||||
if (isLive) {
|
if (initialEventId == null) {
|
||||||
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count, strict = false)
|
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count, strict = false)
|
||||||
} else {
|
} else {
|
||||||
paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2, strict = false)
|
paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2, strict = false)
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.matrix.android.internal.session.sync
|
package im.vector.matrix.android.internal.session.sync
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.read.FullyReadContent
|
import im.vector.matrix.android.api.session.room.read.FullyReadContent
|
||||||
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
|
@ -37,12 +38,13 @@ internal class RoomFullyReadHandler @Inject constructor() {
|
||||||
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
|
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
|
||||||
readMarkerId = content.eventId
|
readMarkerId = content.eventId
|
||||||
}
|
}
|
||||||
val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply {
|
val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId)
|
||||||
eventId = content.eventId
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the old marker if any
|
// Remove the old marker if any
|
||||||
readMarkerEntity.timelineEvent?.firstOrNull()?.readMarker = null
|
if (readMarkerEntity.eventId.isNotEmpty()) {
|
||||||
|
val oldReadMarkerEvent = TimelineEventEntity.where(realm, eventId = readMarkerEntity.eventId).findFirst()
|
||||||
|
oldReadMarkerEvent?.readMarker = null
|
||||||
|
}
|
||||||
|
readMarkerEntity.eventId = content.eventId
|
||||||
// Attach to timelineEvent if known
|
// Attach to timelineEvent if known
|
||||||
val timelineEventEntity = TimelineEventEntity.where(realm, eventId = content.eventId).findFirst()
|
val timelineEventEntity = TimelineEventEntity.where(realm, eventId = content.eventId).findFirst()
|
||||||
timelineEventEntity?.readMarker = readMarkerEntity
|
timelineEventEntity?.readMarker = readMarkerEntity
|
||||||
|
|
|
@ -23,3 +23,7 @@ fun TimelineEvent.canReact(): Boolean {
|
||||||
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
// Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
|
||||||
return root.getClearType() == EventType.MESSAGE && root.sendState.isSent() && !root.isRedacted()
|
return root.getClearType() == EventType.MESSAGE && root.sendState.isSent() && !root.isRedacted()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TimelineEvent.displayReadMarker(myUserId: String): Boolean {
|
||||||
|
return hasReadMarker && readReceipts.find { it.user.userId == myUserId } == null
|
||||||
|
}
|
||||||
|
|
|
@ -42,9 +42,9 @@ class ReadMarkerView @JvmOverloads constructor(
|
||||||
private var callback: Callback? = null
|
private var callback: Callback? = null
|
||||||
private var callbackDispatcherJob: Job? = null
|
private var callbackDispatcherJob: Job? = null
|
||||||
|
|
||||||
fun bindView(informationData: MessageInformationData, readMarkerCallback: Callback) {
|
fun bindView(displayReadMarker: Boolean, readMarkerCallback: Callback) {
|
||||||
this.callback = readMarkerCallback
|
this.callback = readMarkerCallback
|
||||||
if (informationData.displayReadMarker) {
|
if (displayReadMarker) {
|
||||||
visibility = VISIBLE
|
visibility = VISIBLE
|
||||||
callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) {
|
callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) {
|
||||||
delay(DELAY_IN_MS)
|
delay(DELAY_IN_MS)
|
||||||
|
|
|
@ -44,6 +44,7 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.core.util.Pair
|
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.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
@ -57,6 +58,7 @@ import com.airbnb.mvrx.Loading
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.args
|
import com.airbnb.mvrx.args
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
|
import com.airbnb.mvrx.withState
|
||||||
import com.github.piasy.biv.BigImageViewer
|
import com.github.piasy.biv.BigImageViewer
|
||||||
import com.github.piasy.biv.loader.ImageLoader
|
import com.github.piasy.biv.loader.ImageLoader
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
@ -78,6 +80,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageTextConten
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
|
||||||
import im.vector.matrix.android.api.session.room.send.SendState
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
|
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
|
||||||
|
@ -96,6 +99,7 @@ import im.vector.riotx.core.glide.GlideApp
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
|
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
|
||||||
import im.vector.riotx.core.ui.views.NotificationAreaView
|
import im.vector.riotx.core.ui.views.NotificationAreaView
|
||||||
|
import im.vector.riotx.core.utils.Debouncer
|
||||||
import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
||||||
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
|
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
|
||||||
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE
|
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE
|
||||||
|
@ -105,6 +109,7 @@ import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CA
|
||||||
import im.vector.riotx.core.utils.allGranted
|
import im.vector.riotx.core.utils.allGranted
|
||||||
import im.vector.riotx.core.utils.checkPermissions
|
import im.vector.riotx.core.utils.checkPermissions
|
||||||
import im.vector.riotx.core.utils.copyToClipboard
|
import im.vector.riotx.core.utils.copyToClipboard
|
||||||
|
import im.vector.riotx.core.utils.createUIHandler
|
||||||
import im.vector.riotx.core.utils.openCamera
|
import im.vector.riotx.core.utils.openCamera
|
||||||
import im.vector.riotx.core.utils.shareMedia
|
import im.vector.riotx.core.utils.shareMedia
|
||||||
import im.vector.riotx.core.utils.toast
|
import im.vector.riotx.core.utils.toast
|
||||||
|
@ -127,7 +132,6 @@ import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsB
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction
|
import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet
|
import im.vector.riotx.features.home.room.detail.timeline.action.ViewEditHistoryBottomSheet
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
|
import im.vector.riotx.features.home.room.detail.timeline.action.ViewReactionBottomSheet
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem
|
import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem
|
||||||
|
@ -211,6 +215,8 @@ class RoomDetailFragment :
|
||||||
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
|
||||||
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
|
private val textComposerViewModel: TextComposerViewModel by fragmentViewModel()
|
||||||
|
|
||||||
|
private val debouncer = Debouncer(createUIHandler())
|
||||||
|
|
||||||
@Inject lateinit var session: Session
|
@Inject lateinit var session: Session
|
||||||
@Inject lateinit var avatarRenderer: AvatarRenderer
|
@Inject lateinit var avatarRenderer: AvatarRenderer
|
||||||
@Inject lateinit var timelineEventController: TimelineEventController
|
@Inject lateinit var timelineEventController: TimelineEventController
|
||||||
|
@ -227,7 +233,6 @@ class RoomDetailFragment :
|
||||||
|
|
||||||
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
|
||||||
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
|
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
|
||||||
|
@ -254,6 +259,7 @@ class RoomDetailFragment :
|
||||||
setupInviteView()
|
setupInviteView()
|
||||||
setupNotificationView()
|
setupNotificationView()
|
||||||
setupJumpToReadMarkerView()
|
setupJumpToReadMarkerView()
|
||||||
|
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) }
|
||||||
|
@ -306,6 +312,21 @@ class RoomDetailFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupJumpToBottomView() {
|
||||||
|
jumpToBottomView.isVisible = false
|
||||||
|
jumpToBottomView.setOnClickListener {
|
||||||
|
withState(roomDetailViewModel) { state ->
|
||||||
|
recyclerView.stopScroll()
|
||||||
|
if (state.timeline?.isLive == false) {
|
||||||
|
state.timeline.restartWithEventId(null)
|
||||||
|
} else {
|
||||||
|
layoutManager.scrollToPosition(0)
|
||||||
|
}
|
||||||
|
jumpToBottomView.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupJumpToReadMarkerView() {
|
private fun setupJumpToReadMarkerView() {
|
||||||
jumpToReadMarkerView.callback = this
|
jumpToReadMarkerView.callback = this
|
||||||
}
|
}
|
||||||
|
@ -377,17 +398,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 {
|
||||||
|
@ -416,9 +437,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))
|
||||||
}
|
}
|
||||||
|
@ -428,14 +449,12 @@ class RoomDetailFragment :
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************
|
// PRIVATE METHODS *****************************************************************************
|
||||||
|
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
|
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
|
||||||
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()
|
||||||
endlessScrollListener = EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction ->
|
|
||||||
roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction))
|
|
||||||
}
|
|
||||||
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
|
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
|
||||||
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController)
|
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController)
|
||||||
recyclerView.layoutManager = layoutManager
|
recyclerView.layoutManager = layoutManager
|
||||||
|
@ -446,38 +465,67 @@ class RoomDetailFragment :
|
||||||
it.dispatchTo(scrollOnNewMessageCallback)
|
it.dispatchTo(scrollOnNewMessageCallback)
|
||||||
it.dispatchTo(scrollOnHighlightedEventCallback)
|
it.dispatchTo(scrollOnHighlightedEventCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
recyclerView.addOnScrollListener(endlessScrollListener)
|
|
||||||
recyclerView.setController(timelineEventController)
|
recyclerView.setController(timelineEventController)
|
||||||
|
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||||
|
updateJumpToBottomViewVisibility()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
|
when (newState) {
|
||||||
|
RecyclerView.SCROLL_STATE_IDLE -> {
|
||||||
|
updateJumpToBottomViewVisibility()
|
||||||
|
}
|
||||||
|
RecyclerView.SCROLL_STATE_DRAGGING,
|
||||||
|
RecyclerView.SCROLL_STATE_SETTLING -> {
|
||||||
|
jumpToBottomView.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateJumpToBottomViewVisibility() {
|
||||||
|
debouncer.debounce("jump_to_bottom_visibility", 100, Runnable {
|
||||||
|
Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
|
||||||
|
if (layoutManager.findFirstCompletelyVisibleItemPosition() != 0) {
|
||||||
|
jumpToBottomView.show()
|
||||||
|
} else {
|
||||||
|
jumpToBottomView.hide()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupComposer() {
|
private fun setupComposer() {
|
||||||
val elevation = 6f
|
val elevation = 6f
|
||||||
val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background))
|
val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background))
|
||||||
|
@ -737,7 +785,7 @@ class RoomDetailFragment :
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TimelineEventController.Callback ************************************************************
|
// TimelineEventController.Callback ************************************************************
|
||||||
|
|
||||||
override fun onUrlClicked(url: String): Boolean {
|
override fun onUrlClicked(url: String): Boolean {
|
||||||
return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
|
return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
|
||||||
|
@ -835,6 +883,10 @@ class RoomDetailFragment :
|
||||||
vectorBaseActivity.notImplemented("open audio file")
|
vectorBaseActivity.notImplemented("open audio file")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLoadMore(direction: Timeline.Direction) {
|
||||||
|
roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) {
|
override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -901,7 +953,7 @@ class RoomDetailFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutocompleteUserPresenter.Callback
|
// AutocompleteUserPresenter.Callback
|
||||||
|
|
||||||
override fun onQueryUsers(query: CharSequence?) {
|
override fun onQueryUsers(query: CharSequence?) {
|
||||||
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
|
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
|
||||||
|
@ -1066,6 +1118,7 @@ class RoomDetailFragment :
|
||||||
snack.show()
|
snack.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// VectorInviteView.Callback
|
// VectorInviteView.Callback
|
||||||
|
|
||||||
override fun onAcceptInvite() {
|
override fun onAcceptInvite() {
|
||||||
|
@ -1078,7 +1131,7 @@ class RoomDetailFragment :
|
||||||
roomDetailViewModel.process(RoomDetailActions.RejectInvite)
|
roomDetailViewModel.process(RoomDetailActions.RejectInvite)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JumpToReadMarkerView.Callback
|
// JumpToReadMarkerView.Callback
|
||||||
|
|
||||||
override fun onJumpToReadMarkerClicked(readMarkerId: String) {
|
override fun onJumpToReadMarkerClicked(readMarkerId: String) {
|
||||||
roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false))
|
roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false))
|
||||||
|
|
|
@ -21,6 +21,8 @@ import android.text.TextUtils
|
||||||
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 arrow.core.Option
|
||||||
|
import arrow.core.getOrElse
|
||||||
import com.airbnb.mvrx.FragmentViewModelContext
|
import com.airbnb.mvrx.FragmentViewModelContext
|
||||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
|
@ -635,16 +637,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||||
private fun observeJumpToReadMarkerViewVisibility() {
|
private fun observeJumpToReadMarkerViewVisibility() {
|
||||||
Observable
|
Observable
|
||||||
.combineLatest(
|
.combineLatest(
|
||||||
room.rx().liveRoomSummary(),
|
room.rx().liveRoomSummary().map {
|
||||||
|
val readMarkerId = it.readMarkerId
|
||||||
|
if (readMarkerId == null) {
|
||||||
|
Option.empty()
|
||||||
|
} else {
|
||||||
|
val timelineEvent = room.getTimeLineEvent(readMarkerId)
|
||||||
|
Option.fromNullable(timelineEvent)
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged(),
|
||||||
visibleEventsObservable.distinctUntilChanged(),
|
visibleEventsObservable.distinctUntilChanged(),
|
||||||
isEventVisibleObservable { it.hasReadMarker }.startWith(false).takeUntil { it },
|
isEventVisibleObservable { it.hasReadMarker }.startWith(false),
|
||||||
Function3<RoomSummary, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { roomSummary, currentVisibleEvent, isReadMarkerViewVisible ->
|
Function3<Option<TimelineEvent>, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { readMarkerEvent, currentVisibleEvent, isReadMarkerViewVisible ->
|
||||||
val readMarkerId = roomSummary.readMarkerId
|
if (readMarkerEvent.isEmpty() || isReadMarkerViewVisible) {
|
||||||
if (readMarkerId == null || isReadMarkerViewVisible || !timeline.isLive) {
|
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
val readMarkerPosition = timeline.getTimelineEventWithId(readMarkerId)?.displayIndex
|
val readMarkerPosition = readMarkerEvent.map { it.displayIndex }.getOrElse { Int.MIN_VALUE }
|
||||||
?: Int.MIN_VALUE
|
|
||||||
val currentVisibleEventPosition = currentVisibleEvent.event.displayIndex
|
val currentVisibleEventPosition = currentVisibleEvent.event.displayIndex
|
||||||
readMarkerPosition < currentVisibleEventPosition
|
readMarkerPosition < currentVisibleEventPosition
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.airbnb.epoxy.EpoxyController
|
import com.airbnb.epoxy.EpoxyController
|
||||||
import com.airbnb.epoxy.EpoxyModel
|
import com.airbnb.epoxy.EpoxyModel
|
||||||
|
import com.airbnb.epoxy.VisibilityState
|
||||||
import im.vector.matrix.android.api.session.room.model.message.*
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
@ -50,6 +51,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
||||||
|
|
||||||
interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback {
|
interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback {
|
||||||
|
fun onLoadMore(direction: Timeline.Direction)
|
||||||
fun onEventInvisible(event: TimelineEvent)
|
fun onEventInvisible(event: TimelineEvent)
|
||||||
fun onEventVisible(event: TimelineEvent)
|
fun onEventVisible(event: TimelineEvent)
|
||||||
fun onRoomCreateLinkClicked(url: String)
|
fun onRoomCreateLinkClicked(url: String)
|
||||||
|
@ -158,7 +160,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,6 +182,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
val timestamp = System.currentTimeMillis()
|
val timestamp = System.currentTimeMillis()
|
||||||
showingForwardLoader = LoadingItem_()
|
showingForwardLoader = LoadingItem_()
|
||||||
.id("forward_loading_item_$timestamp")
|
.id("forward_loading_item_$timestamp")
|
||||||
|
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
|
||||||
.addWhen(Timeline.Direction.FORWARDS)
|
.addWhen(Timeline.Direction.FORWARDS)
|
||||||
|
|
||||||
val timelineModels = getModels()
|
val timelineModels = getModels()
|
||||||
|
@ -189,6 +192,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
if (!showingForwardLoader || timelineModels.isNotEmpty()) {
|
if (!showingForwardLoader || timelineModels.isNotEmpty()) {
|
||||||
LoadingItem_()
|
LoadingItem_()
|
||||||
.id("backward_loading_item_$timestamp")
|
.id("backward_loading_item_$timestamp")
|
||||||
|
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS)
|
||||||
.addWhen(Timeline.Direction.BACKWARDS)
|
.addWhen(Timeline.Direction.BACKWARDS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,8 +224,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -251,7 +255,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, eventIdToHighlight, callback) {
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
}
|
}
|
||||||
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
|
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
|
||||||
|
@ -277,6 +281,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
return shouldAdd
|
return shouldAdd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if added
|
||||||
|
*/
|
||||||
|
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
|
||||||
|
return onVisibilityStateChanged { model, view, visibilityState ->
|
||||||
|
if (visibilityState == VisibilityState.VISIBLE) {
|
||||||
|
callback?.onLoadMore(direction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
package im.vector.riotx.features.home.room.detail.timeline.factory
|
package im.vector.riotx.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.riotx.core.extensions.displayReadMarker
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||||
|
@ -29,7 +31,8 @@ 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.MergedHeaderItem_
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer,
|
class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: ActiveSessionHolder,
|
||||||
|
private val avatarRenderer: AvatarRenderer,
|
||||||
private val avatarSizeProvider: AvatarSizeProvider) {
|
private val avatarSizeProvider: AvatarSizeProvider) {
|
||||||
|
|
||||||
private val collapsedEventIds = linkedSetOf<Long>()
|
private val collapsedEventIds = linkedSetOf<Long>()
|
||||||
|
@ -40,6 +43,7 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av
|
||||||
items: List<TimelineEvent>,
|
items: List<TimelineEvent>,
|
||||||
addDaySeparator: Boolean,
|
addDaySeparator: Boolean,
|
||||||
currentPosition: Int,
|
currentPosition: Int,
|
||||||
|
eventIdToHighlight: String?,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
requestModelBuild: () -> Unit)
|
requestModelBuild: () -> Unit)
|
||||||
: MergedHeaderItem? {
|
: MergedHeaderItem? {
|
||||||
|
@ -47,20 +51,30 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av
|
||||||
return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
|
return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
|
var highlighted = false
|
||||||
|
var showReadMarker = false
|
||||||
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
|
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
|
||||||
if (prevSameTypeEvents.isEmpty()) {
|
if (prevSameTypeEvents.isEmpty()) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
|
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
|
||||||
val mergedData = mergedEvents.map { mergedEvent ->
|
val mergedData = ArrayList<MergedHeaderItem.Data>(mergedEvents.size)
|
||||||
|
mergedEvents.forEach { mergedEvent ->
|
||||||
|
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
|
||||||
|
highlighted = true
|
||||||
|
}
|
||||||
|
if (!showReadMarker && mergedEvent.displayReadMarker(sessionHolder.getActiveSession().myUserId)) {
|
||||||
|
showReadMarker = true
|
||||||
|
}
|
||||||
val senderAvatar = mergedEvent.senderAvatar()
|
val senderAvatar = mergedEvent.senderAvatar()
|
||||||
val senderName = mergedEvent.senderName()
|
val senderName = mergedEvent.senderName()
|
||||||
MergedHeaderItem.Data(
|
val data = MergedHeaderItem.Data(
|
||||||
userId = mergedEvent.root.senderId ?: "",
|
userId = mergedEvent.root.senderId ?: "",
|
||||||
avatarUrl = senderAvatar,
|
avatarUrl = senderAvatar,
|
||||||
memberName = senderName ?: "",
|
memberName = senderName ?: "",
|
||||||
eventId = mergedEvent.localId
|
eventId = mergedEvent.localId
|
||||||
)
|
)
|
||||||
|
mergedData.add(data)
|
||||||
}
|
}
|
||||||
val mergedEventIds = mergedEvents.map { it.localId }
|
val mergedEventIds = mergedEvents.map { it.localId }
|
||||||
// We try to find if one of the item id were used as mergeItemCollapseStates key
|
// We try to find if one of the item id were used as mergeItemCollapseStates key
|
||||||
|
@ -82,11 +96,13 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av
|
||||||
onCollapsedStateChanged = {
|
onCollapsedStateChanged = {
|
||||||
mergeItemCollapseStates[event.localId] = it
|
mergeItemCollapseStates[event.localId] = it
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
}
|
},
|
||||||
|
showReadMarker = showReadMarker
|
||||||
)
|
)
|
||||||
MergedHeaderItem_()
|
MergedHeaderItem_()
|
||||||
.id(mergeId)
|
.id(mergeId)
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
|
.highlighted(highlighted)
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.also {
|
.also {
|
||||||
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
|
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.features.home.room.detail.timeline.helper
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
|
||||||
|
|
||||||
class EndlessRecyclerViewScrollListener(private val layoutManager: LinearLayoutManager,
|
|
||||||
private val visibleThreshold: Int,
|
|
||||||
private val onLoadMore: (Timeline.Direction) -> Unit
|
|
||||||
) : RecyclerView.OnScrollListener() {
|
|
||||||
|
|
||||||
// The total number of items in the dataset after the last load
|
|
||||||
private var previousTotalItemCount = 0
|
|
||||||
// True if we are still waiting for the last set of data to load.
|
|
||||||
private var loadingBackwards = true
|
|
||||||
private var loadingForwards = true
|
|
||||||
|
|
||||||
// This happens many times a second during a scroll, so be wary of the code you place here.
|
|
||||||
// We are given a few useful parameters to help us work out if we need to load some more data,
|
|
||||||
// but first we check if we are waiting for the previous load to finish.
|
|
||||||
|
|
||||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
|
||||||
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
|
|
||||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
|
||||||
val totalItemCount = layoutManager.itemCount
|
|
||||||
|
|
||||||
// We check to see if the dataset count has
|
|
||||||
// changed, if so we conclude it has finished loading
|
|
||||||
if (totalItemCount != previousTotalItemCount) {
|
|
||||||
previousTotalItemCount = totalItemCount
|
|
||||||
loadingBackwards = false
|
|
||||||
loadingForwards = false
|
|
||||||
}
|
|
||||||
// If it isn’t currently loading, we check to see if we have reached
|
|
||||||
// the visibleThreshold and need to reload more data.
|
|
||||||
if (!loadingBackwards && lastVisibleItemPosition + visibleThreshold > totalItemCount) {
|
|
||||||
loadingBackwards = true
|
|
||||||
onLoadMore(Timeline.Direction.BACKWARDS)
|
|
||||||
}
|
|
||||||
if (!loadingForwards && firstVisibleItemPosition < visibleThreshold) {
|
|
||||||
loadingForwards = true
|
|
||||||
onLoadMore(Timeline.Direction.FORWARDS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -27,6 +27,7 @@ import im.vector.riotx.core.resources.ColorProvider
|
||||||
import im.vector.riotx.core.utils.isSingleEmoji
|
import im.vector.riotx.core.utils.isSingleEmoji
|
||||||
import im.vector.riotx.features.home.getColorFromUserId
|
import im.vector.riotx.features.home.getColorFromUserId
|
||||||
import im.vector.riotx.core.date.VectorDateFormatter
|
import im.vector.riotx.core.date.VectorDateFormatter
|
||||||
|
import im.vector.riotx.core.extensions.displayReadMarker
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData
|
import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
||||||
|
@ -64,8 +65,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||||
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: ""))
|
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId ?: ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
val displayReadMarker = event.hasReadMarker
|
val displayReadMarker = event.displayReadMarker(session.myUserId)
|
||||||
&& event.readReceipts.find { it.user.userId == session.myUserId } == null
|
|
||||||
|
|
||||||
return MessageInformationData(
|
return MessageInformationData(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
|
|
|
@ -106,7 +106,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||||
holder.memberNameView.setOnLongClickListener(null)
|
holder.memberNameView.setOnLongClickListener(null)
|
||||||
}
|
}
|
||||||
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
||||||
holder.readMarkerView.bindView(attributes.informationData, _readMarkerCallback)
|
holder.readMarkerView.bindView(attributes.informationData.displayReadMarker, _readMarkerCallback)
|
||||||
|
|
||||||
if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) {
|
if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) {
|
||||||
holder.reactionWrapper?.isVisible = false
|
holder.reactionWrapper?.isVisible = false
|
||||||
|
@ -162,7 +162,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||||
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||||
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||||
val timeView by bind<TextView>(R.id.messageTimeView)
|
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||||
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)
|
|
||||||
var reactionWrapper: ViewGroup? = null
|
var reactionWrapper: ViewGroup? = null
|
||||||
var reactionFlowHelper: Flow? = null
|
var reactionFlowHelper: Flow? = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
import im.vector.riotx.core.platform.CheckableView
|
import im.vector.riotx.core.platform.CheckableView
|
||||||
|
import im.vector.riotx.core.ui.views.ReadMarkerView
|
||||||
import im.vector.riotx.core.ui.views.ReadReceiptsView
|
import im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||||
import im.vector.riotx.core.utils.DimensionUtils.dpToPx
|
import im.vector.riotx.core.utils.DimensionUtils.dpToPx
|
||||||
import org.w3c.dom.Attr
|
import org.w3c.dom.Attr
|
||||||
|
@ -49,6 +50,7 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
||||||
val leftGuideline by bind<Guideline>(R.id.messageStartGuideline)
|
val leftGuideline by bind<Guideline>(R.id.messageStartGuideline)
|
||||||
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
||||||
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
||||||
|
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)
|
||||||
|
|
||||||
override fun bindView(itemView: View) {
|
override fun bindView(itemView: View) {
|
||||||
super.bindView(itemView)
|
super.bindView(itemView)
|
||||||
|
|
|
@ -79,6 +79,7 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
||||||
|
|
||||||
data class Attributes(
|
data class Attributes(
|
||||||
val isCollapsed: Boolean,
|
val isCollapsed: Boolean,
|
||||||
|
val showReadMarker: Boolean,
|
||||||
val mergeData: List<Data>,
|
val mergeData: List<Data>,
|
||||||
val avatarRenderer: AvatarRenderer,
|
val avatarRenderer: AvatarRenderer,
|
||||||
val onCollapsedStateChanged: (Boolean) -> Unit
|
val onCollapsedStateChanged: (Boolean) -> Unit
|
||||||
|
|
|
@ -55,7 +55,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||||
)
|
)
|
||||||
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
||||||
holder.readMarkerView.bindView(attributes.informationData, _readMarkerCallback)
|
holder.readMarkerView.bindView(attributes.informationData.displayReadMarker, _readMarkerCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
override fun unbind(holder: Holder) {
|
||||||
|
@ -68,7 +68,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||||
class Holder : BaseHolder(STUB_ID) {
|
class Holder : BaseHolder(STUB_ID) {
|
||||||
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||||
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
|
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
|
||||||
val readMarkerView by bind<ReadMarkerView>(R.id.readMarkerView)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Attributes(
|
data class Attributes(
|
||||||
|
|
BIN
vector/src/main/res/drawable-hdpi/arrow_up_circle.png
Executable file
After Width: | Height: | Size: 686 B |
BIN
vector/src/main/res/drawable-hdpi/chevron_down.png
Executable file
After Width: | Height: | Size: 303 B |
BIN
vector/src/main/res/drawable-mdpi/arrow_up_circle.png
Executable file
After Width: | Height: | Size: 414 B |
BIN
vector/src/main/res/drawable-mdpi/chevron_down.png
Executable file
After Width: | Height: | Size: 231 B |
BIN
vector/src/main/res/drawable-xhdpi/arrow_up_circle.png
Executable file
After Width: | Height: | Size: 869 B |
BIN
vector/src/main/res/drawable-xhdpi/chevron_down.png
Executable file
After Width: | Height: | Size: 391 B |
BIN
vector/src/main/res/drawable-xxhdpi/arrow_up_circle.png
Executable file
After Width: | Height: | Size: 1.3 KiB |
BIN
vector/src/main/res/drawable-xxhdpi/chevron_down.png
Executable file
After Width: | Height: | Size: 454 B |
BIN
vector/src/main/res/drawable-xxxhdpi/arrow_up_circle.png
Executable file
After Width: | Height: | Size: 1.9 KiB |
BIN
vector/src/main/res/drawable-xxxhdpi/chevron_down.png
Executable file
After Width: | Height: | Size: 584 B |
|
@ -153,4 +153,17 @@
|
||||||
app:layout_constraintTop_toBottomOf="@+id/roomToolbar"
|
app:layout_constraintTop_toBottomOf="@+id/roomToolbar"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/jumpToBottomView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:src="@drawable/chevron_down"
|
||||||
|
app:backgroundTint="#FFFFFF"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/composerLayout"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:maxImageSize="16dp"
|
||||||
|
app:tint="@color/black" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -11,11 +11,8 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginLeft="16dp"
|
|
||||||
android:layout_toStartOf="@+id/closeJumpToReadMarkerView"
|
android:layout_toStartOf="@+id/closeJumpToReadMarkerView"
|
||||||
android:layout_toLeftOf="@+id/closeJumpToReadMarkerView"
|
android:drawableStart="@drawable/arrow_up_circle"
|
||||||
android:drawableStart="@drawable/jump_to_unread"
|
|
||||||
android:drawableLeft="@drawable/jump_to_unread"
|
|
||||||
android:drawablePadding="10dp"
|
android:drawablePadding="10dp"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:paddingTop="12dp"
|
android:paddingTop="12dp"
|
||||||
|
|