From 6045eac87a4eb5ae95d6c811482a81f7a09982b0 Mon Sep 17 00:00:00 2001 From: Nikita Fedrunov <66663241+fedrunov@users.noreply.github.com> Date: Tue, 9 Aug 2022 14:31:26 +0200 Subject: [PATCH] recents carousel for new home screen layout (#6707) --- dependencies_groups.gradle | 1 + vector/build.gradle | 3 + .../java/im/vector/app/VectorApplication.kt | 19 +++- .../room/list/home/HomeRoomListFragment.kt | 17 +++- .../room/list/home/HomeRoomListViewModel.kt | 15 ++++ .../home/room/list/home/HomeRoomSection.kt | 4 + .../recent/RecentRoomCarouselController.kt | 86 +++++++++++++++++++ .../room/list/home/recent/RecentRoomItem.kt | 78 +++++++++++++++++ .../src/main/res/layout/item_recent_room.xml | 61 +++++++++++++ 9 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomItem.kt create mode 100644 vector/src/main/res/layout/item_recent_room.xml diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index f60a77a92d..d5972ed846 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -74,6 +74,7 @@ ext.groups = [ 'com.github.javaparser', 'com.github.piasy', 'com.github.shyiko.klob', + 'com.github.rubensousa', 'com.google', 'com.google.android', 'com.google.api.grpc', diff --git a/vector/build.gradle b/vector/build.gradle index 0edaf5424e..e5bd835a8f 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -427,6 +427,9 @@ dependencies { implementation libs.airbnb.epoxyPaging implementation libs.airbnb.mavericks + // Snap Helper https://github.com/rubensousa/GravitySnapHelper + implementation 'com.github.rubensousa:gravitysnaphelper:2.2.2' + // Nightly // API-only library gplayImplementation libs.google.appdistributionApi diff --git a/vector/src/main/java/im/vector/app/VectorApplication.kt b/vector/src/main/java/im/vector/app/VectorApplication.kt index b1bd0fc308..ca7f1b6c8e 100644 --- a/vector/src/main/java/im/vector/app/VectorApplication.kt +++ b/vector/src/main/java/im/vector/app/VectorApplication.kt @@ -25,17 +25,21 @@ import android.content.res.Configuration import android.os.Handler import android.os.HandlerThread import android.os.StrictMode +import android.view.Gravity import androidx.core.provider.FontRequest import androidx.core.provider.FontsContractCompat import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDex +import androidx.recyclerview.widget.SnapHelper +import com.airbnb.epoxy.Carousel import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyController import com.airbnb.mvrx.Mavericks import com.facebook.stetho.Stetho import com.gabrielittner.threetenbp.LazyThreeTen +import com.github.rubensousa.gravitysnaphelper.GravitySnapHelper import com.mapbox.mapboxsdk.Mapbox import com.vanniktech.emoji.EmojiManager import com.vanniktech.emoji.google.GoogleEmojiProvider @@ -141,8 +145,9 @@ class VectorApplication : logInfo() LazyThreeTen.init(this) Mavericks.initialize(debugMode = false) - EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() - EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() + + configureEpoxy() + registerActivityLifecycleCallbacks(VectorActivityLifecycleCallbacks(popupAlertManager)) val fontRequest = FontRequest( "com.google.android.gms.fonts", @@ -198,6 +203,16 @@ class VectorApplication : Mapbox.getInstance(this) } + private fun configureEpoxy() { + EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() + EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() + Carousel.setDefaultGlobalSnapHelperFactory(object : Carousel.SnapHelperFactory() { + override fun buildSnapHelper(context: Context?): SnapHelper { + return GravitySnapHelper(Gravity.START) + } + }) + } + private fun enableStrictModeIfNeeded() { if (Config.ENABLE_STRICT_MODE_LOGS) { StrictMode.setThreadPolicy( diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt index f0eb027785..02122a5ee1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListFragment.kt @@ -30,6 +30,7 @@ import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.epoxy.LayoutManagerStateRestorer +import im.vector.app.core.extensions.cleanup import im.vector.app.core.platform.StateView import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.UserPreferencesProvider @@ -43,6 +44,7 @@ import im.vector.app.features.home.room.list.RoomSummaryPagedController import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel +import im.vector.app.features.home.room.list.home.recent.RecentRoomCarouselController import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.model.RoomSummary @@ -53,7 +55,8 @@ import javax.inject.Inject class HomeRoomListFragment @Inject constructor( private val roomSummaryItemFactory: RoomSummaryItemFactory, - private val userPreferencesProvider: UserPreferencesProvider + private val userPreferencesProvider: UserPreferencesProvider, + private val recentRoomCarouselController: RecentRoomCarouselController ) : VectorBaseFragment(), RoomListListener { @@ -180,6 +183,12 @@ class HomeRoomListFragment @Inject constructor( } }.adapter } + is HomeRoomSection.RecentRoomsData -> recentRoomCarouselController.also { controller -> + controller.listener = this + data.list.observe(viewLifecycleOwner) { list -> + controller.submitList(list) + } + }.adapter } } @@ -192,6 +201,12 @@ class HomeRoomListFragment @Inject constructor( ) } + override fun onDestroyView() { + views.roomListView.cleanup() + recentRoomCarouselController.listener = null + super.onDestroyView() + } + // region RoomListListener override fun onRoomClicked(room: RoomSummary) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt index b95ec50ab0..479e22497f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomListViewModel.kt @@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.SpaceFilter import org.matrix.android.sdk.api.query.toActiveSpaceOrNoFilter import org.matrix.android.sdk.api.query.toActiveSpaceOrOrphanRooms @@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.tag.RoomTag +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.state.isPublic class HomeRoomListViewModel @AssistedInject constructor( @@ -78,6 +80,7 @@ class HomeRoomListViewModel @AssistedInject constructor( private fun configureSections() { val newSections = mutableSetOf() + newSections.add(getRecentRoomsSection()) newSections.add(getAllRoomsSection()) viewModelScope.launch { @@ -89,6 +92,18 @@ class HomeRoomListViewModel @AssistedInject constructor( } } + private fun getRecentRoomsSection(): HomeRoomSection { + val liveList = session.roomService() + .getBreadcrumbsLive(roomSummaryQueryParams { + displayName = QueryStringValue.NoCondition + memberships = listOf(Membership.JOIN) + }) + + return HomeRoomSection.RecentRoomsData( + list = liveList + ) + } + private fun getAllRoomsSection(): HomeRoomSection.RoomSummaryData { val builder = RoomSummaryQueryParams.Builder().also { it.memberships = listOf(Membership.JOIN) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt index 7bfd0a769e..14c76b08bf 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeRoomSection.kt @@ -24,4 +24,8 @@ sealed class HomeRoomSection { data class RoomSummaryData( val list: LiveData> ) : HomeRoomSection() + + data class RecentRoomsData( + val list: LiveData> + ) : HomeRoomSection() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt new file mode 100644 index 0000000000..53832bbc74 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomCarouselController.kt @@ -0,0 +1,86 @@ +/* + * 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.list.home.recent + +import android.content.res.Resources +import android.util.TypedValue +import com.airbnb.epoxy.Carousel +import com.airbnb.epoxy.CarouselModelBuilder +import com.airbnb.epoxy.EpoxyController +import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.carousel +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.list.RoomListListener +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class RecentRoomCarouselController @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val resources: Resources, +) : EpoxyController() { + + private var data: List? = null + var listener: RoomListListener? = null + + private val hPadding = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 16f, + resources.displayMetrics + ).toInt() + + private val itemSpacing = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 24f, + resources.displayMetrics + ).toInt() + + fun submitList(recentList: List) { + this.data = recentList + requestModelBuild() + } + + override fun buildModels() { + val host = this + data?.let { data -> + carousel { + id("recents_carousel") + padding(Carousel.Padding(host.hPadding, host.itemSpacing)) + withModelsFrom(data) { roomSummary -> + val onClick = host.listener?.let { it::onRoomClicked } + val onLongClick = host.listener?.let { it::onRoomLongClicked } + + RecentRoomItem_() + .id(roomSummary.roomId) + .avatarRenderer(host.avatarRenderer) + .matrixItem(roomSummary.toMatrixItem()) + .unreadNotificationCount(roomSummary.notificationCount) + .showHighlighted(roomSummary.highlightCount > 0) + .itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false } + .itemClickListener { onClick?.invoke(roomSummary) } + } + } + } + } +} + +private inline fun CarouselModelBuilder.withModelsFrom( + items: List, + modelBuilder: (T) -> EpoxyModel<*> +) { + models(items.map { modelBuilder(it) }) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomItem.kt new file mode 100644 index 0000000000..6a575a2b6a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/recent/RecentRoomItem.kt @@ -0,0 +1,78 @@ +/* + * 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.list.home.recent + +import android.view.HapticFeedbackConstants +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick +import im.vector.app.features.displayname.getBestName +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.list.UnreadCounterBadgeView +import org.matrix.android.sdk.api.util.MatrixItem + +@EpoxyModelClass +abstract class RecentRoomItem : VectorEpoxyModel(R.layout.item_recent_room) { + + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute lateinit var matrixItem: MatrixItem + @EpoxyAttribute var unreadNotificationCount: Int = 0 + @EpoxyAttribute var showHighlighted: Boolean = false + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var itemLongClickListener: View.OnLongClickListener? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var itemClickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.rootView.onClick(itemClickListener) + holder.rootView.setOnLongClickListener { + it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + itemLongClickListener?.onLongClick(it) ?: false + } + + avatarRenderer.render(matrixItem, holder.avatarImageView) + holder.avatarImageView.contentDescription = matrixItem.getBestName() + holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted)) + holder.title.text = matrixItem.getBestName() + } + + override fun unbind(holder: Holder) { + holder.rootView.setOnClickListener(null) + holder.rootView.setOnLongClickListener(null) + avatarRenderer.clear(holder.avatarImageView) + super.unbind(holder) + } + + class Holder : VectorEpoxyHolder() { + val unreadCounterBadgeView by bind(R.id.recentUnreadCounterBadgeView) + val avatarImageView by bind(R.id.recentImageView) + val title by bind(R.id.recentTitle) + val rootView by bind(R.id.recentRoot) + } +} diff --git a/vector/src/main/res/layout/item_recent_room.xml b/vector/src/main/res/layout/item_recent_room.xml new file mode 100644 index 0000000000..8e17707ff3 --- /dev/null +++ b/vector/src/main/res/layout/item_recent_room.xml @@ -0,0 +1,61 @@ + + + + + + + + + + +