State issues : restore recyclerview state + fix DI issues

This commit is contained in:
ganfra 2019-02-28 18:50:30 +01:00
parent 753e70775a
commit fd3fce6deb
21 changed files with 371 additions and 188 deletions

View File

@ -6,6 +6,8 @@
<w>merlins</w>
<w>moshi</w>
<w>persistor</w>
<w>restorable</w>
<w>restorables</w>
<w>synchronizer</w>
<w>untimelined</w>
</words>

View File

@ -23,6 +23,7 @@ import com.facebook.stetho.Stetho
import com.jakewharton.threetenabp.AndroidThreeTen
import im.vector.matrix.android.BuildConfig
import im.vector.riotredesign.core.di.AppModule
import im.vector.riotredesign.features.home.HomeModule
import org.koin.log.EmptyLogger
import org.koin.standalone.StandAloneContext.startKoin
import timber.log.Timber
@ -32,12 +33,15 @@ class Riot : Application() {

override fun onCreate() {
super.onCreate()
applicationContext.setTheme(R.style.Theme_Riot)
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
Stetho.initializeWithDefaults(this)
}
AndroidThreeTen.init(this)
startKoin(listOf(AppModule(this).definition), logger = EmptyLogger())
val appModule = AppModule(applicationContext).definition
val homeModule = HomeModule(applicationContext).definition
startKoin(listOf(appModule, homeModule), logger = EmptyLogger())
}

override fun attachBaseContext(base: Context) {

View File

@ -18,10 +18,14 @@ package im.vector.riotredesign.core.di

import android.content.Context
import android.content.Context.MODE_PRIVATE
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.core.resources.ColorProvider
import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
import org.koin.dsl.module.module

class AppModule(private val context: Context) {
@ -48,5 +52,22 @@ class AppModule(private val context: Context) {
RoomSelectionRepository(get())
}

single {
SelectedGroupStore()
}

single {
VisibleRoomStore()
}

single {
RoomSummaryComparator()
}

factory {
Matrix.getInstance().currentSession
}


}
}

View File

