From 3066d5f3039f7402768f6df629c38be7f016cacc Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 17 Sep 2019 19:38:05 +0200 Subject: [PATCH] Timeline\ReadMarker: continue fixing issues --- .../api/session/room/timeline/Timeline.kt | 2 +- .../session/room/read/SetReadMarkersTask.kt | 2 - .../session/room/timeline/DefaultTimeline.kt | 8 +- .../session/sync/RoomFullyReadHandler.kt | 12 +- .../riotx/core/extensions/TimelineEvent.kt | 4 + .../riotx/core/ui/views/ReadMarkerView.kt | 4 +- .../home/room/detail/RoomDetailFragment.kt | 121 +++++++++++++----- .../home/room/detail/RoomDetailViewModel.kt | 22 +++- .../timeline/TimelineEventController.kt | 24 +++- .../factory/MergedHeaderItemFactory.kt | 24 +++- .../EndlessRecyclerViewScrollListener.kt | 63 --------- .../helper/MessageInformationDataFactory.kt | 4 +- .../detail/timeline/item/AbsMessageItem.kt | 3 +- .../detail/timeline/item/BaseEventItem.kt | 2 + .../detail/timeline/item/MergedHeaderItem.kt | 1 + .../room/detail/timeline/item/NoticeItem.kt | 3 +- .../res/drawable-hdpi/arrow_up_circle.png | Bin 0 -> 686 bytes .../main/res/drawable-hdpi/chevron_down.png | Bin 0 -> 303 bytes .../res/drawable-mdpi/arrow_up_circle.png | Bin 0 -> 414 bytes .../main/res/drawable-mdpi/chevron_down.png | Bin 0 -> 231 bytes .../res/drawable-xhdpi/arrow_up_circle.png | Bin 0 -> 869 bytes .../main/res/drawable-xhdpi/chevron_down.png | Bin 0 -> 391 bytes .../res/drawable-xxhdpi/arrow_up_circle.png | Bin 0 -> 1332 bytes .../main/res/drawable-xxhdpi/chevron_down.png | Bin 0 -> 454 bytes .../res/drawable-xxxhdpi/arrow_up_circle.png | Bin 0 -> 1910 bytes .../res/drawable-xxxhdpi/chevron_down.png | Bin 0 -> 584 bytes .../main/res/layout/fragment_room_detail.xml | 13 ++ .../res/layout/view_jump_to_read_marker.xml | 5 +- 28 files changed, 181 insertions(+), 136 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt create mode 100755 vector/src/main/res/drawable-hdpi/arrow_up_circle.png create mode 100755 vector/src/main/res/drawable-hdpi/chevron_down.png create mode 100755 vector/src/main/res/drawable-mdpi/arrow_up_circle.png create mode 100755 vector/src/main/res/drawable-mdpi/chevron_down.png create mode 100755 vector/src/main/res/drawable-xhdpi/arrow_up_circle.png create mode 100755 vector/src/main/res/drawable-xhdpi/chevron_down.png create mode 100755 vector/src/main/res/drawable-xxhdpi/arrow_up_circle.png create mode 100755 vector/src/main/res/drawable-xxhdpi/chevron_down.png create mode 100755 vector/src/main/res/drawable-xxxhdpi/arrow_up_circle.png create mode 100755 vector/src/main/res/drawable-xxxhdpi/chevron_down.png diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt index 3f90d3cd13..d0f4bff74b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt @@ -45,7 +45,7 @@ interface Timeline { fun dispose() - fun restartWithEventId(eventId: String) + fun restartWithEventId(eventId: String?) /** diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 9652faae81..26eb16b15c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -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.query.find 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.where 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.util.awaitTransaction import io.realm.Realm -import io.realm.RealmConfiguration import timber.log.Timber import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index e50d25d195..88c13cc056 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -114,7 +114,7 @@ internal class DefaultTimeline( private val timelineID = UUID.randomUUID().toString() override val isLive - get() = initialEventId == null + get() = !hasMoreToLoad(Timeline.Direction.FORWARDS) 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() initialEventId = eventId start() @@ -415,7 +415,7 @@ internal class DefaultTimeline( */ private fun handleInitialLoad() { var shouldFetchInitialEvent = false - val initialDisplayIndex = if (isLive) { + val initialDisplayIndex = if (initialEventId == null) { liveEvents.firstOrNull()?.root?.displayIndex } else { val initialEvent = liveEvents.where() @@ -431,7 +431,7 @@ internal class DefaultTimeline( fetchEvent(currentInitialEventId) } else { val count = min(settings.initialSize, liveEvents.size) - if (isLive) { + if (initialEventId == null) { paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count, strict = false) } else { paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2, strict = false) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt index 9757d0f421..45fbe7329d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomFullyReadHandler.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.sync 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.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity @@ -37,12 +38,13 @@ internal class RoomFullyReadHandler @Inject constructor() { RoomSummaryEntity.getOrCreate(realm, roomId).apply { readMarkerId = content.eventId } - val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply { - eventId = content.eventId - } - + val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId) // 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 val timelineEventEntity = TimelineEventEntity.where(realm, eventId = content.eventId).findFirst() timelineEventEntity?.readMarker = readMarkerEntity diff --git a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt index 58fcd0b5cd..6c7a6be1fd 100644 --- a/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt +++ b/vector/src/main/java/im/vector/riotx/core/extensions/TimelineEvent.kt @@ -23,3 +23,7 @@ fun TimelineEvent.canReact(): Boolean { // Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment 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 +} diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt index becab54da3..f5f086ac8b 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/ReadMarkerView.kt @@ -42,9 +42,9 @@ class ReadMarkerView @JvmOverloads constructor( private var callback: Callback? = null private var callbackDispatcherJob: Job? = null - fun bindView(informationData: MessageInformationData, readMarkerCallback: Callback) { + fun bindView(displayReadMarker: Boolean, readMarkerCallback: Callback) { this.callback = readMarkerCallback - if (informationData.displayReadMarker) { + if (displayReadMarker) { visibility = VISIBLE callbackDispatcherJob = GlobalScope.launch(Dispatchers.Main) { delay(DELAY_IN_MS) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 61338e7858..48cdea6e59 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -44,6 +44,7 @@ import androidx.core.content.ContextCompat import androidx.core.util.Pair import androidx.core.view.ViewCompat import androidx.core.view.forEach +import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager @@ -57,6 +58,7 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.ImageLoader 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.MessageVideoContent 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.getLastMessageContent 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.ui.views.JumpToReadMarkerView 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_WRITING_FILES 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.checkPermissions 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.shareMedia 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.ViewEditHistoryBottomSheet 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.MessageFileItem 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 textComposerViewModel: TextComposerViewModel by fragmentViewModel() + private val debouncer = Debouncer(createUIHandler()) + @Inject lateinit var session: Session @Inject lateinit var avatarRenderer: AvatarRenderer @Inject lateinit var timelineEventController: TimelineEventController @@ -227,7 +233,6 @@ class RoomDetailFragment : private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback - private lateinit var endlessScrollListener: EndlessRecyclerViewScrollListener override fun getLayoutResId() = R.layout.fragment_room_detail @@ -254,6 +259,7 @@ class RoomDetailFragment : setupInviteView() setupNotificationView() setupJumpToReadMarkerView() + setupJumpToBottomView() roomDetailViewModel.subscribe { renderState(it) } textComposerViewModel.subscribe { renderTextComposerState(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() { jumpToReadMarkerView.callback = this } @@ -377,17 +398,17 @@ class RoomDetailFragment : if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { val parser = Parser.builder().build() val document = parser.parse(messageContent.formattedBody - ?: messageContent.body) + ?: messageContent.body) formattedBody = eventHtmlRenderer.render(document) } composerLayout.composerRelatedMessageContent.text = formattedBody - ?: nonFormattedBody + ?: nonFormattedBody composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.expand { @@ -416,9 +437,9 @@ class RoomDetailFragment : REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -428,14 +449,12 @@ class RoomDetailFragment : // PRIVATE METHODS ***************************************************************************** + private fun setupRecyclerView() { val epoxyVisibilityTracker = EpoxyVisibilityTracker() epoxyVisibilityTracker.attach(recyclerView) layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() - endlessScrollListener = EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction -> - roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction)) - } scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController) scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController) recyclerView.layoutManager = layoutManager @@ -446,38 +465,67 @@ class RoomDetailFragment : it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) } - - recyclerView.addOnScrollListener(endlessScrollListener) 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 if (vectorPreferences.swipeToReplyIsEnabled()) { val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), - R.drawable.ic_reply, - object : RoomMessageTouchHelperCallback.QuickReplayHandler { - override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { - (model as? AbsMessageItem)?.attributes?.informationData?.let { - val eventId = it.eventId - roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) - } - } + R.drawable.ic_reply, + object : RoomMessageTouchHelperCallback.QuickReplayHandler { + override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { + (model as? AbsMessageItem)?.attributes?.informationData?.let { + val eventId = it.eventId + roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) + } + } - override fun canSwipeModel(model: EpoxyModel<*>): Boolean { - return when (model) { - is MessageFileItem, - is MessageImageVideoItem, - is MessageTextItem -> { - return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED - } - else -> false - } - } - }) + override fun canSwipeModel(model: EpoxyModel<*>): Boolean { + return when (model) { + is MessageFileItem, + is MessageImageVideoItem, + is MessageTextItem -> { + return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED + } + else -> false + } + } + }) val touchHelper = ItemTouchHelper(swipeCallback) 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() { val elevation = 6f val backgroundDrawable = ColorDrawable(ThemeUtils.getColor(requireContext(), R.attr.riotx_background)) @@ -737,7 +785,7 @@ class RoomDetailFragment : .show() } - // TimelineEventController.Callback ************************************************************ +// TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String): Boolean { return permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { @@ -835,6 +883,10 @@ class RoomDetailFragment : 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) { } @@ -901,7 +953,7 @@ class RoomDetailFragment : } } - // AutocompleteUserPresenter.Callback +// AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { textComposerViewModel.process(TextComposerActions.QueryUsers(query)) @@ -1066,6 +1118,7 @@ class RoomDetailFragment : snack.show() } + // VectorInviteView.Callback override fun onAcceptInvite() { @@ -1078,7 +1131,7 @@ class RoomDetailFragment : roomDetailViewModel.process(RoomDetailActions.RejectInvite) } - // JumpToReadMarkerView.Callback +// JumpToReadMarkerView.Callback override fun onJumpToReadMarkerClicked(readMarkerId: String) { roomDetailViewModel.process(RoomDetailActions.NavigateToEvent(readMarkerId, false)) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index a45ea55825..ee4b3c0423 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -21,6 +21,8 @@ import android.text.TextUtils import androidx.annotation.IdRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import arrow.core.Option +import arrow.core.getOrElse import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success @@ -635,16 +637,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun observeJumpToReadMarkerViewVisibility() { Observable .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(), - isEventVisibleObservable { it.hasReadMarker }.startWith(false).takeUntil { it }, - Function3 { roomSummary, currentVisibleEvent, isReadMarkerViewVisible -> - val readMarkerId = roomSummary.readMarkerId - if (readMarkerId == null || isReadMarkerViewVisible || !timeline.isLive) { + isEventVisibleObservable { it.hasReadMarker }.startWith(false), + Function3, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { readMarkerEvent, currentVisibleEvent, isReadMarkerViewVisible -> + if (readMarkerEvent.isEmpty() || isReadMarkerViewVisible) { false } else { - val readMarkerPosition = timeline.getTimelineEventWithId(readMarkerId)?.displayIndex - ?: Int.MIN_VALUE + val readMarkerPosition = readMarkerEvent.map { it.displayIndex }.getOrElse { Int.MIN_VALUE } val currentVisibleEventPosition = currentVisibleEvent.event.displayIndex readMarkerPosition < currentVisibleEventPosition } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 147666345e..652f35fb67 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -24,6 +24,7 @@ import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController 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.timeline.Timeline 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 { interface Callback : BaseCallback, ReactionPillCallback, AvatarCallback, UrlClickCallback, ReadReceiptsCallback { + fun onLoadMore(direction: Timeline.Direction) fun onEventInvisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent) fun onRoomCreateLinkClicked(url: String) @@ -158,7 +160,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == eventIdToHighlight - || modelCache[i]?.eventId == this.eventIdToHighlight) { + || modelCache[i]?.eventId == this.eventIdToHighlight) { modelCache[i] = null } } @@ -180,6 +182,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val timestamp = System.currentTimeMillis() showingForwardLoader = LoadingItem_() .id("forward_loading_item_$timestamp") + .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS) .addWhen(Timeline.Direction.FORWARDS) val timelineModels = getModels() @@ -189,6 +192,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec if (!showingForwardLoader || timelineModels.isNotEmpty()) { LoadingItem_() .id("backward_loading_item_$timestamp") + .setVisibilityStateChangedListener(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 // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -251,7 +255,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec it.id(event.localId) 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() } val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) @@ -277,6 +281,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec 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) { // Search in the cache var realPosition = 0 diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 06514e5973..42f0688e50 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -17,6 +17,8 @@ package im.vector.riotx.features.home.room.detail.timeline.factory 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.room.detail.timeline.TimelineEventController 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 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 collapsedEventIds = linkedSetOf() @@ -40,6 +43,7 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av items: List, addDaySeparator: Boolean, currentPosition: Int, + eventIdToHighlight: String?, callback: TimelineEventController.Callback?, requestModelBuild: () -> Unit) : MergedHeaderItem? { @@ -47,20 +51,30 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { null } else { + var highlighted = false + var showReadMarker = false val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) if (prevSameTypeEvents.isEmpty()) { null } else { val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() - val mergedData = mergedEvents.map { mergedEvent -> + val mergedData = ArrayList(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 senderName = mergedEvent.senderName() - MergedHeaderItem.Data( + val data = MergedHeaderItem.Data( userId = mergedEvent.root.senderId ?: "", avatarUrl = senderAvatar, memberName = senderName ?: "", eventId = mergedEvent.localId ) + mergedData.add(data) } val mergedEventIds = mergedEvents.map { it.localId } // 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 = { mergeItemCollapseStates[event.localId] = it requestModelBuild() - } + }, + showReadMarker = showReadMarker ) MergedHeaderItem_() .id(mergeId) .leftGuideline(avatarSizeProvider.leftGuideline) + .highlighted(highlighted) .attributes(attributes) .also { it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt deleted file mode 100644 index 9bcb7c634f..0000000000 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt +++ /dev/null @@ -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) - } - } - - -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 7ca5204766..b8a89a4669 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -27,6 +27,7 @@ import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.isSingleEmoji import im.vector.riotx.features.home.getColorFromUserId 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.ReactionInfoData 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 ?: "")) } - val displayReadMarker = event.hasReadMarker - && event.readReceipts.find { it.user.userId == session.myUserId } == null + val displayReadMarker = event.displayReadMarker(session.myUserId) return MessageInformationData( eventId = eventId, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index b299d61540..44a5e2bdfb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -106,7 +106,7 @@ abstract class AbsMessageItem : BaseEventItem() { holder.memberNameView.setOnLongClickListener(null) } 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()) { holder.reactionWrapper?.isVisible = false @@ -162,7 +162,6 @@ abstract class AbsMessageItem : BaseEventItem() { val avatarImageView by bind(R.id.messageAvatarImageView) val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) - val readMarkerView by bind(R.id.readMarkerView) var reactionWrapper: ViewGroup? = null var reactionFlowHelper: Flow? = null } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt index 532d56f580..a97ec23c97 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -24,6 +24,7 @@ import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyModel 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.utils.DimensionUtils.dpToPx import org.w3c.dom.Attr @@ -49,6 +50,7 @@ abstract class BaseEventItem : VectorEpoxyModel val leftGuideline by bind(R.id.messageStartGuideline) val checkableBackground by bind(R.id.messageSelectedBackground) val readReceiptsView by bind(R.id.readReceiptsView) + val readMarkerView by bind(R.id.readMarkerView) override fun bindView(itemView: View) { super.bindView(itemView) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index 0ac068c379..f07575e1a5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -79,6 +79,7 @@ abstract class MergedHeaderItem : BaseEventItem() { data class Attributes( val isCollapsed: Boolean, + val showReadMarker: Boolean, val mergeData: List, val avatarRenderer: AvatarRenderer, val onCollapsedStateChanged: (Boolean) -> Unit diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index d3443cb0fb..8e61a3be1f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -55,7 +55,7 @@ abstract class NoticeItem : BaseEventItem() { ) holder.view.setOnLongClickListener(attributes.itemLongClickListener) 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) { @@ -68,7 +68,6 @@ abstract class NoticeItem : BaseEventItem() { class Holder : BaseHolder(STUB_ID) { val avatarImageView by bind(R.id.itemNoticeAvatarView) val noticeTextView by bind(R.id.itemNoticeTextView) - val readMarkerView by bind(R.id.readMarkerView) } data class Attributes( diff --git a/vector/src/main/res/drawable-hdpi/arrow_up_circle.png b/vector/src/main/res/drawable-hdpi/arrow_up_circle.png new file mode 100755 index 0000000000000000000000000000000000000000..c7fba081c151926a44f8b955da6857a2c9a7e6f7 GIT binary patch literal 686 zcmV;f0#W^mP)Px%Xh}ptR7efImcL2^K@i40u?z}EEJR645yiq+5L+P@K7obcGx!ERfTfMCwu(g_ zKr1bUfQq0fhzM$9{C$@hZ|~eoOoV*!?d<&7nZ4a*ZSKLb9)fhAh58)RK@qlsxPfSV=p?Hm({8Lc^7vME?Jki;O zi?IZ=Lvx%uF;w6O6n1kX0tH_UwqS@BO1FYK#e(uzMraz+3}wf= zhQDPoS{A`&i+;2q_C@Guj52v$LBbUt6+rPpE8;^=0ue2->X zQZe0i!fi)nEeWR4ny?O)cpKh8K~j2HQ1M0FnPEo7s;1~-En*!*9j1kt>3$esdRS2L zMLe0II!4Efc+oKU2xS?bk+^fBLN9$4WCb4Ru0q8ZF=d8l71M7*nCpnFC6Q*elUd?D zn@DsrjF`1i_gY5Z@M&fkJ&o}ay{(qd_wiE2PT{8wZS+pjSHy-ze;`$d-Ot}36REMJ zAy#`NKvb5SCapGU3o!ClKH#Q|96<8kiX}Px#=}AOER5%f(l*U*pi^`R4u}PiI57YxMnD{x2c#?kV_@>94 zf-h#ewhLTye7VAulU{ zq}U|zYv6qzmb`@`T6mrsL_3kDbw9Ee&p|l(`UGE`(4IFjC?Eg;002ovPDHLkV1g32 BdZPdU literal 0 HcmV?d00001 diff --git a/vector/src/main/res/drawable-mdpi/arrow_up_circle.png b/vector/src/main/res/drawable-mdpi/arrow_up_circle.png new file mode 100755 index 0000000000000000000000000000000000000000..aad94e9a4e8492b15a30423dd8965ea33cf2eff4 GIT binary patch literal 414 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 literal 0 HcmV?d00001 diff --git a/vector/src/main/res/drawable-xxxhdpi/arrow_up_circle.png b/vector/src/main/res/drawable-xxxhdpi/arrow_up_circle.png new file mode 100755 index 0000000000000000000000000000000000000000..61fd2b1e48dd4a03e94839c014b2de8fa0176a87 GIT binary patch literal 1910 zcmV-+2Z{KJP)Px+FiAu~RCodHoXu-pMHt3&rLD9W!4|SHHQ0q(m#u|T$fiV)NLStH#-(lwVg+}F zKyjyFn?g(6g)06DMX}U{BJ^Wf{2-B1>!O>Akyw*L{5>}_m)_iY=giDK_apVb@Z{V( z^UnM6oO5Q*nVFl$*nnkYV`C>Fj6ViNwBpfdG}cuxC~5T5F=R~P?>IOFUUt~e(TZb( zy1U?Khg%%4LfRdU+g~800j9x4umtK7*^*7!w#Xe`0ttEo%z92wp)^lJ2;&0!uhA* zR6!6L#;*fEI{nUPKeWr7ANF;-_|fi&D_4DvTtN9lM!vP2MqTMb&H-=}b$yXxkaD)Jq4#QCepBtpCy?(dQ4aNzgh5@-Raed< zbWW!x)GIy302E71luJFg8-~;&SM@o6qVq$#YDO3aivUXJRh@8-FM)*GV8r>rPy4I988%niUf0SnkF|ZbasZ89G|pe z1XWj3-V-+l^==h=ebD_qbhni!W$RI~f>@uVT+`p(P}a+yB^n`yrbFIJhV@-BgV^YH z)@s{Uj*&?adn+IKP~H}B#uxI^BY-8!x%$BtCC#Z=axEjCWL2E)1Gj+Au%OTTk_}`i zVmJ0Mc`j;-#uGpjPa#>Ou=XcpY>tD_bC$~VR$r?q2cTR+qTKD7Nh&pUY03FoBgGhw zM9z2m;45ZOVmTu-h#;KD63M!l0&aEzv6Q z*i$ZFEeBijja89aP6X;?uYhBzZ6bQIamCq;s3ld!a%*zp3DR15NVL*$BUF)ERx>%? zZCxZOH{}?TCMTXCEvi|+}k{)|HbGYw5vK~C47QQ{M#zaPA8Ql`OX!s>V`y|jEkUWLba~U>cApR z#y+M}m%b&*a`qoi$xhM@bXolP1odjn-wI5eEX)>F@6WpsqO`qoV2W3+}HxM}P)ixAeLE23k8p)ow0<_9G!>iQ2gR}QJ zYFX$v%4EM*!R8P6cnPB~f(LwmQ0Ty#=TgioJ21SCBW<;PA>g2RX~zXz8gw9(&LVs$ ziRI3ktP<-j;;y*yPtw-IlGQjXf+1J7a%^%^MbHthF5QMtq`2I)wJ#)X1Zf9*NYumE zvrQyqeeh+<%^Jm;ocJr~Y9!XzBX!%oDwbQ5lO{oQMt8{6>g?X$?kq&&w{lx{oU~>X zWy6|XJIKBI#NB2tr-KH0U;A=@i|P9pzA$inH&C|Si{6VQ@MT9GjP`Zm&^rT(KL!cF#a#2npS#(@<*1>GwH!mm( z{9X=RLYF%u?7jp)<(n5=u!GqCq8HrHz#k6!oc-dNLLYX^x4$@obbo3yktL^70vSdf z66$^fl_P1i1uu4nVS2QQGe(fRibmUj6G)o3IwDv8|J_LLuAS1s@mi}-}*DJg(G#`H{0A-f6<0X?Fl>zvKIo>#7)|I(bWFfx1Mt`tmQjibt`M6GBo`v^x&8TA|TN&<&*@fA!T!T)8b7aR2}S07*qoM6N<$f;Z-&p8x;= literal 0 HcmV?d00001 diff --git a/vector/src/main/res/drawable-xxxhdpi/chevron_down.png b/vector/src/main/res/drawable-xxxhdpi/chevron_down.png new file mode 100755 index 0000000000000000000000000000000000000000..31b1eb464ad2c87757a9a00c66b152a57548b87a GIT binary patch literal 584 zcmV-O0=NB%P)Px%0!c(cRA>d=nNLo_Kop1HD+w+!Bu0%JPXG%R#-%IWdKgdPHN1fvS-CWt5Ca=e zK!_Wa#1#pS{gpq0ly;``X4+vApzTcGyzeVa%M4Ro+U@fb#gki7bb)Gz+8 z$$sE(S$v#S(qurSnyZ}5^rdQiHR$!8SC5Cb-RgACc&Z+hqW$&U`Gcw6ijph;4;~G1 z0#B)n0P81)vp0jM?k0Rf*aUmD{Mx5*DWw{rO8A3UjTE%mq4*~sZLOSFgeXA^e$E$R zz}qDWCh%a7Fxq3l+XV?G@L°j*d(duASoEWr$($6y9KFQCl>&}CI_kv6$61we#N zpoK`^pSC%#2?$MsEj)NP1cV|%0)DH2s!Nc9&lFHq36k)+0;(oK8eU64RV288*A`H= z1Xu7z0?Lx$65d#VZwap93kmQg!3TU{0j?$Zg0~XjN`g;#YXQ;{e8Yajs)NE$SHSq7>%R~ zR+8`_vt=#vQ~DuCC-0h0Bgr=JqtWo_@br%O=QzFz9mKD1V!mhEyB@s0yh-+@6YCcf WVtT2XUdo;T0000 + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_jump_to_read_marker.xml b/vector/src/main/res/layout/view_jump_to_read_marker.xml index 35e14a649d..4ded65e8f8 100644 --- a/vector/src/main/res/layout/view_jump_to_read_marker.xml +++ b/vector/src/main/res/layout/view_jump_to_read_marker.xml @@ -11,11 +11,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:layout_marginLeft="16dp" android:layout_toStartOf="@+id/closeJumpToReadMarkerView" - android:layout_toLeftOf="@+id/closeJumpToReadMarkerView" - android:drawableStart="@drawable/jump_to_unread" - android:drawableLeft="@drawable/jump_to_unread" + android:drawableStart="@drawable/arrow_up_circle" android:drawablePadding="10dp" android:gravity="center_vertical" android:paddingTop="12dp"