Use MvRx in room detail

This commit is contained in:
ganfra 2018-12-29 17:54:03 +01:00 committed by ganfra
parent 5a75e3db81
commit 7f11c141c7
12 changed files with 191 additions and 64 deletions

View File

@ -7,6 +7,10 @@ kapt {
correctErrorTypes = true correctErrorTypes = true
} }


androidExtensions {
experimental = true
}

android { android {
compileSdkVersion 28 compileSdkVersion 28
defaultConfig { defaultConfig {
@ -64,7 +68,6 @@ dependencies {


implementation 'com.github.bumptech.glide:glide:4.8.0' implementation 'com.github.bumptech.glide:glide:4.8.0'
kapt 'com.github.bumptech.glide:compiler:4.8.0' kapt 'com.github.bumptech.glide:compiler:4.8.0'
//todo remove that
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'





View File

@ -1,6 +1,9 @@
package im.vector.riotredesign.core.platform package im.vector.riotredesign.core.platform


import android.os.Bundle
import android.os.Parcelable
import com.airbnb.mvrx.BaseMvRxFragment import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.MvRx


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


@ -16,5 +19,8 @@ abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
//no-ops by default //no-ops by default
} }


protected fun setArguments(args: Parcelable? = null) {
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
}


} }

View File

@ -17,6 +17,7 @@ import im.vector.riotredesign.core.platform.OnBackPressed
import im.vector.riotredesign.core.platform.RiotActivity import im.vector.riotredesign.core.platform.RiotActivity
import im.vector.riotredesign.core.platform.ToolbarConfigurable 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 im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
import im.vector.riotredesign.features.home.room.detail.RoomDetailFragment import im.vector.riotredesign.features.home.room.detail.RoomDetailFragment
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import org.koin.standalone.StandAloneContext.loadKoinModules import org.koin.standalone.StandAloneContext.loadKoinModules
@ -88,7 +89,8 @@ class HomeActivity : RiotActivity(), HomeNavigator, ToolbarConfigurable {
// HomeNavigator ******************************************************************************* // HomeNavigator *******************************************************************************


override fun openRoomDetail(roomId: String, eventId: String?) { override fun openRoomDetail(roomId: String, eventId: String?) {
val roomDetailFragment = RoomDetailFragment.newInstance(roomId) val args = RoomDetailArgs(roomId, eventId)
val roomDetailFragment = RoomDetailFragment.newInstance(args)
if (drawerLayout.isDrawerOpen(Gravity.LEFT)) { if (drawerLayout.isDrawerOpen(Gravity.LEFT)) {
closeDrawerLayout(Gravity.LEFT) { replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer) } closeDrawerLayout(Gravity.LEFT) { replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer) }
} else { } else {

View File

@ -40,44 +40,38 @@ class HomeViewModel(initialState: HomeViewState,


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


private fun handlePermalinkClicked(action: HomeActions.PermalinkClicked) { private fun handlePermalinkClicked(action: HomeActions.PermalinkClicked) = withState { state ->
withState { state -> when (action.permalinkData) {
when (action.permalinkData) { is PermalinkData.EventLink -> {
is PermalinkData.EventLink -> {


} }
is PermalinkData.RoomLink -> { is PermalinkData.RoomLink -> {


} }
is PermalinkData.GroupLink -> { is PermalinkData.GroupLink -> {


} }
is PermalinkData.UserLink -> { is PermalinkData.UserLink -> {


} }
is PermalinkData.FallbackLink -> { is PermalinkData.FallbackLink -> {


}
} }
} }
} }


private fun handleSelectRoom(action: HomeActions.SelectRoom) { private fun handleSelectRoom(action: HomeActions.SelectRoom) = withState { state ->
withState { state -> if (state.selectedRoomId != action.roomSummary.roomId) {
if (state.selectedRoomId != action.roomSummary.roomId) { roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId)
roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId) setState { copy(selectedRoomId = action.roomSummary.roomId, shouldOpenRoomDetail = true) }
setState { copy(selectedRoomId = action.roomSummary.roomId, shouldOpenRoomDetail = true) }
}
} }
} }


private fun handleSelectGroup(action: HomeActions.SelectGroup) { private fun handleSelectGroup(action: HomeActions.SelectGroup) = withState { state ->
withState { state -> if (state.selectedGroup?.groupId != action.groupSummary.groupId) {
if (state.selectedGroup?.groupId != action.groupSummary.groupId) { setState { copy(selectedGroup = action.groupSummary) }
setState { copy(selectedGroup = action.groupSummary) } } else {
} else { setState { copy(selectedGroup = null) }
setState { copy(selectedGroup = null) }
}
} }
} }



View File

@ -0,0 +1,7 @@
package im.vector.riotredesign.features.home.room.detail

sealed class RoomDetailActions {

data class SendMessage(val text: String) : RoomDetailActions()

}

View File

@ -1,51 +1,51 @@
package im.vector.riotredesign.features.home.room.detail package im.vector.riotredesign.features.home.room.detail


import android.arch.lifecycle.Observer
import android.arch.paging.PagedList
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
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 com.airbnb.mvrx.Success
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import im.vector.matrix.android.api.Matrix import com.airbnb.mvrx.args
import im.vector.matrix.android.api.MatrixCallback import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.permalinks.PermalinkParser import im.vector.matrix.android.api.permalinks.PermalinkParser
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.Room
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.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.core.utils.FragmentArgumentDelegate
import im.vector.riotredesign.core.utils.UnsafeFragmentArgumentDelegate
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.HomeActions import im.vector.riotredesign.features.home.HomeActions
import im.vector.riotredesign.features.home.HomeViewModel import im.vector.riotredesign.features.home.HomeViewModel
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.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.core.parameter.parametersOf


@Parcelize
data class RoomDetailArgs(
val roomId: String,
val eventId: String? = null
) : Parcelable

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


companion object { companion object {


fun newInstance(roomId: String, eventId: String? = null): RoomDetailFragment { fun newInstance(args: RoomDetailArgs): RoomDetailFragment {
return RoomDetailFragment().apply { return RoomDetailFragment().apply {
this.roomId = roomId setArguments(args)
this.eventId = eventId
} }
} }
} }


private val viewModel: HomeViewModel by activityViewModel() private val homeViewModel: HomeViewModel by activityViewModel()
private val currentSession = Matrix.getInstance().currentSession private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
private var roomId: String by UnsafeFragmentArgumentDelegate() private val roomDetailArgs: RoomDetailArgs by args()
private var eventId: String? by FragmentArgumentDelegate()
private val timelineEventController by inject<TimelineEventController> { parametersOf(roomId) } private val timelineEventController by inject<TimelineEventController> { parametersOf(roomDetailArgs.roomId) }
private lateinit var room: Room
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? {
@ -54,21 +54,16 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {


override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
room = currentSession.getRoom(roomId)!!
setupRecyclerView() setupRecyclerView()
setupToolbar() setupToolbar()
room.loadRoomMembersIfNeeded()
room.timeline(eventId).observe(this, Observer { renderEvents(it) })
room.roomSummary.observe(this, Observer { renderRoomSummary(it) })
sendButton.setOnClickListener { sendButton.setOnClickListener {
val textMessage = composerEditText.text.toString() val textMessage = composerEditText.text.toString()
if (textMessage.isNotBlank()) { if (textMessage.isNotBlank()) {
composerEditText.text = null composerEditText.text = null
room.sendTextMessage(textMessage, object : MatrixCallback<Event> { roomDetailViewModel.accept(RoomDetailActions.SendMessage(textMessage))

})
} }
} }
roomDetailViewModel.subscribe { renderState(it) }
} }


private fun setupToolbar() { private fun setupToolbar() {
@ -87,6 +82,15 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
timelineEventController.callback = this timelineEventController.callback = this
} }


private fun renderState(state: RoomDetailViewState) {
when (state.asyncTimeline) {
is Success -> renderTimeline(state.asyncTimeline())
}
when (state.asyncRoomSummary) {
is Success -> renderRoomSummary(state.asyncRoomSummary())
}
}

private fun renderRoomSummary(roomSummary: RoomSummary?) { private fun renderRoomSummary(roomSummary: RoomSummary?) {
roomSummary?.let { roomSummary?.let {
toolbarTitleView.text = it.displayName toolbarTitleView.text = it.displayName
@ -100,16 +104,16 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
} }
} }


private fun renderEvents(events: PagedList<EnrichedEvent>?) { private fun renderTimeline(timeline: Timeline?) {
scrollOnNewMessageCallback.hasBeenUpdated.set(true) scrollOnNewMessageCallback.hasBeenUpdated.set(true)
timelineEventController.timeline = events timelineEventController.timeline = timeline
} }


// TimelineEventController.Callback ************************************************************ // TimelineEventController.Callback ************************************************************


override fun onUrlClicked(url: String) { override fun onUrlClicked(url: String) {
val permalinkData = PermalinkParser.parse(url) val permalinkData = PermalinkParser.parse(url)
viewModel.accept(HomeActions.PermalinkClicked(permalinkData)) homeViewModel.accept(HomeActions.PermalinkClicked(permalinkData))
} }


} }

View File

@ -0,0 +1,62 @@
package im.vector.riotredesign.features.home.room.detail

import android.support.v4.app.FragmentActivity
import com.airbnb.mvrx.BaseMvRxViewModel
import com.airbnb.mvrx.MvRxViewModelFactory
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
import im.vector.matrix.rx.rx

class RoomDetailViewModel(initialState: RoomDetailViewState,
session: Session
) : BaseMvRxViewModel<RoomDetailViewState>(initialState) {

private val room = session.getRoom(initialState.roomId)!!
private val roomId = initialState.roomId
private val eventId = initialState.eventId

companion object : MvRxViewModelFactory<RoomDetailViewState> {

@JvmStatic
override fun create(activity: FragmentActivity, state: RoomDetailViewState): RoomDetailViewModel {
val currentSession = Matrix.getInstance().currentSession
return RoomDetailViewModel(state, currentSession)
}
}

init {
observeRoomSummary()
observeTimeline()
room.loadRoomMembersIfNeeded()
}

fun accept(action: RoomDetailActions) {
when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action)
}
}

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

