Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

recents carousel for new home screen layout #6707

Merged
merged 10 commits into from
Aug 9, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dependencies_groups.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions vector/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 17 additions & 2 deletions vector/src/main/java/im/vector/app/VectorApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<FragmentRoomListBinding>(),
RoomListListener {

Expand Down Expand Up @@ -180,6 +183,12 @@ class HomeRoomListFragment @Inject constructor(
}
}.adapter
}
is HomeRoomSection.RecentRoomsData -> recentRoomCarouselController.also { controller ->
controller.listener = this
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any clean up of RecyclerViews in onDestroyView() of the fragment. Could we override it and clean the adapters associated to the lists (there is a RecyclerView.cleanUp() extension) + removing the listener from the controllers as we are passing the Fragment instance here.

data.list.observe(viewLifecycleOwner) { list ->
controller.submitList(list)
}
}.adapter
}
}

Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -78,6 +80,7 @@ class HomeRoomListViewModel @AssistedInject constructor(
private fun configureSections() {
val newSections = mutableSetOf<HomeRoomSection>()

newSections.add(getRecentRoomsSection())
newSections.add(getAllRoomsSection())

viewModelScope.launch {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ sealed class HomeRoomSection {
data class RoomSummaryData(
val list: LiveData<PagedList<RoomSummary>>
) : HomeRoomSection()

data class RecentRoomsData(
val list: LiveData<List<RoomSummary>>
) : HomeRoomSection()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 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
) : EpoxyController() {

private var data: List<RoomSummary>? = null
var listener: RoomListListener? = null

fun submitList(recentList: List<RoomSummary>) {
this.data = recentList
requestModelBuild()
}

override fun buildModels() {
val host = this
data?.let { data ->
carousel {
id("recents_carousel")
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 <T> CarouselModelBuilder.withModelsFrom(
items: List<T>,
modelBuilder: (T) -> EpoxyModel<*>
) {
models(items.map { modelBuilder(it) })
}
Original file line number Diff line number Diff line change
@@ -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<RecentRoomItem.Holder>(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<UnreadCounterBadgeView>(R.id.recentUnreadCounterBadgeView)
val avatarImageView by bind<ImageView>(R.id.recentImageView)
val title by bind<TextView>(R.id.recentTitle)
val rootView by bind<ViewGroup>(R.id.recentRoot)
}
}
62 changes: 62 additions & 0 deletions vector/src/main/res/layout/item_recent_room.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/recentRoot"
android:layout_width="84dp"
android:layout_height="wrap_content"
android:background="?android:colorBackground"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ Possible overdraw: Root element paints background ?android:colorBackground with a theme that also paints a background (inferred theme is @style/Theme.Vector.Light)
  • ⚠️ Possible overdraw: Root element paints background ?android:colorBackground with a theme that also paints a background (inferred theme is @style/Theme.Vector.Light)

android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • ⚠️ Attribute android:foreground has no effect on API levels lower than 23 (current min is 21)
  • ⚠️ Attribute android:foreground has no effect on API levels lower than 23 (current min is 21)

android:paddingVertical="12dp"
tools:viewBindingIgnore="true">

<ImageView
android:id="@+id/recentImageView"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginHorizontal="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@sample/room_round_avatars" />

<im.vector.app.features.home.room.list.UnreadCounterBadgeView
android:id="@+id/recentUnreadCounterBadgeView"
style="@style/Widget.Vector.TextView.Micro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:minWidth="18dp"
android:minHeight="18dp"
android:textColor="?colorOnError"
android:visibility="gone"
app:layout_constraintCircle="@id/recentImageView"
app:layout_constraintCircleAngle="45"
app:layout_constraintCircleRadius="28dp"
tools:background="@drawable/bg_unread_highlight"
tools:ignore="MissingConstraints"
tools:text="24"
tools:visibility="visible" />


<TextView
android:id="@+id/recentTitle"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:lines="1"
android:textColor="?vctr_content_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/recentImageView"
tools:text="Coffee" />


</androidx.constraintlayout.widget.ConstraintLayout>