diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt new file mode 100644 index 0000000000..c162098cff --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt @@ -0,0 +1,75 @@ +/* + + * 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 + +import androidx.recyclerview.widget.LinearLayoutManager +import im.vector.riotx.core.di.ScreenScope +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import timber.log.Timber +import javax.inject.Inject + +@ScreenScope +class ReadMarkerHelper @Inject constructor() { + + lateinit var timelineEventController: TimelineEventController + lateinit var layoutManager: LinearLayoutManager + var callback: Callback? = null + + private var state: RoomDetailViewState? = null + + fun updateState(state: RoomDetailViewState) { + this.state = state + checkJumpToReadMarkerVisibility() + } + + fun onTimelineScrolled() { + checkJumpToReadMarkerVisibility() + } + + private fun checkJumpToReadMarkerVisibility() { + val nonNullState = this.state ?: return + val lastVisibleItem = layoutManager.findLastVisibleItemPosition() + val readMarkerId = nonNullState.asyncRoomSummary()?.readMarkerId + if (readMarkerId == null) { + callback?.onVisibilityUpdated(false, null) + } + val positionOfReadMarker = timelineEventController.searchPositionOfEvent(readMarkerId) + Timber.v("Position of readMarker: $positionOfReadMarker") + Timber.v("Position of lastVisibleItem: $lastVisibleItem") + if (positionOfReadMarker == null) { + if (nonNullState.timeline?.isLive == true && lastVisibleItem > 0) { + callback?.onVisibilityUpdated(true, readMarkerId) + } else { + callback?.onVisibilityUpdated(false, readMarkerId) + } + } else { + if (positionOfReadMarker > lastVisibleItem) { + callback?.onVisibilityUpdated(true, readMarkerId) + } else { + callback?.onVisibilityUpdated(false, readMarkerId) + } + } + } + + + interface Callback { + fun onVisibilityUpdated(show: Boolean, readMarkerId: String?) + } + + +} \ No newline at end of file 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 a55bd8bcc0..a94e1941a1 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 @@ -227,6 +227,7 @@ class RoomDetailFragment : @Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var eventHtmlRenderer: EventHtmlRenderer @Inject lateinit var vectorPreferences: VectorPreferences + @Inject lateinit var readMarkerHelper: ReadMarkerHelper private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback @@ -398,22 +399,22 @@ 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 updateComposerText(defaultContent) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) avatarRenderer.render(event.senderAvatar, event.root.senderId - ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) + ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) avatarRenderer.render(event.senderAvatar, - event.root.senderId ?: "", - event.senderName, - composerLayout.composerRelatedMessageAvatar) + event.root.senderId ?: "", + event.senderName, + composerLayout.composerRelatedMessageAvatar) composerLayout.expand { //need to do it here also when not using quick reply focusComposerAndShowKeyboard() @@ -451,9 +452,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)) } @@ -479,6 +480,13 @@ class RoomDetailFragment : it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) } + readMarkerHelper.timelineEventController = timelineEventController + readMarkerHelper.layoutManager = layoutManager + readMarkerHelper.callback = object : ReadMarkerHelper.Callback { + override fun onVisibilityUpdated(show: Boolean, readMarkerId: String?) { + jumpToReadMarkerView.render(show, readMarkerId) + } + } recyclerView.setController(timelineEventController) recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { @@ -486,6 +494,7 @@ class RoomDetailFragment : if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) { updateJumpToBottomViewVisibility() } + readMarkerHelper.onTimelineScrolled() } override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { @@ -504,26 +513,26 @@ class RoomDetailFragment : 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, composerLayout.composerEditText.text.toString())) - } - } + 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, composerLayout.composerEditText.text.toString())) + } + } - 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) } @@ -698,6 +707,7 @@ class RoomDetailFragment : } private fun renderState(state: RoomDetailViewState) { + readMarkerHelper.updateState(state) renderRoomSummary(state) val summary = state.asyncRoomSummary() val inviter = state.asyncInviter() @@ -726,7 +736,6 @@ class RoomDetailFragment : composerLayout.visibility = View.GONE notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) } - jumpToReadMarkerView.render(state.showJumpToReadMarker, summary?.readMarkerId) } private fun renderRoomSummary(state: RoomDetailViewState) { @@ -1151,7 +1160,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 72a09fbad6..22850a96f0 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 @@ -119,7 +119,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro observeRoomSummary() observeEventDisplayedActions() observeSummaryState() - observeJumpToReadMarkerViewVisibility() observeReadMarkerVisibility() observeDrafts() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() @@ -185,23 +184,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro copy( // Create a sendMode from a draft and retrieve the TimelineEvent sendMode = when (draft) { - is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) - is UserDraft.QUOTE -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.QUOTE(timelineEvent, draft.text) - } - } - is UserDraft.REPLY -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.REPLY(timelineEvent, draft.text) - } - } - is UserDraft.EDIT -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.EDIT(timelineEvent, draft.text) - } - } - } ?: SendMode.REGULAR("") + is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) + is UserDraft.QUOTE -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.QUOTE(timelineEvent, draft.text) + } + } + is UserDraft.REPLY -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.REPLY(timelineEvent, draft.text) + } + } + is UserDraft.EDIT -> { + room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> + SendMode.EDIT(timelineEvent, draft.text) + } + } + } ?: SendMode.REGULAR("") ) } } @@ -210,7 +209,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleTombstoneEvent(action: RoomDetailActions.HandleTombstoneEvent) { val tombstoneContent = action.event.getClearContent().toModel() - ?: return + ?: return val roomId = tombstoneContent.replacementRoom ?: "" val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN @@ -345,7 +344,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.EDIT -> { //is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId if (inReplyTo != null) { //TODO check if same content? room.getTimeLineEvent(inReplyTo)?.let { @@ -354,13 +353,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } else { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val existingBody = messageContent?.body ?: "" if (existingBody != action.text) { room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "", - messageContent?.type ?: MessageType.MSGTYPE_TEXT, - action.text, - action.autoMarkdown) + messageContent?.type ?: MessageType.MSGTYPE_TEXT, + action.text, + action.autoMarkdown) } else { Timber.w("Same message content, do not send edition") } @@ -371,7 +370,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() - ?: state.sendMode.timelineEvent.root.getClearContent().toModel() + ?: state.sendMode.timelineEvent.root.getClearContent().toModel() val textMsg = messageContent?.body val finalText = legacyRiotQuoteText(textMsg, action.text) @@ -702,45 +701,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro .disposeOnClear() } - private fun observeJumpToReadMarkerViewVisibility() { - Observable.combineLatest( - room.rx().liveRoomSummary() - .map { - val readMarkerId = it.readMarkerId - if (readMarkerId == null) { - Option.empty() - } else { - val readMarkerIndex = room.getTimeLineEvent(readMarkerId)?.displayIndex - ?: Int.MIN_VALUE - Option.just(readMarkerIndex) - } - } - .distinctUntilChanged(), - visibleEventsObservable.distinctUntilChanged(), - isEventVisibleObservable { it.hasReadMarker }.startWith(false).takeUntil { it }, - Function3, RoomDetailActions.TimelineEventTurnsVisible, Boolean, Boolean> { readMarkerIndex, currentVisibleEvent, isReadMarkerViewVisible -> - if (readMarkerIndex.isEmpty() || isReadMarkerViewVisible) { - false - } else { - val currentVisibleEventPosition = currentVisibleEvent.event.displayIndex - readMarkerIndex.getOrElse { Int.MIN_VALUE } < currentVisibleEventPosition - } - } - ) - .distinctUntilChanged() - .subscribe { - setState { copy(showJumpToReadMarker = it) } - } - .disposeOnClear() - } - - private fun isEventVisibleObservable(filterEvent: (TimelineEvent) -> Boolean): Observable { - return Observable.merge( - visibleEventsObservable.filter { filterEvent(it.event) }.map { true }, - invisibleEventsObservable.filter { filterEvent(it.event) }.map { false } - ) - } - private fun observeRoomSummary() { room.rx().liveRoomSummary() .execute { async -> diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 3a8afb1ebe..2be78506a0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -52,7 +52,6 @@ data class RoomDetailViewState( val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.IDLE, - val showJumpToReadMarker: Boolean = false, val highlightedEventId: String? = null, val hideReadMarker: Boolean = false ) : MvRxState { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt index 92e38c112b..08add3f0c7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ScrollOnHighlightedEventCallback.kt @@ -56,7 +56,6 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa // Do not scroll it item is already visible if (positionToScroll !in firstVisibleItem..lastVisibleItem) { Timber.v("Scroll to $positionToScroll") - // Note: Offset will be from the bottom, since the layoutManager is reversed layoutManager.scrollToPosition(positionToScroll) } scheduledEventId.set(null) 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 701e412b1d..fdc37a7f35 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 @@ -158,7 +158,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec synchronized(modelCache) { for (i in 0 until modelCache.size) { if (modelCache[i]?.eventId == viewState.highlightedEventId - || modelCache[i]?.eventId == eventIdToHighlight) { + || modelCache[i]?.eventId == eventIdToHighlight) { modelCache[i] = null } } @@ -229,8 +229,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) } }