diff --git a/.idea/dictionaries/ganfra.xml b/.idea/dictionaries/ganfra.xml index 7e1fdcdd..c3f99f8d 100644 --- a/.idea/dictionaries/ganfra.xml +++ b/.idea/dictionaries/ganfra.xml @@ -6,6 +6,8 @@ merlins moshi persistor + restorable + restorables synchronizer untimelined diff --git a/app/src/main/java/im/vector/riotredesign/Riot.kt b/app/src/main/java/im/vector/riotredesign/Riot.kt index 8385443c..acc59cde 100644 --- a/app/src/main/java/im/vector/riotredesign/Riot.kt +++ b/app/src/main/java/im/vector/riotredesign/Riot.kt @@ -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) { diff --git a/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt b/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt index bbc97315..f67c9c11 100644 --- a/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt +++ b/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt @@ -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 + } + + } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/epoxy/LayoutManagerStateRestorer.kt b/app/src/main/java/im/vector/riotredesign/core/epoxy/LayoutManagerStateRestorer.kt new file mode 100644 index 00000000..b4b9f181 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/core/epoxy/LayoutManagerStateRestorer.kt @@ -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() + + override fun onSaveInstanceState(outState: Bundle) { + val layoutManagerState = layoutManager.onSaveInstanceState() + outState.putParcelable(LAYOUT_MANAGER_STATE, layoutManagerState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle?) { + val parcelable = savedInstanceState?.getParcelable(LAYOUT_MANAGER_STATE) + layoutManagerState.set(parcelable) + } + + override fun onInserted(position: Int, count: Int) { + layoutManagerState.getAndSet(null)?.also { + layoutManager.onRestoreInstanceState(it) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/platform/Restorable.kt b/app/src/main/java/im/vector/riotredesign/core/platform/Restorable.kt new file mode 100644 index 00000000..683fed73 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/core/platform/Restorable.kt @@ -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?) + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/platform/RiotActivity.kt b/app/src/main/java/im/vector/riotredesign/core/platform/RiotActivity.kt index 2465399d..db1aa476 100644 --- a/app/src/main/java/im/vector/riotredesign/core/platform/RiotActivity.kt +++ b/app/src/main/java/im/vector/riotredesign/core/platform/RiotActivity.kt @@ -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() + + 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.register(): T { + Util.assertMainThread() + restorables.add(this) + return this + } protected fun Disposable.disposeOnDestroy(): Disposable { uiDisposables.add(this) diff --git a/app/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt b/app/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt index 72a7f4b2..0b7996fa 100644 --- a/app/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt @@ -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() + + 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.register(): T { + assertMainThread() + restorables.add(this) + return this + } + } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt index 12842034..db37dfc0 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt @@ -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() @@ -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() diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index 28627657..71d9f94c 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -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() } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt index 4fb84ea1..f6583b97 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt @@ -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) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt index 30b6a6d0..fb99e839 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt @@ -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() 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) } } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt index f35c46fc..1ab01369 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt @@ -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() val selectedGroupHolder = viewModelContext.activity.get() return GroupListViewModel(state, selectedGroupHolder, currentSession) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt index 3f24f9e0..0fc0c9d4 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt @@ -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() { +class GroupSummaryController : TypedEpoxyController() { + + var callback: Callback? = null override fun buildModels(viewState: GroupListViewState) { buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 76f91c7b..1e151134 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -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 { parametersOf(roomDetailArgs.roomId) } + private val timelineEventController by inject() private val homePermalinkHandler by inject() + 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 } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index 3ce30c0e..4ca21b94 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -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() val visibleRoomHolder = viewModelContext.activity.get() return RoomDetailViewModel(state, currentSession, visibleRoomHolder) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index ec865e4b..883476de 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -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( @@ -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>) { 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) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt index efc8b664..dc9c393b 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt @@ -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() + } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt index 904cf66c..deeba711 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt @@ -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() private val roomController by inject() - private val homeViewModel: RoomListViewModel by activityViewModel() + private val homeNavigator by inject() + 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)) + } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt index 0612016c..f5213432 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt @@ -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() val roomSelectionRepository = viewModelContext.activity.get() val selectedGroupHolder = viewModelContext.activity.get() val visibleRoomHolder = viewModelContext.activity.get() @@ -61,6 +62,10 @@ class RoomListViewModel(initialState: RoomListViewState, private val roomListFilter = BehaviorRelay.createDefault>(Option.empty()) + private val _openRoomLiveData = MutableLiveData>() + val openRoomLiveData: LiveData> + 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)) + } } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt index 57d0be76..7466618e 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt @@ -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 = 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, - val directRooms: List, - val groupRooms: List, - val lowPriorities: List, - val serverNotices: List -) + 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> + +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() } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt index 8178e2b8..de850df5 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt @@ -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() { - 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, @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) }