private fun handleSendMessage(action: RoomDetailActions.SendMessage) {
room.sendTextMessage(action.text, callback = object : MatrixCallback<Event> {})
}

private fun observeRoomSummary() {
room.rx().liveRoomSummary()
.execute { async ->
copy(asyncRoomSummary = async)
}
}

private fun observeTimeline() {
room.rx().timeline(eventId)
.execute { async ->
copy(asyncTimeline = async)
}
}


}

View File

@ -0,0 +1,21 @@
package im.vector.riotredesign.features.home.room.detail

import android.arch.paging.PagedList
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.room.model.RoomSummary

typealias Timeline = PagedList<EnrichedEvent>

data class RoomDetailViewState(
val roomId: String,
val eventId: String?,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val asyncTimeline: Async<Timeline> = Uninitialized
) : MvRxState {

constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)

}

View File

@ -7,6 +7,7 @@ import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.LoadingItemModel_ import im.vector.riotredesign.features.home.LoadingItemModel_
import im.vector.riotredesign.features.home.room.detail.Timeline


class TimelineEventController(private val roomId: String, class TimelineEventController(private val roomId: String,
private val messageItemFactory: MessageItemFactory, private val messageItemFactory: MessageItemFactory,
@ -36,7 +37,7 @@ class TimelineEventController(private val roomId: String,
} }


private var snapshotList: List<EnrichedEvent>? = emptyList() private var snapshotList: List<EnrichedEvent>? = emptyList()
var timeline: PagedList<EnrichedEvent>? = null var timeline: Timeline? = null
set(value) { set(value) {
field?.removeWeakCallback(pagedListCallback) field?.removeWeakCallback(pagedListCallback)
field = value field = value

View File

@ -29,7 +29,7 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
} }


private val homeNavigator by inject<HomeNavigator>() private val homeNavigator by inject<HomeNavigator>()
private val viewModel: HomeViewModel by activityViewModel() private val homeViewModel: HomeViewModel by activityViewModel()
private lateinit var roomController: RoomSummaryController private lateinit var roomController: RoomSummaryController


override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -41,18 +41,18 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
roomController = RoomSummaryController(this) roomController = RoomSummaryController(this)
stateView.contentView = epoxyRecyclerView stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(roomController) epoxyRecyclerView.setController(roomController)
viewModel.subscribe { renderState(it) } homeViewModel.subscribe { renderState(it) }
} }