@ -0,0 +1,48 @@
/*
* 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.riotredesign.core.epoxy

import android.os.Bundle
import android.os.Parcelable
import androidx.recyclerview.widget.RecyclerView
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
import im.vector.riotredesign.core.platform.Restorable
import java.util.concurrent.atomic.AtomicReference

private const val LAYOUT_MANAGER_STATE = "LAYOUT_MANAGER_STATE"

class LayoutManagerStateRestorer(private val layoutManager: RecyclerView.LayoutManager) : Restorable, DefaultListUpdateCallback {

private var layoutManagerState = AtomicReference<Parcelable?>()

override fun onSaveInstanceState(outState: Bundle) {
val layoutManagerState = layoutManager.onSaveInstanceState()
outState.putParcelable(LAYOUT_MANAGER_STATE, layoutManagerState)
}

override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
val parcelable = savedInstanceState?.getParcelable<Parcelable>(LAYOUT_MANAGER_STATE)
layoutManagerState.set(parcelable)
}

override fun onInserted(position: Int, count: Int) {
layoutManagerState.getAndSet(null)?.also {
layoutManager.onRestoreInstanceState(it)
}
}

}

View File

@ -0,0 +1,27 @@
/*
* 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.riotredesign.core.platform

import android.os.Bundle

interface Restorable {

fun onSaveInstanceState(outState: Bundle)

fun onRestoreInstanceState(savedInstanceState: Bundle?)

}

View File

@ -16,13 +16,34 @@

package im.vector.riotredesign.core.platform

import android.os.Bundle
import androidx.annotation.MainThread
import com.airbnb.mvrx.BaseMvRxActivity
import com.bumptech.glide.util.Util
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable

abstract class RiotActivity : BaseMvRxActivity() {

private val uiDisposables = CompositeDisposable()
private val restorables = ArrayList<Restorable>()

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
restorables.forEach { it.onSaveInstanceState(outState) }
}

override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
restorables.forEach { it.onRestoreInstanceState(savedInstanceState) }
super.onRestoreInstanceState(savedInstanceState)
}

@MainThread
protected fun <T : Restorable> T.register(): T {
Util.assertMainThread()
restorables.add(this)
return this
}

protected fun Disposable.disposeOnDestroy(): Disposable {
uiDisposables.add(this)

View File

@ -18,8 +18,10 @@ package im.vector.riotredesign.core.platform

import android.os.Bundle
import android.os.Parcelable
import androidx.annotation.MainThread
import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.MvRx
import com.bumptech.glide.util.Util.assertMainThread

abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {

@ -27,6 +29,18 @@ abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
activity as RiotActivity
}

private val restorables = ArrayList<Restorable>()

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
restorables.forEach { it.onSaveInstanceState(outState) }
}

override fun onViewStateRestored(savedInstanceState: Bundle?) {
restorables.forEach { it.onRestoreInstanceState(savedInstanceState) }
super.onViewStateRestored(savedInstanceState)
}

override fun onBackPressed(): Boolean {
return false
}
@ -39,4 +53,11 @@ abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
}

@MainThread
protected fun <T : Restorable> T.register(): T {
assertMainThread()
restorables.add(this)
return this
}

}

View File

@ -37,12 +37,12 @@ import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
import kotlinx.android.synthetic.main.activity_home.*
import org.koin.android.ext.android.inject
import org.koin.standalone.StandAloneContext.loadKoinModules
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope


class HomeActivity : RiotActivity(), ToolbarConfigurable {


private val homeActivityViewModel: HomeActivityViewModel by viewModel()
private val homeNavigator by inject<HomeNavigator>()

@ -53,10 +53,10 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable {
}

override fun onCreate(savedInstanceState: Bundle?) {
loadKoinModules(listOf(HomeModule(this).definition))
homeNavigator.activity = this
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
bindScope(getOrCreateScope(HomeModule.HOME_SCOPE))
homeNavigator.activity = this
drawerLayout.addDrawerListener(drawerListener)
if (savedInstanceState == null) {
val homeDrawerFragment = HomeDrawerFragment.newInstance()

View File

@ -16,7 +16,8 @@

package im.vector.riotredesign.features.home

import im.vector.matrix.android.api.Matrix
import android.content.Context
import im.vector.riotredesign.features.home.group.GroupSummaryController
import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import im.vector.riotredesign.features.home.room.detail.timeline.CallItemFactory
@ -35,84 +36,83 @@ import im.vector.riotredesign.features.home.room.list.RoomSummaryController
import im.vector.riotredesign.features.html.EventHtmlRenderer
import org.koin.dsl.module.module

class HomeModule(homeActivity: HomeActivity) {
class HomeModule(context: Context) {

val definition = module(override = true) {
companion object {
const val HOME_SCOPE = "HOME_SCOPE"
const val ROOM_DETAIL_SCOPE = "ROOM_DETAIL_SCOPE"
const val ROOM_LIST_SCOPE = "ROOM_LIST_SCOPE"
const val GROUP_LIST_SCOPE = "GROUP_LIST_SCOPE"
}

single {
Matrix.getInstance().currentSession
}
val definition = module {

single {
// Activity scope

scope(HOME_SCOPE) {
TimelineDateFormatter(get())
}

single {
EventHtmlRenderer(homeActivity, get())
}

single {
MessageItemFactory(get(), get(), get(), get())
}

single {
RoomNameItemFactory(get())
}

single {
RoomTopicItemFactory(get())
}

single {
RoomMemberItemFactory(get())
}

single {
CallItemFactory(get())
}

single {
RoomHistoryVisibilityItemFactory(get())
}

single {
DefaultItemFactory()
}

single {
TimelineItemFactory(get(), get(), get(), get(), get(), get(), get())
}

single {
scope(HOME_SCOPE) {
HomeNavigator()
}

factory {
RoomSummaryController(get())
}

factory { (roomId: String) ->
TimelineEventController(roomId, get(), get(), get())
}

single {
TimelineMediaSizeProvider()
}

single {
SelectedGroupStore()
}

single {
VisibleRoomStore()
}

single {
scope(HOME_SCOPE) {
HomePermalinkHandler(get())
}

single {
RoomSummaryComparator()
scope(HOME_SCOPE) {
RoomNameItemFactory(get())
}

scope(HOME_SCOPE) {
RoomTopicItemFactory(get())
}

scope(HOME_SCOPE) {
RoomMemberItemFactory(get())
}

scope(HOME_SCOPE) {
CallItemFactory(get())
}

scope(HOME_SCOPE) {
RoomHistoryVisibilityItemFactory(get())
}

scope(HOME_SCOPE) {
DefaultItemFactory()
}

scope(HOME_SCOPE) {
TimelineMediaSizeProvider()
}

scope(HOME_SCOPE) {
EventHtmlRenderer(context, get())
}

scope(HOME_SCOPE) {
MessageItemFactory(get(), get(), get(), get())
}

scope(HOME_SCOPE) {
TimelineItemFactory(get(), get(), get(), get(), get(), get(), get())
}

// Fragment scopes

scope(ROOM_DETAIL_SCOPE) {
TimelineEventController(get(), get(), get())
}

scope(ROOM_LIST_SCOPE) {
RoomSummaryController(get())
}

scope(GROUP_LIST_SCOPE) {
GroupSummaryController()
}



View File

@ -36,9 +36,6 @@ class HomeNavigator {
eventId: String?,
addToBackstack: Boolean = false) {
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
if (!addToBackstack && isRoot(roomId)) {
return
}
activity?.let {
val args = RoomDetailArgs(roomId, eventId)
val roomDetailFragment = RoomDetailFragment.newInstance(args)

View File

@ -27,7 +27,11 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.StateView
import im.vector.riotredesign.features.home.HomeModule
import kotlinx.android.synthetic.main.fragment_group_list.*
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope

class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {

@ -38,8 +42,7 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
}

private val viewModel: GroupListViewModel by fragmentViewModel()

private lateinit var groupController: GroupSummaryController
private val groupController by inject<GroupSummaryController>()

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_group_list, container, false)
@ -47,7 +50,8 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
groupController = GroupSummaryController(this)
bindScope(getOrCreateScope(HomeModule.GROUP_LIST_SCOPE))
groupController.callback = this
stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(groupController)
viewModel.subscribe { renderState(it) }
@ -56,7 +60,7 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
private fun renderState(state: GroupListViewState) {
when (state.asyncGroups) {
is Incomplete -> renderLoading()
is Success -> renderSuccess(state)
is Success -> renderSuccess(state)
}
}


View File

@ -19,7 +19,6 @@ package im.vector.riotredesign.features.home.group
import arrow.core.Option
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel
@ -34,7 +33,7 @@ class GroupListViewModel(initialState: GroupListViewState,

@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: GroupListViewState): GroupListViewModel? {
val currentSession = Matrix.getInstance().currentSession
val currentSession = viewModelContext.activity.get<Session>()
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
return GroupListViewModel(state, selectedGroupHolder, currentSession)
}

View File

@ -19,8 +19,9 @@ package im.vector.riotredesign.features.home.group
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.group.model.GroupSummary

class GroupSummaryController(private val callback: Callback? = null
) : TypedEpoxyController<GroupListViewState>() {
class GroupSummaryController : TypedEpoxyController<GroupListViewState>() {

var callback: Callback? = null

override fun buildModels(viewState: GroupListViewState) {
buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup)

View File

@ -25,19 +25,21 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.home.HomePermalinkHandler
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.*
import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope

@Parcelize
data class RoomDetailArgs(
@ -45,6 +47,7 @@ data class RoomDetailArgs(
val eventId: String? = null
) : Parcelable


class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {

companion object {
@ -57,10 +60,9 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
}

private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
private val roomDetailArgs: RoomDetailArgs by args()

private val timelineEventController by inject<TimelineEventController> { parametersOf(roomDetailArgs.roomId) }
private val timelineEventController by inject<TimelineEventController>()
private val homePermalinkHandler by inject<HomePermalinkHandler>()

private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -69,6 +71,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
setupRecyclerView()
setupToolbar()
setupSendButton()
@ -80,6 +83,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
}

// PRIVATE METHODS *****************************************************************************

private fun setupToolbar() {
val parentActivity = riotActivity
if (parentActivity is ToolbarConfigurable) {
@ -91,10 +96,14 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView)
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
recyclerView.layoutManager = layoutManager
recyclerView.setHasFixedSize(true)
timelineEventController.addModelBuildListener { it.dispatchTo(scrollOnNewMessageCallback) }
timelineEventController.addModelBuildListener {
it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback)
}
recyclerView.setController(timelineEventController)
timelineEventController.callback = this
}

View File

@ -19,7 +19,6 @@ package im.vector.riotredesign.features.home.room.detail
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
@ -46,7 +45,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,

@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? {
val currentSession = Matrix.getInstance().currentSession
val currentSession = viewModelContext.activity.get<Session>()
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>()
return RoomDetailViewModel(state, currentSession, visibleRoomHolder)
}

View File

@ -29,8 +29,7 @@ import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.detail.timeline.paging.PagedListEpoxyController

class TimelineEventController(private val roomId: String,
private val dateFormatter: TimelineDateFormatter,
class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
private val timelineItemFactory: TimelineItemFactory,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider
) : PagedListEpoxyController<TimelineEvent>(
@ -82,7 +81,7 @@ class TimelineEventController(private val roomId: String,
}
if (addDaySeparator) {
val formattedDay = dateFormatter.formatMessageDay(date)
val daySeparatorItem = DaySeparatorItem_().formattedDay(formattedDay).id(roomId + formattedDay)
val daySeparatorItem = DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
epoxyModels.add(daySeparatorItem)
}
return epoxyModels
@ -90,13 +89,13 @@ class TimelineEventController(private val roomId: String,

override fun addModels(models: List<EpoxyModel<*>>) {
LoadingItemModel_()
.id(roomId + "forward_loading_item")
.id("forward_loading_item")
.addIf(isLoadingForward, this)

super.add(models)

LoadingItemModel_()
.id(roomId + "backward_loading_item")
.id("backward_loading_item")
.addIf(!hasReachedEnd, this)
}


View File

@ -22,8 +22,8 @@ sealed class RoomListActions {

data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions()

object RoomDisplayed : RoomListActions()

data class FilterRooms(val roomName: CharSequence? = null) : RoomListActions()

data class ToggleCategory(val category: RoomCategory) : RoomListActions()

}

View File

@ -22,19 +22,25 @@ import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.extensions.setupAsSearch
import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.StateView
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.home.HomeNavigator
import kotlinx.android.synthetic.main.fragment_room_list.*
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope

class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {

@ -44,9 +50,9 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
}
}

private val homeNavigator by inject<HomeNavigator>()
private val roomController by inject<RoomSummaryController>()
private val homeViewModel: RoomListViewModel by activityViewModel()
private val homeNavigator by inject<HomeNavigator>()
private val roomListViewModel: RoomListViewModel by fragmentViewModel()

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_room_list, container, false)
@ -54,11 +60,36 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(HomeModule.ROOM_LIST_SCOPE))
setupRecyclerView()
setupFilterView()
roomListViewModel.subscribe { renderState(it) }
roomListViewModel.openRoomLiveData.observeEvent(this) {
homeNavigator.openRoomDetail(it, null)
}
}

private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
epoxyRecyclerView.layoutManager = layoutManager
roomController.callback = this
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(roomController)
setupFilterView()
homeViewModel.subscribe { renderState(it) }
}

private fun setupFilterView() {
filterRoomView.setupAsSearch()
filterRoomView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) = Unit

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
roomListViewModel.accept(RoomListActions.FilterRooms(s))
}
})
}

private fun renderState(state: RoomListViewState) {
@ -90,24 +121,13 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
stateView.state = StateView.State.Error(message)
}

private fun setupFilterView() {
filterRoomView.setupAsSearch()
filterRoomView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) = Unit

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit

override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
homeViewModel.accept(RoomListActions.FilterRooms(s))
}
})
}

// RoomSummaryController.Callback **************************************************************

override fun onRoomSelected(room: RoomSummary) {
homeViewModel.accept(RoomListActions.SelectRoom(room))
homeNavigator.openRoomDetail(room.roomId, null)
roomListViewModel.accept(RoomListActions.SelectRoom(room))
}

override fun onToggleRoomCategory(roomCategory: RoomCategory) {
roomListViewModel.accept(RoomListActions.ToggleCategory(roomCategory))
}
}

View File

@ -16,22 +16,23 @@

package im.vector.riotredesign.features.home.room.list

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel
import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import io.reactivex.Observable
import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit

@ -49,7 +50,7 @@ class RoomListViewModel(initialState: RoomListViewState,

@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? {
val currentSession = Matrix.getInstance().currentSession
val currentSession = viewModelContext.activity.get<Session>()
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>()
@ -61,6 +62,10 @@ class RoomListViewModel(initialState: RoomListViewState,

private val roomListFilter = BehaviorRelay.createDefault<Option<RoomListFilterName>>(Option.empty())

private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
val openRoomLiveData: LiveData<LiveEvent<String>>
get() = _openRoomLiveData

init {
observeRoomSummaries()
observeVisibleRoom()
@ -68,16 +73,18 @@ class RoomListViewModel(initialState: RoomListViewState,

fun accept(action: RoomListActions) {
when (action) {
is RoomListActions.SelectRoom -> handleSelectRoom(action)
is RoomListActions.FilterRooms -> handleFilterRooms(action)
is RoomListActions.SelectRoom -> handleSelectRoom(action)
is RoomListActions.FilterRooms -> handleFilterRooms(action)
is RoomListActions.ToggleCategory -> handleToggleCategory(action)
}
}

// PRIVATE METHODS *****************************************************************************

private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state ->
if (state.selectedRoomId != action.roomSummary.roomId) {
if (state.visibleRoomId != action.roomSummary.roomId) {
roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId)
_openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId))
}
}

@ -86,10 +93,14 @@ class RoomListViewModel(initialState: RoomListViewState,
roomListFilter.accept(optionalFilter)
}

private fun handleToggleCategory(action: RoomListActions.ToggleCategory) = setState {
this.toggle(action.category)
}

private fun observeVisibleRoom() {
visibleRoomHolder.observe()
.doOnNext {
setState { copy(selectedRoomId = it) }
setState { copy(visibleRoomId = it) }
}
.subscribe()
.disposeOnClear()
@ -159,13 +170,13 @@ class RoomListViewModel(initialState: RoomListViewState,
}
}

return RoomSummaries(
favourites = favourites.sortedWith(roomSummaryComparator),
directRooms = directChats.sortedWith(roomSummaryComparator),
groupRooms = groupRooms.sortedWith(roomSummaryComparator),
lowPriorities = lowPriorities.sortedWith(roomSummaryComparator),
serverNotices = serverNotices.sortedWith(roomSummaryComparator)
)
return RoomSummaries().apply {
put(RoomCategory.FAVOURITE, favourites.sortedWith(roomSummaryComparator))
put(RoomCategory.DIRECT, directChats.sortedWith(roomSummaryComparator))
put(RoomCategory.GROUP, groupRooms.sortedWith(roomSummaryComparator))
put(RoomCategory.LOW_PRIORITY, lowPriorities.sortedWith(roomSummaryComparator))
put(RoomCategory.SERVER_NOTICE, serverNotices.sortedWith(roomSummaryComparator))
}
}



View File

@ -16,24 +16,54 @@

package im.vector.riotredesign.features.home.room.list

import androidx.annotation.StringRes
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R

data class RoomListViewState(
val asyncRooms: Async<RoomSummaries> = Uninitialized,
val selectedRoomId: String? = null
) : MvRxState
val visibleRoomId: String? = null,
val isFavouriteRoomsExpanded: Boolean = true,
val isDirectRoomsExpanded: Boolean = false,
val isGroupRoomsExpanded: Boolean = false,
val isLowPriorityRoomsExpanded: Boolean = false,
val isServerNoticeRoomsExpanded: Boolean = false
) : MvRxState {

data class RoomSummaries(
val favourites: List<RoomSummary>,
val directRooms: List<RoomSummary>,
val groupRooms: List<RoomSummary>,
val lowPriorities: List<RoomSummary>,
val serverNotices: List<RoomSummary>
)
fun isCategoryExpanded(roomCategory: RoomCategory): Boolean {
return when (roomCategory) {
RoomCategory.FAVOURITE -> isFavouriteRoomsExpanded
RoomCategory.DIRECT -> isDirectRoomsExpanded
RoomCategory.GROUP -> isGroupRoomsExpanded
RoomCategory.LOW_PRIORITY -> isLowPriorityRoomsExpanded
RoomCategory.SERVER_NOTICE -> isServerNoticeRoomsExpanded
}
}

fun toggle(roomCategory: RoomCategory): RoomListViewState {
return when (roomCategory) {
RoomCategory.FAVOURITE -> copy(isFavouriteRoomsExpanded = !isFavouriteRoomsExpanded)
RoomCategory.DIRECT -> copy(isDirectRoomsExpanded = !isDirectRoomsExpanded)
RoomCategory.GROUP -> copy(isGroupRoomsExpanded = !isGroupRoomsExpanded)
RoomCategory.LOW_PRIORITY -> copy(isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded)
RoomCategory.SERVER_NOTICE -> copy(isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded)
}
}
}

typealias RoomSummaries = LinkedHashMap<RoomCategory, List<RoomSummary>>

enum class RoomCategory(@StringRes val titleRes: Int) {
FAVOURITE(R.string.room_list_favourites),
DIRECT(R.string.room_list_direct),
GROUP(R.string.room_list_group),
LOW_PRIORITY(R.string.room_list_low_priority),
SERVER_NOTICE(R.string.room_list_system_alert)
}

fun RoomSummaries?.isNullOrEmpty(): Boolean {
return this == null || (directRooms.isEmpty() && groupRooms.isEmpty() && favourites.isEmpty() && lowPriorities.isEmpty() && serverNotices.isEmpty())
return this == null || isEmpty()
}

View File

@ -19,65 +19,34 @@ package im.vector.riotredesign.features.home.room.list
import androidx.annotation.StringRes
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider

class RoomSummaryController(private val stringProvider: StringProvider
) : TypedEpoxyController<RoomListViewState>() {

private var isFavoriteRoomsExpanded = true
private var isDirectRoomsExpanded = false
private var isGroupRoomsExpanded = false
private var isLowPriorityRoomsExpanded = false
private var isServerNoticeRoomsExpanded = false

var callback: Callback? = null

override fun buildModels(viewState: RoomListViewState) {
val roomSummaries = viewState.asyncRooms()
val favourites = roomSummaries?.favourites ?: emptyList()
buildRoomCategory(viewState, favourites, R.string.room_list_favourites, isFavoriteRoomsExpanded) {
isFavoriteRoomsExpanded = !isFavoriteRoomsExpanded
roomSummaries?.forEach { (category, summaries) ->
if (summaries.isEmpty()) {
return@forEach
} else {
val isExpanded = viewState.isCategoryExpanded(category)
buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) {
callback?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(summaries, viewState.visibleRoomId)
}
}
}
if (isFavoriteRoomsExpanded) {
buildRoomModels(favourites, viewState.selectedRoomId)
}

val directRooms = roomSummaries?.directRooms ?: emptyList()
buildRoomCategory(viewState, directRooms, R.string.room_list_direct, isDirectRoomsExpanded) {
isDirectRoomsExpanded = !isDirectRoomsExpanded
}
if (isDirectRoomsExpanded) {
buildRoomModels(directRooms, viewState.selectedRoomId)
}

val groupRooms = roomSummaries?.groupRooms ?: emptyList()
buildRoomCategory(viewState, groupRooms, R.string.room_list_group, isGroupRoomsExpanded) {
isGroupRoomsExpanded = !isGroupRoomsExpanded
}
if (isGroupRoomsExpanded) {
buildRoomModels(groupRooms, viewState.selectedRoomId)
}

val lowPriorities = roomSummaries?.lowPriorities ?: emptyList()
buildRoomCategory(viewState, lowPriorities, R.string.room_list_low_priority, isLowPriorityRoomsExpanded) {
isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded
}
if (isLowPriorityRoomsExpanded) {
buildRoomModels(lowPriorities, viewState.selectedRoomId)
}

val serverNotices = roomSummaries?.serverNotices ?: emptyList()
buildRoomCategory(viewState, serverNotices, R.string.room_list_system_alert, isServerNoticeRoomsExpanded) {
isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded
}
if (isServerNoticeRoomsExpanded) {
buildRoomModels(serverNotices, viewState.selectedRoomId)
}

}

private fun buildRoomCategory(viewState: RoomListViewState, summaries: List<RoomSummary>, @StringRes titleRes: Int, isExpanded: Boolean, mutateExpandedState: () -> Unit) {
if (summaries.isEmpty()) {
return
}
//TODO should add some business logic later
val unreadCount = if (summaries.isEmpty()) {
0
@ -117,6 +86,7 @@ class RoomSummaryController(private val stringProvider: StringProvider
}

interface Callback {
fun onToggleRoomCategory(roomCategory: RoomCategory)
fun onRoomSelected(room: RoomSummary)
}