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>merlins</w>
<w>moshi</w> <w>moshi</w>
<w>persistor</w> <w>persistor</w>
<w>restorable</w>
<w>restorables</w>
<w>synchronizer</w> <w>synchronizer</w>
<w>untimelined</w> <w>untimelined</w>
</words> </words>

View File

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


override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
applicationContext.setTheme(R.style.Theme_Riot)
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
Stetho.initializeWithDefaults(this) Stetho.initializeWithDefaults(this)
} }
AndroidThreeTen.init(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) { 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
import android.content.Context.MODE_PRIVATE 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.ColorProvider
import im.vector.riotredesign.core.resources.LocaleProvider import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringProvider 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.RoomSelectionRepository
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
import org.koin.dsl.module.module import org.koin.dsl.module.module


class AppModule(private val context: Context) { class AppModule(private val context: Context) {
@ -48,5 +52,22 @@ class AppModule(private val context: Context) {
RoomSelectionRepository(get()) 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 package im.vector.riotredesign.core.platform


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


abstract class RiotActivity : BaseMvRxActivity() { abstract class RiotActivity : BaseMvRxActivity() {


private val uiDisposables = CompositeDisposable() 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 { protected fun Disposable.disposeOnDestroy(): Disposable {
uiDisposables.add(this) uiDisposables.add(this)

View File

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


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


abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed { abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {


@ -27,6 +29,18 @@ abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
activity as RiotActivity 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 { override fun onBackPressed(): Boolean {
return false return false
} }
@ -39,4 +53,11 @@ abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } 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 im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import org.koin.android.ext.android.inject 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 { class HomeActivity : RiotActivity(), ToolbarConfigurable {



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


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


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

View File

@ -16,7 +16,8 @@


package im.vector.riotredesign.features.home 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.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore import im.vector.riotredesign.features.home.room.VisibleRoomStore
import im.vector.riotredesign.features.home.room.detail.timeline.CallItemFactory 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 im.vector.riotredesign.features.html.EventHtmlRenderer
import org.koin.dsl.module.module 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 { val definition = module {
Matrix.getInstance().currentSession
}


single { // Activity scope

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


single { scope(HOME_SCOPE) {
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 {
HomeNavigator() HomeNavigator()
} }


factory { scope(HOME_SCOPE) {
RoomSummaryController(get())
}

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

single {
TimelineMediaSizeProvider()
}

single {
SelectedGroupStore()
}

single {
VisibleRoomStore()
}

single {
HomePermalinkHandler(get()) HomePermalinkHandler(get())
} }


single { scope(HOME_SCOPE) {
RoomSummaryComparator() 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?, eventId: String?,
addToBackstack: Boolean = false) { addToBackstack: Boolean = false) {
Timber.v("Open room detail $roomId - $eventId - $addToBackstack") Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
if (!addToBackstack && isRoot(roomId)) {
return
}
activity?.let { activity?.let {
val args = RoomDetailArgs(roomId, eventId) val args = RoomDetailArgs(roomId, eventId)
val roomDetailFragment = RoomDetailFragment.newInstance(args) 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.R
import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.StateView import im.vector.riotredesign.core.platform.StateView
import im.vector.riotredesign.features.home.HomeModule
import kotlinx.android.synthetic.main.fragment_group_list.* 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 { class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {


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


private val viewModel: GroupListViewModel by fragmentViewModel() private val viewModel: GroupListViewModel by fragmentViewModel()

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


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


override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
groupController = GroupSummaryController(this) bindScope(getOrCreateScope(HomeModule.GROUP_LIST_SCOPE))
groupController.callback = this
stateView.contentView = epoxyRecyclerView stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(groupController) epoxyRecyclerView.setController(groupController)
viewModel.subscribe { renderState(it) } viewModel.subscribe { renderState(it) }
@ -56,7 +60,7 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
private fun renderState(state: GroupListViewState) { private fun renderState(state: GroupListViewState) {
when (state.asyncGroups) { when (state.asyncGroups) {
is Incomplete -> renderLoading() 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 arrow.core.Option
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.core.platform.RiotViewModel
@ -34,7 +33,7 @@ class GroupListViewModel(initialState: GroupListViewState,


@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: GroupListViewState): GroupListViewModel? { 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>() val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
return GroupListViewModel(state, selectedGroupHolder, currentSession) return GroupListViewModel(state, selectedGroupHolder, currentSession)
} }

View File

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


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


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

View File

@ -25,19 +25,21 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R 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.RiotFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.features.home.AvatarRenderer 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.HomePermalinkHandler
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.fragment_room_detail.*
import org.koin.android.ext.android.inject 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 @Parcelize
data class RoomDetailArgs( data class RoomDetailArgs(
@ -45,6 +47,7 @@ data class RoomDetailArgs(
val eventId: String? = null val eventId: String? = null
) : Parcelable ) : Parcelable



class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {


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


private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
private val roomDetailArgs: RoomDetailArgs by args() private val timelineEventController by inject<TimelineEventController>()

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

private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback


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


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


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

private fun setupToolbar() { private fun setupToolbar() {
val parentActivity = riotActivity val parentActivity = riotActivity
if (parentActivity is ToolbarConfigurable) { if (parentActivity is ToolbarConfigurable) {
@ -91,10 +96,14 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
val epoxyVisibilityTracker = EpoxyVisibilityTracker() val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView) epoxyVisibilityTracker.attach(recyclerView)
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
timelineEventController.addModelBuildListener { it.dispatchTo(scrollOnNewMessageCallback) } timelineEventController.addModelBuildListener {
it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback)
}
recyclerView.setController(timelineEventController) recyclerView.setController(timelineEventController)
timelineEventController.callback = this 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.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay 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.MatrixCallback
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
@ -46,7 +45,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,


@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? { 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>() val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>()
return RoomDetailViewModel(state, currentSession, visibleRoomHolder) 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.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.detail.timeline.paging.PagedListEpoxyController import im.vector.riotredesign.features.home.room.detail.timeline.paging.PagedListEpoxyController


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


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


super.add(models) super.add(models)


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



View File

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


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


object RoomDisplayed : RoomListActions()

data class FilterRooms(val roomName: CharSequence? = null) : 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success 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.failure.Failure
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R 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.extensions.setupAsSearch
import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.StateView import im.vector.riotredesign.core.platform.StateView
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.home.HomeNavigator import im.vector.riotredesign.features.home.HomeNavigator
import kotlinx.android.synthetic.main.fragment_room_list.* import kotlinx.android.synthetic.main.fragment_room_list.*
import org.koin.android.ext.android.inject 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 { 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 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? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_room_list, container, false) return inflater.inflate(R.layout.fragment_room_list, container, false)
@ -54,11 +60,36 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {


override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) 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.callback = this
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
stateView.contentView = epoxyRecyclerView stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(roomController) 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) { private fun renderState(state: RoomListViewState) {
@ -90,24 +121,13 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
stateView.state = StateView.State.Error(message) 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 ************************************************************** // RoomSummaryController.Callback **************************************************************


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


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

View File

@ -16,22 +16,23 @@


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


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


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


@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? { 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 roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>() val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>() 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 roomListFilter = BehaviorRelay.createDefault<Option<RoomListFilterName>>(Option.empty())


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

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


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


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


private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state -> 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) roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId)
_openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId))
} }
} }


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


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

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


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





View File

@ -16,24 +16,54 @@


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


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


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


data class RoomSummaries( fun isCategoryExpanded(roomCategory: RoomCategory): Boolean {
val favourites: List<RoomSummary>, return when (roomCategory) {
val directRooms: List<RoomSummary>, RoomCategory.FAVOURITE -> isFavouriteRoomsExpanded
val groupRooms: List<RoomSummary>, RoomCategory.DIRECT -> isDirectRoomsExpanded
val lowPriorities: List<RoomSummary>, RoomCategory.GROUP -> isGroupRoomsExpanded
val serverNotices: List<RoomSummary> 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 { 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 androidx.annotation.StringRes
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider import im.vector.riotredesign.core.resources.StringProvider


class RoomSummaryController(private val stringProvider: StringProvider class RoomSummaryController(private val stringProvider: StringProvider
) : TypedEpoxyController<RoomListViewState>() { ) : 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 var callback: Callback? = null


override fun buildModels(viewState: RoomListViewState) { override fun buildModels(viewState: RoomListViewState) {
val roomSummaries = viewState.asyncRooms() val roomSummaries = viewState.asyncRooms()
val favourites = roomSummaries?.favourites ?: emptyList() roomSummaries?.forEach { (category, summaries) ->
buildRoomCategory(viewState, favourites, R.string.room_list_favourites, isFavoriteRoomsExpanded) { if (summaries.isEmpty()) {
isFavoriteRoomsExpanded = !isFavoriteRoomsExpanded 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) { 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 //TODO should add some business logic later
val unreadCount = if (summaries.isEmpty()) { val unreadCount = if (summaries.isEmpty()) {
0 0
@ -117,6 +86,7 @@ class RoomSummaryController(private val stringProvider: StringProvider
} }


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