From 008432af3687baf53f0dc32824dee27fd57b8d69 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 10 Nov 2022 18:28:03 +0100 Subject: [PATCH] Move TypingView into the timeline as another item (#7565) * Typing view as item in list * Don't show TypingItem if we're showing a forward loader --- changelog.d/7496.feature | 1 + .../app/core/ui/views/TypingMessageView.kt | 5 -- .../home/room/detail/TimelineFragment.kt | 12 --- .../timeline/TimelineEventController.kt | 15 +++- .../room/detail/timeline/item/TypingItem.kt | 76 +++++++++++++++++++ .../src/main/res/layout/fragment_timeline.xml | 14 +--- .../src/main/res/layout/item_typing_users.xml | 8 ++ 7 files changed, 98 insertions(+), 33 deletions(-) create mode 100644 changelog.d/7496.feature create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt create mode 100644 vector/src/main/res/layout/item_typing_users.xml diff --git a/changelog.d/7496.feature b/changelog.d/7496.feature new file mode 100644 index 0000000000..721164ee06 --- /dev/null +++ b/changelog.d/7496.feature @@ -0,0 +1 @@ +Move TypingView inside the timeline items. diff --git a/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt index 263f043fad..b6dc404d01 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/TypingMessageView.kt @@ -48,9 +48,4 @@ class TypingMessageView @JvmOverloads constructor( views.typingUserText.text = typingHelper.getNotificationTypingMessage(typingUsers) views.typingUserAvatars.render(typingUsers, avatarRenderer) } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - removeAllViews() - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 60dd1320d3..e1392b7580 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1154,7 +1154,6 @@ class TimelineFragment : } val summary = mainState.asyncRoomSummary() renderToolbar(summary) - renderTypingMessageNotification(summary, mainState) views.removeJitsiWidgetView.render(mainState) if (mainState.hasFailedSending) { lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = true, createFailedMessagesWarningCallback())?.isVisible = true @@ -1230,17 +1229,6 @@ class TimelineFragment : voiceMessageRecorderContainer.isVisible = false } - private fun renderTypingMessageNotification(roomSummary: RoomSummary?, state: RoomDetailViewState) { - if (!isThreadTimeLine() && roomSummary != null) { - views.typingMessageView.isInvisible = state.typingUsers.isNullOrEmpty() - state.typingUsers - ?.take(MAX_TYPING_MESSAGE_USERS_COUNT) - ?.let { senders -> views.typingMessageView.render(senders, avatarRenderer) } - } else { - views.typingMessageView.isInvisible = true - } - } - private fun renderToolbar(roomSummary: RoomSummary?) { when { isLocalRoom() -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 18c626bda8..57ad4331ce 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -32,6 +32,7 @@ import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.prevOrNull import im.vector.app.core.time.Clock +import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.JitsiState import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailViewState @@ -57,6 +58,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationD import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryEvents import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem +import im.vector.app.features.home.room.detail.timeline.item.TypingItem_ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.media.AttachmentData import im.vector.app.features.media.ImageContentRenderer @@ -94,6 +96,7 @@ class TimelineEventController @Inject constructor( private val readReceiptsItemFactory: ReadReceiptsItemFactory, private val reactionListFactory: ReactionsSummaryFactory, private val clock: Clock, + private val avatarRenderer: AvatarRenderer, ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { /** @@ -104,7 +107,7 @@ class TimelineEventController @Inject constructor( val highlightedEventId: String? = null, val jitsiState: JitsiState = JitsiState(), val roomSummary: RoomSummary? = null, - val rootThreadEventId: String? = null + val rootThreadEventId: String? = null, ) { constructor(state: RoomDetailViewState) : this( @@ -112,7 +115,7 @@ class TimelineEventController @Inject constructor( highlightedEventId = state.highlightedEventId, jitsiState = state.jitsiState, roomSummary = state.asyncRoomSummary(), - rootThreadEventId = state.rootThreadEventId + rootThreadEventId = state.rootThreadEventId, ) fun isFromThreadTimeline(): Boolean = rootThreadEventId != null @@ -286,7 +289,7 @@ class TimelineEventController @Inject constructor( private val interceptorHelper = TimelineControllerInterceptorHelper( ::positionOfReadMarker, - adapterPositionMapping + adapterPositionMapping, ) init { @@ -334,6 +337,12 @@ class TimelineEventController @Inject constructor( .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS) .addWhenLoading(Timeline.Direction.FORWARDS) + if (!showingForwardLoader) { + val typingUsers = partialState.roomSummary?.typingUsers.orEmpty() + val typingItem = TypingItem_().id("typing_view").avatarRenderer(avatarRenderer).users(typingUsers) + add(typingItem) + } + val timelineModels = getModels() add(timelineModels) if (hasReachedInvite && hasUTD) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt new file mode 100644 index 0000000000..2ca0ebea48 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/TypingItem.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022 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.app.features.home.room.detail.timeline.item + +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.airbnb.epoxy.EpoxyModelWithHolder +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.ui.views.TypingMessageView +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.session.room.sender.SenderInfo + +@EpoxyModelClass +abstract class TypingItem : EpoxyModelWithHolder() { + + companion object { + private const val MAX_TYPING_MESSAGE_USERS_COUNT = 4 + } + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + lateinit var avatarRenderer: AvatarRenderer + + @EpoxyAttribute + var users: List = emptyList() + + override fun getDefaultLayout(): Int = R.layout.item_typing_users + + override fun bind(holder: TypingHolder) { + super.bind(holder) + + val typingUsers = users.take(MAX_TYPING_MESSAGE_USERS_COUNT) + holder.typingView.apply { + animate().cancel() + val duration = 100L + if (typingUsers.isEmpty()) { + animate().translationY(height.toFloat()) + .alpha(0f) + .setDuration(duration) + .withEndAction { + isInvisible = true + }.start() + } else { + isVisible = true + + translationY = height.toFloat() + alpha = 0f + render(typingUsers, avatarRenderer) + animate().translationY(0f) + .alpha(1f) + .setDuration(duration) + .start() + } + } + } + + class TypingHolder : VectorEpoxyHolder() { + val typingView by bind(R.id.typingMessageView) + } +} diff --git a/vector/src/main/res/layout/fragment_timeline.xml b/vector/src/main/res/layout/fragment_timeline.xml index 100cf694e0..2d07464e89 100644 --- a/vector/src/main/res/layout/fragment_timeline.xml +++ b/vector/src/main/res/layout/fragment_timeline.xml @@ -85,7 +85,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:overScrollMode="always" - app:layout_constraintBottom_toTopOf="@id/typingMessageView" + app:layout_constraintBottom_toTopOf="@id/bottomBarrier" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" @@ -107,18 +107,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/removeJitsiWidgetView" /> - - +