From 2b66b4c4b7346ddf2e038328945c36824610aa32 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 11 Jan 2019 12:07:38 +0100 Subject: [PATCH] Add a HomeActivityViewModel, VisibleRoomHandler and Event classes to handle some navigation. Not perfect but ok for now --- .../riotredesign/core/extensions/LiveData.kt | 19 ++++++ .../vector/riotredesign/core/utils/Event.kt | 40 ++++++++++++ .../features/home/HomeActivity.kt | 7 +++ .../features/home/HomeActivityViewModel.kt | 61 +++++++++++++++++++ .../riotredesign/features/home/HomeModule.kt | 6 ++ .../features/home/room/VisibleRoomHolder.kt | 19 ++++++ .../home/room/detail/RoomDetailActions.kt | 1 + .../home/room/detail/RoomDetailFragment.kt | 5 ++ .../home/room/detail/RoomDetailViewModel.kt | 11 +++- .../home/room/list/RoomListFragment.kt | 9 +-- .../home/room/list/RoomListViewModel.kt | 25 +++++--- 11 files changed, 184 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/im/vector/riotredesign/core/extensions/LiveData.kt create mode 100644 app/src/main/java/im/vector/riotredesign/core/utils/Event.kt create mode 100644 app/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt create mode 100644 app/src/main/java/im/vector/riotredesign/features/home/room/VisibleRoomHolder.kt diff --git a/app/src/main/java/im/vector/riotredesign/core/extensions/LiveData.kt b/app/src/main/java/im/vector/riotredesign/core/extensions/LiveData.kt new file mode 100644 index 00000000..bf33acda --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/core/extensions/LiveData.kt @@ -0,0 +1,19 @@ +package im.vector.riotredesign.core.extensions + +import android.arch.lifecycle.LifecycleOwner +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.Observer +import im.vector.riotredesign.core.utils.Event +import im.vector.riotredesign.core.utils.EventObserver + +inline fun LiveData.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) { + this.observe(owner, Observer { observer(it) }) +} + +inline fun LiveData.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) { + this.observe(owner, Observer { it?.run(observer) }) +} + +inline fun LiveData>.observeEvent(owner: LifecycleOwner, crossinline observer: (T) -> Unit) { + this.observe(owner, EventObserver { it.run(observer) }) +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/utils/Event.kt b/app/src/main/java/im/vector/riotredesign/core/utils/Event.kt new file mode 100644 index 00000000..d67ada66 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/core/utils/Event.kt @@ -0,0 +1,40 @@ +package im.vector.riotredesign.core.utils + +import android.arch.lifecycle.Observer + +open class Event(private val content: T) { + + var hasBeenHandled = false + private set // Allow external read but not write + + /** + * Returns the content and prevents its use again. + */ + fun getContentIfNotHandled(): T? { + return if (hasBeenHandled) { + null + } else { + hasBeenHandled = true + content + } + } + + /** + * Returns the content, even if it's already been handled. + */ + fun peekContent(): T = content +} + +/** + * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has + * already been handled. + * + * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled. + */ +class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> { + override fun onChanged(event: Event?) { + event?.getContentIfNotHandled()?.let { value -> + onEventUnhandledContent(value) + } + } +} \ 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 2fd2b653..da6a6731 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 @@ -9,7 +9,9 @@ import android.support.v7.app.ActionBarDrawerToggle import android.support.v7.widget.Toolbar import android.view.Gravity import android.view.MenuItem +import com.airbnb.mvrx.viewModel import im.vector.riotredesign.R +import im.vector.riotredesign.core.extensions.observeEvent import im.vector.riotredesign.core.extensions.replaceFragment import im.vector.riotredesign.core.platform.OnBackPressed import im.vector.riotredesign.core.platform.RiotActivity @@ -22,6 +24,8 @@ import org.koin.standalone.StandAloneContext.loadKoinModules class HomeActivity : RiotActivity(), ToolbarConfigurable { + + private val homeActivityViewModel: HomeActivityViewModel by viewModel() private val homeNavigator by inject() override fun onCreate(savedInstanceState: Bundle?) { @@ -35,6 +39,9 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable { replaceFragment(loadingDetail, R.id.homeDetailFragmentContainer) replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer) } + homeActivityViewModel.openRoomLiveData.observeEvent(this) { + homeNavigator.openRoomDetail(it, null) + } } override fun onDestroy() { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt new file mode 100644 index 00000000..108469da --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt @@ -0,0 +1,61 @@ +package im.vector.riotredesign.features.home + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import android.support.v4.app.FragmentActivity +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +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 +import im.vector.riotredesign.core.utils.Event +import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository +import io.reactivex.rxkotlin.subscribeBy +import org.koin.android.ext.android.get + +class EmptyState : MvRxState + +class HomeActivityViewModel(state: EmptyState, + private val session: Session, + roomSelectionRepository: RoomSelectionRepository +) : RiotViewModel(state) { + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(activity: FragmentActivity, state: EmptyState): HomeActivityViewModel { + val session = Matrix.getInstance().currentSession + val roomSelectionRepository = activity.get() + return HomeActivityViewModel(state, session, roomSelectionRepository) + } + } + + private val _openRoomLiveData = MutableLiveData>() + val openRoomLiveData: LiveData> + get() = _openRoomLiveData + + init { + val lastSelectedRoom = roomSelectionRepository.lastSelectedRoom() + if (lastSelectedRoom == null) { + getTheFirstRoomWhenAvailable() + } else { + _openRoomLiveData.postValue(Event(lastSelectedRoom)) + } + } + + private fun getTheFirstRoomWhenAvailable() { + session.rx().liveRoomSummaries() + .filter { it.isNotEmpty() } + .first(emptyList()) + .subscribeBy { + val firstRoom = it.firstOrNull() + if (firstRoom != null) { + _openRoomLiveData.postValue(Event(firstRoom.roomId)) + } + } + .disposeOnClear() + } + + +} \ No newline at end of file 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 86a414f1..1c30a4ca 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 @@ -1,6 +1,7 @@ package im.vector.riotredesign.features.home import im.vector.riotredesign.features.home.group.SelectedGroupHolder +import im.vector.riotredesign.features.home.room.VisibleRoomHolder import im.vector.riotredesign.features.home.room.detail.timeline.MessageItemFactory import im.vector.riotredesign.features.home.room.detail.timeline.TextItemFactory import im.vector.riotredesign.features.home.room.detail.timeline.TimelineDateFormatter @@ -35,9 +36,14 @@ class HomeModule(private val homeActivity: HomeActivity) { SelectedGroupHolder() } + single { + VisibleRoomHolder() + } + single { HomePermalinkHandler(get()) } + } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/VisibleRoomHolder.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/VisibleRoomHolder.kt new file mode 100644 index 00000000..c2888943 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/VisibleRoomHolder.kt @@ -0,0 +1,19 @@ +package im.vector.riotredesign.features.home.room + +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject + +class VisibleRoomHolder { + + private val visibleRoomStream = BehaviorSubject.create() + + fun setVisibleRoom(roomId: String) { + visibleRoomStream.onNext(roomId) + } + + fun visibleRoom(): Observable { + return visibleRoomStream.hide() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 032caa8a..89175598 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -3,5 +3,6 @@ package im.vector.riotredesign.features.home.room.detail sealed class RoomDetailActions { data class SendMessage(val text: String) : RoomDetailActions() + object IsDisplayed : RoomDetailActions() } \ No newline at end of file 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 a03f21a5..ee8ddbbf 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 @@ -62,6 +62,11 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { roomDetailViewModel.subscribe { renderState(it) } } + override fun onResume() { + super.onResume() + roomDetailViewModel.accept(RoomDetailActions.IsDisplayed) + } + private fun setupToolbar() { val parentActivity = riotActivity if (parentActivity is ToolbarConfigurable) { 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 05a68813..d76b4cf4 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 @@ -8,9 +8,12 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.rx.rx import im.vector.riotredesign.core.platform.RiotViewModel +import im.vector.riotredesign.features.home.room.VisibleRoomHolder +import org.koin.android.ext.android.get class RoomDetailViewModel(initialState: RoomDetailViewState, - session: Session + private val session: Session, + private val visibleRoomHolder: VisibleRoomHolder ) : RiotViewModel(initialState) { private val room = session.getRoom(initialState.roomId)!! @@ -22,7 +25,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, @JvmStatic override fun create(activity: FragmentActivity, state: RoomDetailViewState): RoomDetailViewModel { val currentSession = Matrix.getInstance().currentSession - return RoomDetailViewModel(state, currentSession) + val visibleRoomHolder = activity.get() + return RoomDetailViewModel(state, currentSession, visibleRoomHolder) } } @@ -35,6 +39,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, fun accept(action: RoomDetailActions) { when (action) { is RoomDetailActions.SendMessage -> handleSendMessage(action) + is RoomDetailActions.IsDisplayed -> visibleRoomHolder.setVisibleRoom(roomId) } } @@ -54,7 +59,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, private fun observeTimeline() { room.rx().timeline(eventId) .execute { timelineData -> - copy(asyncTimelineData= timelineData) + copy(asyncTimelineData = timelineData) } } 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 35b4d272..f66ee630 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 @@ -44,15 +44,12 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback { private fun renderState(state: RoomListViewState) { when (state.asyncRooms) { is Incomplete -> renderLoading() - is Success -> renderSuccess(state) - is Fail -> renderFailure(state.asyncRooms.error) + is Success -> renderSuccess(state) + is Fail -> renderFailure(state.asyncRooms.error) } } private fun renderSuccess(state: RoomListViewState) { - if (state.selectedRoomId != null) { - homeNavigator.openRoomDetail(state.selectedRoomId, null) - } if (state.asyncRooms().isNullOrEmpty()) { stateView.state = StateView.State.Empty(getString(R.string.room_list_empty)) } else { @@ -68,7 +65,7 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback { private fun renderFailure(error: Throwable) { val message = when (error) { is Failure.NetworkConnection -> getString(R.string.error_no_network) - else -> getString(R.string.error_common) + else -> getString(R.string.error_common) } stateView.state = StateView.State.Error(message) } 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 198e3fee..40a882f8 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 @@ -10,13 +10,16 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.rx.rx import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.features.home.group.SelectedGroupHolder +import im.vector.riotredesign.features.home.room.VisibleRoomHolder import io.reactivex.Observable import io.reactivex.functions.BiFunction +import io.reactivex.rxkotlin.subscribeBy import org.koin.android.ext.android.get class RoomListViewModel(initialState: RoomListViewState, private val session: Session, private val selectedGroupHolder: SelectedGroupHolder, + private val visibleRoomHolder: VisibleRoomHolder, private val roomSelectionRepository: RoomSelectionRepository) : RiotViewModel(initialState) { @@ -27,12 +30,14 @@ class RoomListViewModel(initialState: RoomListViewState, val currentSession = Matrix.getInstance().currentSession val roomSelectionRepository = activity.get() val selectedGroupHolder = activity.get() - return RoomListViewModel(state, currentSession, selectedGroupHolder, roomSelectionRepository) + val visibleRoomHolder = activity.get() + return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository) } } init { observeRoomSummaries() + observeVisibleRoom() } fun accept(action: RoomListActions) { @@ -46,10 +51,17 @@ class RoomListViewModel(initialState: RoomListViewState, private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state -> if (state.selectedRoomId != action.roomSummary.roomId) { roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId) - setState { copy(selectedRoomId = action.roomSummary.roomId) } } } + private fun observeVisibleRoom() { + visibleRoomHolder.visibleRoom() + .subscribeBy { + setState { copy(selectedRoomId = it) } + } + .disposeOnClear() + } + private fun observeRoomSummaries() { Observable.combineLatest, Option, RoomSummaries>( session.rx().liveRoomSummaries(), @@ -78,15 +90,8 @@ class RoomListViewModel(initialState: RoomListViewState, } ) .execute { async -> - val summaries = async() - val selectedRoomId = selectedRoomId - ?: roomSelectionRepository.lastSelectedRoom() - ?: summaries?.directRooms?.firstOrNull()?.roomId - ?: summaries?.groupRooms?.firstOrNull()?.roomId - copy( - asyncRooms = async, - selectedRoomId = selectedRoomId + asyncRooms = async ) } }