private fun renderState(state: HomeViewState) { private fun renderState(state: HomeViewState) {
when (state.asyncRooms) { when (state.asyncRooms) {
is Incomplete -> renderLoading() is Incomplete -> renderLoading()
is Success -> renderSuccess(state) is Success -> renderSuccess(state)
is Fail -> renderFailure(state.asyncRooms.error) is Fail -> renderFailure(state.asyncRooms.error)
} }
if (state.shouldOpenRoomDetail && state.selectedRoomId != null) { if (state.shouldOpenRoomDetail && state.selectedRoomId != null) {
homeNavigator.openRoomDetail(state.selectedRoomId, null) homeNavigator.openRoomDetail(state.selectedRoomId, null)
viewModel.accept(HomeActions.RoomDisplayed) homeViewModel.accept(HomeActions.RoomDisplayed)
} }
} }


@ -72,13 +72,13 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
private fun renderFailure(error: Throwable) { private fun renderFailure(error: Throwable) {
val message = when (error) { val message = when (error) {
is Failure.NetworkConnection -> getString(R.string.error_no_network) 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) stateView.state = StateView.State.Error(message)
} }


override fun onRoomSelected(room: RoomSummary) { override fun onRoomSelected(room: RoomSummary) {
viewModel.accept(HomeActions.SelectRoom(room)) homeViewModel.accept(HomeActions.SelectRoom(room))
} }


} }

View File

@ -39,6 +39,9 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'


// Paging
api "android.arch.paging:runtime:1.0.1"



testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:runner:1.0.2'

View File

@ -0,0 +1,24 @@
package im.vector.matrix.rx

import android.arch.paging.PagedList
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.model.RoomSummary
import io.reactivex.Observable

class RxRoom(private val room: Room) {

fun liveRoomSummary(): Observable<RoomSummary> {
return room.roomSummary.asObservable()
}

fun timeline(eventId: String? = null): Observable<PagedList<EnrichedEvent>> {
return room.timeline(eventId).asObservable()
}

}

fun Room.rx(): RxRoom {
return RxRoom(this)
}