Merge branch 'feature/navigation' into develop

This commit is contained in:
ganfra 2019-01-14 16:46:54 +01:00
commit 5e89627867
97 changed files with 1723 additions and 834 deletions

View File

@ -7,6 +7,10 @@ kapt {
correctErrorTypes = true correctErrorTypes = true
} }
androidExtensions {
experimental = true
}
android { android {
compileSdkVersion 28 compileSdkVersion 28
defaultConfig { defaultConfig {
@ -43,7 +47,7 @@ configurations.all { strategy ->
dependencies { dependencies {
def epoxy_version = "2.19.0" def epoxy_version = "2.19.0"
def arrow_version = "0.8.0"
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android")
@ -52,25 +56,34 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation 'com.android.support.constraint:constraint-layout:1.1.3'
// Paging
implementation "android.arch.paging:runtime:1.0.1"
implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1' implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1'
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'
// rx
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'com.jakewharton.rxrelay2:rxrelay:2.1.0'
implementation("com.airbnb.android:epoxy:$epoxy_version") implementation("com.airbnb.android:epoxy:$epoxy_version")
kapt "com.airbnb.android:epoxy-processor:$epoxy_version" kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
implementation "com.airbnb.android:epoxy-paging:$epoxy_version"
implementation 'com.airbnb.android:mvrx:0.6.0' implementation 'com.airbnb.android:mvrx:0.6.0'
// FP
implementation "io.arrow-kt:arrow-core:$arrow_version"
// UI
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'
// DI
implementation "org.koin:koin-android:$koin_version" implementation "org.koin:koin-android:$koin_version"
implementation "org.koin:koin-android-scope:$koin_version" implementation "org.koin:koin-android-scope:$koin_version"
// TESTS
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'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

View File

@ -1,7 +1,9 @@
package im.vector.riotredesign.core.di package im.vector.riotredesign.core.di
import android.content.Context import android.content.Context
import android.content.Context.MODE_PRIVATE
import im.vector.riotredesign.core.resources.LocaleProvider import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
import org.koin.dsl.module.module import org.koin.dsl.module.module
class AppModule(private val context: Context) { class AppModule(private val context: Context) {
@ -12,5 +14,13 @@ class AppModule(private val context: Context) {
LocaleProvider(context.resources) LocaleProvider(context.resources)
} }
single {
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
}
single {
RoomSelectionRepository(get())
}
} }
} }

View File

@ -10,6 +10,10 @@ fun Fragment.replaceFragment(fragment: Fragment, frameId: Int) {
fragmentManager?.inTransaction { replace(frameId, fragment) } fragmentManager?.inTransaction { replace(frameId, fragment) }
} }
fun Fragment.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
fragmentManager?.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
}
fun Fragment.addChildFragment(fragment: Fragment, frameId: Int) { fun Fragment.addChildFragment(fragment: Fragment, frameId: Int) {
childFragmentManager.inTransaction { add(frameId, fragment) } childFragmentManager.inTransaction { add(frameId, fragment) }
} }
@ -18,10 +22,6 @@ fun Fragment.replaceChildFragment(fragment: Fragment, frameId: Int) {
childFragmentManager.inTransaction { replace(frameId, fragment) } childFragmentManager.inTransaction { replace(frameId, fragment) }
} }
fun Fragment.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
fragmentManager?.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
}
fun Fragment.addChildFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) { fun Fragment.addChildFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
childFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) } childFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
} }

View File

@ -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.LiveEvent
import im.vector.riotredesign.core.utils.EventObserver
inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) {
this.observe(owner, Observer { observer(it) })
}
inline fun <T> LiveData<T>.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) {
this.observe(owner, Observer { it?.run(observer) })
}
inline fun <T> LiveData<LiveEvent<T>>.observeEvent(owner: LifecycleOwner, crossinline observer: (T) -> Unit) {
this.observe(owner, EventObserver { it.run(observer) })
}

View File

@ -0,0 +1,10 @@
package im.vector.riotredesign.core.platform
interface OnBackPressed {
/**
* Returns true, if the on back pressed event has been handled by this Fragment.
* Otherwise return false
*/
fun onBackPressed(): Boolean
}

View File

@ -1,17 +1,26 @@
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() { abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
val riotActivity: RiotActivity by lazy { val riotActivity: RiotActivity by lazy {
activity as RiotActivity activity as RiotActivity
} }
override fun onBackPressed(): Boolean {
return false
}
override fun invalidate() { override fun invalidate() {
//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

@ -0,0 +1,7 @@
package im.vector.riotredesign.core.platform
import com.airbnb.mvrx.BaseMvRxViewModel
import com.airbnb.mvrx.MvRxState
abstract class RiotViewModel<S : MvRxState>(initialState: S)
: BaseMvRxViewModel<S>(initialState, debugMode = false)

View File

@ -1,6 +0,0 @@
package im.vector.riotredesign.core.utils
object Constants {
}

View File

@ -1,63 +0,0 @@
package im.vector.riotredesign.core.utils
import android.os.Binder
import android.os.Bundle
import android.support.v4.app.BundleCompat
import android.support.v4.app.Fragment
import kotlin.reflect.KProperty
class FragmentArgumentDelegate<T : Any> : kotlin.properties.ReadWriteProperty<Fragment, T?> {
var value: T? = null
override operator fun getValue(thisRef: android.support.v4.app.Fragment, property: kotlin.reflect.KProperty<*>): T? {
if (value == null) {
val args = thisRef.arguments
@Suppress("UNCHECKED_CAST")
value = args?.get(property.name) as T?
}
return value
}
override operator fun setValue(thisRef: Fragment, property: KProperty<*>, value: T?) {
if (value == null) return
if (thisRef.arguments == null) {
thisRef.arguments = Bundle()
}
val args = thisRef.arguments!!
val key = property.name
when (value) {
is String -> args.putString(key, value)
is Int -> args.putInt(key, value)
is Short -> args.putShort(key, value)
is Long -> args.putLong(key, value)
is Byte -> args.putByte(key, value)
is ByteArray -> args.putByteArray(key, value)
is Char -> args.putChar(key, value)
is CharArray -> args.putCharArray(key, value)
is CharSequence -> args.putCharSequence(key, value)
is Float -> args.putFloat(key, value)
is Bundle -> args.putBundle(key, value)
is Binder -> BundleCompat.putBinder(args, key, value)
is android.os.Parcelable -> args.putParcelable(key, value)
is java.io.Serializable -> args.putSerializable(key, value)
else -> throw IllegalStateException("Type ${value.javaClass.name} of property ${property.name} is not supported")
}
}
}
class UnsafeFragmentArgumentDelegate<T : Any> : kotlin.properties.ReadWriteProperty<Fragment, T> {
private val innerDelegate = FragmentArgumentDelegate<T>()
override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
innerDelegate.setValue(thisRef, property, value)
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
return innerDelegate.getValue(thisRef, property)!!
}
}

View File

@ -0,0 +1,40 @@
package im.vector.riotredesign.core.utils
import android.arch.lifecycle.Observer
open class LiveEvent<out T>(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 [LiveEvent]s, simplifying the pattern of checking if the [LiveEvent]'s content has
* already been handled.
*
* [onEventUnhandledContent] is *only* called if the [LiveEvent]'s contents has not been handled.
*/
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<LiveEvent<T>> {
override fun onChanged(event: LiveEvent<T>?) {
event?.getContentIfNotHandled()?.let { value ->
onEventUnhandledContent(value)
}
}
}

View File

@ -5,14 +5,12 @@ import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.core.platform.RiotActivity import im.vector.riotredesign.core.platform.RiotActivity
import im.vector.riotredesign.features.home.HomeActivity import im.vector.riotredesign.features.home.HomeActivity
import im.vector.riotredesign.features.login.LoginActivity import im.vector.riotredesign.features.login.LoginActivity
import org.koin.android.ext.android.inject
class MainActivity : RiotActivity() { class MainActivity : RiotActivity() {
private val authenticator = Matrix.getInstance().authenticator() private val authenticator = Matrix.getInstance().authenticator()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val intent = if (authenticator.hasActiveSessions()) { val intent = if (authenticator.hasActiveSessions()) {

View File

@ -1,14 +0,0 @@
package im.vector.riotredesign.features.home
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
sealed class HomeActions {
data class SelectRoom(val roomSummary: RoomSummary) : HomeActions()
data class SelectGroup(val groupSummary: GroupSummary) : HomeActions()
object RoomDisplayed : HomeActions()
}

View File

@ -3,28 +3,34 @@ package im.vector.riotredesign.features.home
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.support.v4.app.FragmentManager
import android.support.v4.view.GravityCompat import android.support.v4.view.GravityCompat
import android.support.v4.widget.DrawerLayout
import android.support.v7.app.ActionBarDrawerToggle import android.support.v7.app.ActionBarDrawerToggle
import android.support.v7.widget.Toolbar import android.support.v7.widget.Toolbar
import android.view.Gravity import android.view.Gravity
import android.view.MenuItem import android.view.MenuItem
import android.view.View import com.airbnb.mvrx.viewModel
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.extensions.replaceFragment import im.vector.riotredesign.core.extensions.replaceFragment
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.RoomDetailFragment
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import org.koin.android.ext.android.inject
import org.koin.standalone.StandAloneContext.loadKoinModules import org.koin.standalone.StandAloneContext.loadKoinModules
class HomeActivity : RiotActivity(), HomeNavigator, ToolbarConfigurable { class HomeActivity : RiotActivity(), ToolbarConfigurable {
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
private val homeNavigator by inject<HomeNavigator>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
loadKoinModules(listOf(HomeModule(this).definition)) loadKoinModules(listOf(HomeModule(this).definition))
homeNavigator.activity = this
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home) setContentView(R.layout.activity_home)
if (savedInstanceState == null) { if (savedInstanceState == null) {
@ -33,6 +39,14 @@ class HomeActivity : RiotActivity(), HomeNavigator, ToolbarConfigurable {
replaceFragment(loadingDetail, R.id.homeDetailFragmentContainer) replaceFragment(loadingDetail, R.id.homeDetailFragmentContainer)
replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer) replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer)
} }
homeActivityViewModel.openRoomLiveData.observeEvent(this) {
homeNavigator.openRoomDetail(it, null)
}
}
override fun onDestroy() {
homeNavigator.activity = null
super.onDestroy()
} }
override fun configure(toolbar: Toolbar) { override fun configure(toolbar: Toolbar) {
@ -46,7 +60,6 @@ class HomeActivity : RiotActivity(), HomeNavigator, ToolbarConfigurable {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
// Android home
android.R.id.home -> { android.R.id.home -> {
drawerLayout.openDrawer(GravityCompat.START) drawerLayout.openDrawer(GravityCompat.START)
return true return true
@ -60,29 +73,31 @@ class HomeActivity : RiotActivity(), HomeNavigator, ToolbarConfigurable {
if (drawerLayout.isDrawerOpen(Gravity.LEFT)) { if (drawerLayout.isDrawerOpen(Gravity.LEFT)) {
drawerLayout.closeDrawer(Gravity.LEFT) drawerLayout.closeDrawer(Gravity.LEFT)
} else { } else {
super.onBackPressed() val handled = recursivelyDispatchOnBackPressed(supportFragmentManager)
} if (!handled) {
} super.onBackPressed()
override fun openRoomDetail(roomId: String) {
val roomDetailFragment = RoomDetailFragment.newInstance(roomId)
if (drawerLayout.isDrawerOpen(Gravity.LEFT)) {
closeDrawerLayout(Gravity.LEFT) { replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer) }
} else {
replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer)
}
}
private fun closeDrawerLayout(gravity: Int, actionOnClose: () -> Unit) {
drawerLayout.addDrawerListener(object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerClosed(p0: View) {
drawerLayout.removeDrawerListener(this)
actionOnClose()
} }
}) }
drawerLayout.closeDrawer(gravity)
} }
private fun recursivelyDispatchOnBackPressed(fm: FragmentManager): Boolean {
if (fm.backStackEntryCount == 0)
return false
val reverseOrder = fm.fragments.filter { it is OnBackPressed }.reversed()
for (f in reverseOrder) {
val handledByChildFragments = recursivelyDispatchOnBackPressed(f.childFragmentManager)
if (handledByChildFragments) {
return true
}
val backPressable = f as OnBackPressed
if (backPressable.onBackPressed()) {
return true
}
}
return false
}
companion object { companion object {
fun newIntent(context: Context): Intent { fun newIntent(context: Context): Intent {
return Intent(context, HomeActivity::class.java) return Intent(context, HomeActivity::class.java)

View File

@ -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.LiveEvent
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<EmptyState>(state) {
companion object : MvRxViewModelFactory<EmptyState> {
@JvmStatic
override fun create(activity: FragmentActivity, state: EmptyState): HomeActivityViewModel {
val session = Matrix.getInstance().currentSession
val roomSelectionRepository = activity.get<RoomSelectionRepository>()
return HomeActivityViewModel(state, session, roomSelectionRepository)
}
}
private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
val openRoomLiveData: LiveData<LiveEvent<String>>
get() = _openRoomLiveData
init {
val lastSelectedRoom = roomSelectionRepository.lastSelectedRoom()
if (lastSelectedRoom == null) {
getTheFirstRoomWhenAvailable()
} else {
_openRoomLiveData.postValue(LiveEvent(lastSelectedRoom))
}
}
private fun getTheFirstRoomWhenAvailable() {
session.rx().liveRoomSummaries()
.filter { it.isNotEmpty() }
.first(emptyList())
.subscribeBy {
val firstRoom = it.firstOrNull()
if (firstRoom != null) {
_openRoomLiveData.postValue(LiveEvent(firstRoom.roomId))
}
}
.disposeOnClear()
}
}

View File

@ -5,7 +5,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.replaceFragment import im.vector.riotredesign.core.extensions.replaceChildFragment
import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.features.home.group.GroupListFragment import im.vector.riotredesign.features.home.group.GroupListFragment
import im.vector.riotredesign.features.home.room.list.RoomListFragment import im.vector.riotredesign.features.home.room.list.RoomListFragment
@ -27,9 +27,9 @@ class HomeDrawerFragment : RiotFragment() {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
if (savedInstanceState == null) { if (savedInstanceState == null) {
val groupListFragment = GroupListFragment.newInstance() val groupListFragment = GroupListFragment.newInstance()
replaceFragment(groupListFragment, R.id.groupListFragmentContainer) replaceChildFragment(groupListFragment, R.id.groupListFragmentContainer)
val roomListFragment = RoomListFragment.newInstance() val roomListFragment = RoomListFragment.newInstance()
replaceFragment(roomListFragment, R.id.roomListFragmentContainer) replaceChildFragment(roomListFragment, R.id.roomListFragmentContainer)
} }
} }

View File

@ -1,5 +1,7 @@
package im.vector.riotredesign.features.home 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.MessageItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.TextItemFactory import im.vector.riotredesign.features.home.room.detail.timeline.TextItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineDateFormatter import im.vector.riotredesign.features.home.room.detail.timeline.TimelineDateFormatter
@ -22,13 +24,26 @@ class HomeModule(private val homeActivity: HomeActivity) {
TextItemFactory() TextItemFactory()
} }
factory { single {
homeActivity as HomeNavigator HomeNavigator()
} }
factory { (roomId: String) -> factory { (roomId: String) ->
TimelineEventController(roomId, get(), get(), get()) TimelineEventController(roomId, get(), get(), get())
} }
single {
SelectedGroupHolder()
}
single {
VisibleRoomHolder()
}
single {
HomePermalinkHandler(get())
}
} }
} }

View File

@ -1,7 +1,59 @@
package im.vector.riotredesign.features.home package im.vector.riotredesign.features.home
interface HomeNavigator { import android.support.v4.app.FragmentManager
import android.view.Gravity
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.addFragmentToBackstack
import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.features.home.room.detail.RoomDetailArgs
import im.vector.riotredesign.features.home.room.detail.RoomDetailFragment
import kotlinx.android.synthetic.main.activity_home.*
import timber.log.Timber
fun openRoomDetail(roomId: String) class HomeNavigator {
var activity: HomeActivity? = null
private var currentRoomId: String? = null
fun openRoomDetail(roomId: String,
eventId: String?,
addToBackstack: Boolean = false) {
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
if (!addToBackstack && isRoomOpened(roomId)) {
return
}
activity?.let {
val args = RoomDetailArgs(roomId, eventId)
val roomDetailFragment = RoomDetailFragment.newInstance(args)
it.drawerLayout?.closeDrawer(Gravity.LEFT)
if (addToBackstack) {
it.addFragmentToBackstack(roomDetailFragment, R.id.homeDetailFragmentContainer, roomId)
} else {
currentRoomId = roomId
clearBackStack(it.supportFragmentManager)
it.replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer)
}
}
}
fun openGroupDetail(groupId: String) {
Timber.v("Open group detail $groupId")
}
fun openUserDetail(userId: String) {
Timber.v("Open user detail $userId")
}
fun isRoomOpened(roomId: String): Boolean {
return currentRoomId == roomId
}
private fun clearBackStack(fragmentManager: FragmentManager) {
if (fragmentManager.backStackEntryCount > 0) {
val first = fragmentManager.getBackStackEntryAt(0)
fragmentManager.popBackStack(first.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
}
} }

View File

@ -0,0 +1,38 @@
package im.vector.riotredesign.features.home
import android.net.Uri
import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser
class HomePermalinkHandler(private val navigator: HomeNavigator) {
fun launch(deepLink: String?) {
val uri = deepLink?.let { Uri.parse(it) }
launch(uri)
}
fun launch(deepLink: Uri?) {
if (deepLink == null) {
return
}
val permalinkData = PermalinkParser.parse(deepLink)
when (permalinkData) {
is PermalinkData.EventLink -> {
navigator.openRoomDetail(permalinkData.roomIdOrAlias, permalinkData.eventId, true)
}
is PermalinkData.RoomLink -> {
navigator.openRoomDetail(permalinkData.roomIdOrAlias, null, true)
}
is PermalinkData.GroupLink -> {
navigator.openGroupDetail(permalinkData.groupId)
}
is PermalinkData.UserLink -> {
navigator.openUserDetail(permalinkData.userId)
}
is PermalinkData.FallbackLink -> {
}
}
}
}

View File

@ -1,85 +0,0 @@
package im.vector.riotredesign.features.home
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.session.Session
import im.vector.matrix.rx.rx
class HomeViewModel(initialState: HomeViewState, private val session: Session) : BaseMvRxViewModel<HomeViewState>(initialState) {
companion object : MvRxViewModelFactory<HomeViewState> {
@JvmStatic
override fun create(activity: FragmentActivity, state: HomeViewState): HomeViewModel {
val currentSession = Matrix.getInstance().currentSession
return HomeViewModel(state, currentSession)
}
}
init {
observeRoomSummaries()
observeGroupSummaries()
}
fun accept(action: HomeActions) {
when (action) {
is HomeActions.SelectRoom -> handleSelectRoom(action)
is HomeActions.SelectGroup -> handleSelectGroup(action)
is HomeActions.RoomDisplayed -> setState { copy(shouldOpenRoomDetail = false) }
}
}
// PRIVATE METHODS *****************************************************************************
private fun handleSelectRoom(action: HomeActions.SelectRoom) {
withState { state ->
if (state.selectedRoom?.roomId != action.roomSummary.roomId) {
session.saveLastSelectedRoom(action.roomSummary)
setState { copy(selectedRoom = action.roomSummary, shouldOpenRoomDetail = true) }
}
}
}
private fun handleSelectGroup(action: HomeActions.SelectGroup) {
withState { state ->
if (state.selectedGroup?.groupId != action.groupSummary.groupId) {
setState { copy(selectedGroup = action.groupSummary) }
} else {
setState { copy(selectedGroup = null) }
}
}
}
private fun observeRoomSummaries() {
session
.rx().liveRoomSummaries()
.execute { async ->
val summaries = async()
val directRooms = summaries?.filter { it.isDirect } ?: emptyList()
val groupRooms = summaries?.filter { !it.isDirect } ?: emptyList()
val selectedRoom = selectedRoom
?: session.lastSelectedRoom()
?: directRooms.firstOrNull()
?: groupRooms.firstOrNull()
copy(
asyncRooms = async,
directRooms = directRooms,
groupRooms = groupRooms,
selectedRoom = selectedRoom
)
}
}
private fun observeGroupSummaries() {
session
.rx().liveGroupSummaries()
.execute { async ->
copy(asyncGroups = async)
}
}
}

View File

@ -1,17 +0,0 @@
package im.vector.riotredesign.features.home
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
data class HomeViewState(
val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val directRooms: List<RoomSummary> = emptyList(),
val groupRooms: List<RoomSummary> = emptyList(),
val selectedRoom: RoomSummary? = null,
val shouldOpenRoomDetail: Boolean = true,
val asyncGroups: Async<List<GroupSummary>> = Uninitialized,
val selectedGroup: GroupSummary? = null
) : MvRxState

View File

@ -0,0 +1,9 @@
package im.vector.riotredesign.features.home.group
import im.vector.matrix.android.api.session.group.model.GroupSummary
sealed class GroupListActions {
data class SelectGroup(val groupSummary: GroupSummary) : GroupListActions()
}

View File

@ -6,14 +6,11 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
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.session.group.model.GroupSummary 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.HomeActions
import im.vector.riotredesign.features.home.HomeViewModel
import im.vector.riotredesign.features.home.HomeViewState
import kotlinx.android.synthetic.main.fragment_group_list.* import kotlinx.android.synthetic.main.fragment_group_list.*
class GroupListFragment : RiotFragment(), GroupSummaryController.Callback { class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
@ -24,7 +21,7 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
} }
} }
private val viewModel: HomeViewModel by activityViewModel() private val viewModel: GroupListViewModel by fragmentViewModel()
private lateinit var groupController: GroupSummaryController private lateinit var groupController: GroupSummaryController
@ -40,14 +37,14 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
viewModel.subscribe { renderState(it) } viewModel.subscribe { renderState(it) }
} }
private fun renderState(state: HomeViewState) { 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)
} }
} }
private fun renderSuccess(state: HomeViewState) { private fun renderSuccess(state: GroupListViewState) {
stateView.state = StateView.State.Content stateView.state = StateView.State.Content
groupController.setData(state) groupController.setData(state)
} }
@ -57,7 +54,7 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
} }
override fun onGroupSelected(groupSummary: GroupSummary) { override fun onGroupSelected(groupSummary: GroupSummary) {
viewModel.accept(HomeActions.SelectGroup(groupSummary)) viewModel.accept(GroupListActions.SelectGroup(groupSummary))
} }
} }

View File

@ -0,0 +1,64 @@
package im.vector.riotredesign.features.home.group
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.session.Session
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel
import org.koin.android.ext.android.get
class GroupListViewModel(initialState: GroupListViewState,
private val selectedGroupHolder: SelectedGroupHolder,
private val session: Session
) : RiotViewModel<GroupListViewState>(initialState) {
companion object : MvRxViewModelFactory<GroupListViewState> {
@JvmStatic
override fun create(activity: FragmentActivity, state: GroupListViewState): GroupListViewModel {
val currentSession = Matrix.getInstance().currentSession
val selectedGroupHolder = activity.get<SelectedGroupHolder>()
return GroupListViewModel(state, selectedGroupHolder, currentSession)
}
}
init {
observeGroupSummaries()
observeState()
}
private fun observeState() {
subscribe {
selectedGroupHolder.setSelectedGroup(it.selectedGroup)
}
}
fun accept(action: GroupListActions) {
when (action) {
is GroupListActions.SelectGroup -> handleSelectGroup(action)
}
}
// PRIVATE METHODS *****************************************************************************
private fun handleSelectGroup(action: GroupListActions.SelectGroup) = withState { state ->
if (state.selectedGroup?.groupId != action.groupSummary.groupId) {
setState { copy(selectedGroup = action.groupSummary) }
} else {
setState { copy(selectedGroup = null) }
}
}
private fun observeGroupSummaries() {
session
.rx().liveGroupSummaries()
.execute { async ->
copy(asyncGroups = async)
}
}
}

View File

@ -0,0 +1,11 @@
package im.vector.riotredesign.features.home.group
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.group.model.GroupSummary
data class GroupListViewState(
val asyncGroups: Async<List<GroupSummary>> = Uninitialized,
val selectedGroup: GroupSummary? = null
) : MvRxState

View File

@ -2,12 +2,11 @@ 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
import im.vector.riotredesign.features.home.HomeViewState
class GroupSummaryController(private val callback: Callback? = null class GroupSummaryController(private val callback: Callback? = null
) : TypedEpoxyController<HomeViewState>() { ) : TypedEpoxyController<GroupListViewState>() {
override fun buildModels(viewState: HomeViewState) { override fun buildModels(viewState: GroupListViewState) {
buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup) buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup)
} }

View File

@ -0,0 +1,22 @@
package im.vector.riotredesign.features.home.group
import arrow.core.Option
import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.session.group.model.GroupSummary
import io.reactivex.Observable
class SelectedGroupHolder {
private val selectedGroupStream = BehaviorRelay.createDefault<Option<GroupSummary>>(Option.empty())
fun setSelectedGroup(group: GroupSummary?) {
val optionValue = Option.fromNullable(group)
selectedGroupStream.accept(optionValue)
}
fun selectedGroup(): Observable<Option<GroupSummary>> {
return selectedGroupStream.hide()
}
}

View File

@ -0,0 +1,20 @@
package im.vector.riotredesign.features.home.room
import com.jakewharton.rxrelay2.BehaviorRelay
import io.reactivex.Observable
import io.reactivex.subjects.BehaviorSubject
class VisibleRoomHolder {
private val visibleRoomStream = BehaviorRelay.create<String>()
fun setVisibleRoom(roomId: String) {
visibleRoomStream.accept(roomId)
}
fun visibleRoom(): Observable<String> {
return visibleRoomStream.hide()
}
}

View File

@ -0,0 +1,8 @@
package im.vector.riotredesign.features.home.room.detail
sealed class RoomDetailActions {
data class SendMessage(val text: String) : RoomDetailActions()
object IsDisplayed : RoomDetailActions()
}

View File

@ -1,48 +1,47 @@
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 im.vector.matrix.android.api.Matrix import com.airbnb.mvrx.Success
import im.vector.matrix.android.api.MatrixCallback import com.airbnb.mvrx.args
import im.vector.matrix.android.api.permalinks.PermalinkParser import com.airbnb.mvrx.fragmentViewModel
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.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.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.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
import timber.log.Timber
@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 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 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? {
@ -51,21 +50,15 @@ 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() setupSendButton()
room.timeline(eventId).observe(this, Observer { renderEvents(it) }) roomDetailViewModel.subscribe { renderState(it) }
room.roomSummary.observe(this, Observer { renderRoomSummary(it) }) }
sendButton.setOnClickListener {
val textMessage = composerEditText.text.toString()
if (textMessage.isNotBlank()) {
composerEditText.text = null
room.sendTextMessage(textMessage, object : MatrixCallback<Event> {
}) override fun onResume() {
} super.onResume()
} roomDetailViewModel.accept(RoomDetailActions.IsDisplayed)
} }
private fun setupToolbar() { private fun setupToolbar() {
@ -79,13 +72,43 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
recyclerView.setHasFixedSize(true)
timelineEventController.addModelBuildListener { it.dispatchTo(scrollOnNewMessageCallback) } timelineEventController.addModelBuildListener { it.dispatchTo(scrollOnNewMessageCallback) }
recyclerView.setController(timelineEventController) recyclerView.setController(timelineEventController)
timelineEventController.callback = this timelineEventController.callback = this
} }
private fun renderRoomSummary(roomSummary: RoomSummary?) { private fun setupSendButton() {
roomSummary?.let { sendButton.setOnClickListener {
val textMessage = composerEditText.text.toString()
if (textMessage.isNotBlank()) {
composerEditText.text = null
roomDetailViewModel.accept(RoomDetailActions.SendMessage(textMessage))
}
}
}
private fun renderState(state: RoomDetailViewState) {
renderRoomSummary(state)
renderTimeline(state)
}
private fun renderTimeline(state: RoomDetailViewState) {
when (state.asyncTimelineData) {
is Success -> {
val timelineData = state.asyncTimelineData()
val lockAutoScroll = timelineData?.let {
it.events == timelineEventController.currentList && it.isLoadingForward
} ?: true
scrollOnNewMessageCallback.isLocked.set(lockAutoScroll)
timelineEventController.update(timelineData)
}
}
}
private fun renderRoomSummary(state: RoomDetailViewState) {
state.asyncRoomSummary()?.let {
toolbarTitleView.text = it.displayName toolbarTitleView.text = it.displayName
AvatarRenderer.render(it, toolbarAvatarImageView) AvatarRenderer.render(it, toolbarAvatarImageView)
if (it.topic.isNotEmpty()) { if (it.topic.isNotEmpty()) {
@ -97,16 +120,10 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
} }
} }
private fun renderEvents(events: PagedList<EnrichedEvent>?) {
scrollOnNewMessageCallback.hasBeenUpdated.set(true)
timelineEventController.timeline = events
}
// TimelineEventController.Callback ************************************************************ // TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String) { override fun onUrlClicked(url: String) {
val permalinkData = PermalinkParser.parse(url) homePermalinkHandler.launch(url)
Timber.v("Permalink data : $permalinkData")
} }
} }

View File

@ -0,0 +1,67 @@
package im.vector.riotredesign.features.home.room.detail
import android.support.v4.app.FragmentActivity
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
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,
private val session: Session,
private val visibleRoomHolder: VisibleRoomHolder
) : RiotViewModel<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
val visibleRoomHolder = activity.get<VisibleRoomHolder>()
return RoomDetailViewModel(state, currentSession, visibleRoomHolder)
}
}
init {
observeRoomSummary()
observeTimeline()
room.loadRoomMembersIfNeeded()
}
fun accept(action: RoomDetailActions) {
when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.IsDisplayed -> visibleRoomHolder.setVisibleRoom(roomId)
}
}
// 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 { timelineData ->
copy(asyncTimelineData = timelineData)
}
}
}

View File

@ -0,0 +1,18 @@
package im.vector.riotredesign.features.home.room.detail
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.matrix.android.api.session.room.timeline.TimelineData
data class RoomDetailViewState(
val roomId: String,
val eventId: String?,
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
val asyncTimelineData: Async<TimelineData> = Uninitialized
) : MvRxState {
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)
}

View File

@ -6,10 +6,10 @@ import java.util.concurrent.atomic.AtomicBoolean
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback { class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback {
val hasBeenUpdated = AtomicBoolean(false) var isLocked = AtomicBoolean(true)
override fun onInserted(position: Int, count: Int) { override fun onInserted(position: Int, count: Int) {
if (hasBeenUpdated.compareAndSet(true, false) && position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) { if (isLocked.compareAndSet(false, true) && position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) {
layoutManager.scrollToPosition(0) layoutManager.scrollToPosition(0)
} }
} }

View File

@ -3,19 +3,17 @@ package im.vector.riotredesign.features.home.room.detail.timeline
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixLinkify
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel import im.vector.riotredesign.core.epoxy.KotlinModel
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
data class MessageItem( class MessageItem(
val message: CharSequence? = null, val message: CharSequence? = null,
val time: CharSequence? = null, val time: CharSequence? = null,
val avatarUrl: String?, val avatarUrl: String?,
val memberName: CharSequence? = null, val memberName: CharSequence? = null,
val showInformation: Boolean = true, val showInformation: Boolean = true
val onUrlClickedListener: ((url: String) -> Unit)? = null
) : KotlinModel(R.layout.item_event_message) { ) : KotlinModel(R.layout.item_event_message) {
private val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView) private val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
@ -25,11 +23,7 @@ data class MessageItem(
override fun bind() { override fun bind() {
messageView.text = message messageView.text = message
MatrixLinkify.addLinks(messageView, object : MatrixPermalinkSpan.Callback { MatrixLinkify.addLinkMovementMethod(messageView)
override fun onUrlClicked(url: String) {
onUrlClickedListener?.invoke(url)
}
})
if (showInformation) { if (showInformation) {
avatarImageView.visibility = View.VISIBLE avatarImageView.visibility = View.VISIBLE
memberNameView.visibility = View.VISIBLE memberNameView.visibility = View.VISIBLE

View File

@ -1,6 +1,10 @@
package im.vector.riotredesign.features.home.room.detail.timeline package im.vector.riotredesign.features.home.room.detail.timeline
import im.vector.matrix.android.api.session.events.model.EnrichedEvent import android.text.SpannableStringBuilder
import android.text.util.Linkify
import im.vector.matrix.android.api.permalinks.MatrixLinkify
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.session.events.model.TimelineEvent
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.MessageContent import im.vector.matrix.android.api.session.room.model.MessageContent
import org.threeten.bp.LocalDateTime import org.threeten.bp.LocalDateTime
@ -9,8 +13,8 @@ class MessageItemFactory(private val timelineDateFormatter: TimelineDateFormatte
private val messagesDisplayedWithInformation = HashSet<String?>() private val messagesDisplayedWithInformation = HashSet<String?>()
fun create(event: EnrichedEvent, fun create(event: TimelineEvent,
nextEvent: EnrichedEvent?, nextEvent: TimelineEvent?,
addDaySeparator: Boolean, addDaySeparator: Boolean,
date: LocalDateTime, date: LocalDateTime,
callback: TimelineEventController.Callback? callback: TimelineEventController.Callback?
@ -25,14 +29,24 @@ class MessageItemFactory(private val timelineDateFormatter: TimelineDateFormatte
if (addDaySeparator || nextRoomMember != roomMember) { if (addDaySeparator || nextRoomMember != roomMember) {
messagesDisplayedWithInformation.add(event.root.eventId) messagesDisplayedWithInformation.add(event.root.eventId)
} }
val message = messageContent.body?.let {
val spannable = SpannableStringBuilder(it)
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
override fun onUrlClicked(url: String) {
callback?.onUrlClicked(url)
}
})
Linkify.addLinks(spannable, Linkify.ALL)
spannable
}
val showInformation = messagesDisplayedWithInformation.contains(event.root.eventId) val showInformation = messagesDisplayedWithInformation.contains(event.root.eventId)
return MessageItem( return MessageItem(
message = messageContent.body, message = message,
avatarUrl = roomMember.avatarUrl, avatarUrl = roomMember.avatarUrl,
showInformation = showInformation, showInformation = showInformation,
time = timelineDateFormatter.formatMessageHour(date), time = timelineDateFormatter.formatMessageHour(date),
memberName = roomMember.displayName ?: event.root.sender, memberName = roomMember.displayName ?: event.root.sender
onUrlClickedListener = { callback?.onUrlClicked(it) }
) )
} }

View File

@ -4,7 +4,7 @@ import android.widget.TextView
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel import im.vector.riotredesign.core.epoxy.KotlinModel
data class TextItem( class TextItem(
val text: CharSequence? = null val text: CharSequence? = null
) : KotlinModel(R.layout.item_event_text) { ) : KotlinModel(R.layout.item_event_text) {

View File

@ -1,10 +1,10 @@
package im.vector.riotredesign.features.home.room.detail.timeline package im.vector.riotredesign.features.home.room.detail.timeline
import im.vector.matrix.android.api.session.events.model.EnrichedEvent import im.vector.matrix.android.api.session.events.model.TimelineEvent
class TextItemFactory { class TextItemFactory {
fun create(event: EnrichedEvent): TextItem? { fun create(event: TimelineEvent): TextItem? {
val text = "${event.root.type} events are not yet handled" val text = "${event.root.type} events are not yet handled"
return TextItem(text = text) return TextItem(text = text)
} }

View File

@ -1,93 +1,84 @@
package im.vector.riotredesign.features.home.room.detail.timeline package im.vector.riotredesign.features.home.room.detail.timeline
import android.arch.paging.PagedList
import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel
import im.vector.matrix.android.api.session.events.model.EnrichedEvent import im.vector.matrix.android.api.session.events.model.TimelineEvent
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineData
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.paging.PagedListEpoxyController
class TimelineEventController(private val roomId: String, class TimelineEventController(private val roomId: String,
private val messageItemFactory: MessageItemFactory, private val messageItemFactory: MessageItemFactory,
private val textItemFactory: TextItemFactory, private val textItemFactory: TextItemFactory,
private val dateFormatter: TimelineDateFormatter private val dateFormatter: TimelineDateFormatter
) : EpoxyController( ) : PagedListEpoxyController<TimelineEvent>(
EpoxyAsyncUtil.getAsyncBackgroundHandler(), EpoxyAsyncUtil.getAsyncBackgroundHandler(),
EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyAsyncUtil.getAsyncBackgroundHandler()
) { ) {
init { init {
setFilterDuplicates(true) setFilterDuplicates(true)
} }
private val pagedListCallback = object : PagedList.Callback() { private var isLoadingForward: Boolean = false
override fun onChanged(position: Int, count: Int) { private var isLoadingBackward: Boolean = false
buildSnapshotList() private var hasReachedEnd: Boolean = false
}
override fun onInserted(position: Int, count: Int) {
buildSnapshotList()
}
override fun onRemoved(position: Int, count: Int) {
buildSnapshotList()
}
}
private var snapshotList: List<EnrichedEvent>? = emptyList()
var timeline: PagedList<EnrichedEvent>? = null
set(value) {
field?.removeWeakCallback(pagedListCallback)
field = value
field?.addWeakCallback(null, pagedListCallback)
buildSnapshotList()
}
var callback: Callback? = null var callback: Callback? = null
override fun buildModels() { fun update(timelineData: TimelineData?) {
buildModels(snapshotList) timelineData?.let {
isLoadingForward = it.isLoadingForward
isLoadingBackward = it.isLoadingBackward
hasReachedEnd = it.events.lastOrNull()?.root?.type == EventType.STATE_ROOM_CREATE
submitList(it.events)
requestModelBuild()
}
} }
private fun buildModels(data: List<EnrichedEvent?>?) {
if (data.isNullOrEmpty()) { override fun buildItemModels(currentPosition: Int, items: List<TimelineEvent?>): List<EpoxyModel<*>> {
return if (items.isNullOrEmpty()) {
return emptyList()
} }
for (index in 0 until data.size) { val epoxyModels = ArrayList<EpoxyModel<*>>()
val event = data[index] ?: continue val event = items[currentPosition] ?: return emptyList()
val nextEvent = if (index + 1 < data.size) data[index + 1] else null val nextEvent = if (currentPosition + 1 < items.size) items[currentPosition + 1] else null
val date = event.root.localDateTime() val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime() val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
val item = when (event.root.type) { val item = when (event.root.type) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, addDaySeparator, date, callback) EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, addDaySeparator, date, callback)
else -> textItemFactory.create(event) else -> textItemFactory.create(event)
} }
item item?.also {
?.onBind { timeline?.loadAround(index) } it.id(event.localId)
?.id(event.localId) epoxyModels.add(it)
?.addTo(this)
if (addDaySeparator) {
val formattedDay = dateFormatter.formatMessageDay(date)
DaySeparatorItem(formattedDay).id(roomId + formattedDay).addTo(this)
}
} }
//It's a hack at the moment if (addDaySeparator) {
val isLastEvent = data.last()?.root?.type == EventType.STATE_ROOM_CREATE val formattedDay = dateFormatter.formatMessageDay(date)
val daySeparatorItem = DaySeparatorItem(formattedDay).id(roomId + formattedDay)
epoxyModels.add(daySeparatorItem)
}
return epoxyModels
}
override fun addModels(models: List<EpoxyModel<*>>) {
LoadingItemModel_()
.id(roomId + "forward_loading_item")
.addIf(isLoadingForward, this)
super.add(models)
LoadingItemModel_() LoadingItemModel_()
.id(roomId + "backward_loading_item") .id(roomId + "backward_loading_item")
.addIf(!isLastEvent, this) .addIf(!hasReachedEnd, this)
} }
private fun buildSnapshotList() {
snapshotList = timeline?.snapshot() ?: emptyList()
requestModelBuild()
}
interface Callback { interface Callback {
fun onUrlClicked(url: String) fun onUrlClicked(url: String)

View File

@ -0,0 +1,129 @@
/*
* Copyright 2018 The Android Open Source Project
*
* 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.features.home.room.detail.timeline.paging
import android.arch.paging.PagedList
import android.os.Handler
import android.support.v7.util.DiffUtil
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyViewHolder
/**
* An [EpoxyController] that can work with a [PagedList].
*
* Internally, it caches the model for each item in the [PagedList]. You should override
* [buildItemModel] method to build the model for the given item. Since [PagedList] might include
* `null` items if placeholders are enabled, this method needs to handle `null` values in the list.
*
* By default, the model for each item is added to the model list. To change this behavior (to
* filter items or inject extra items), you can override [addModels] function and manually add built
* models.
*
* @param T The type of the items in the [PagedList].
*/
abstract class PagedListEpoxyController<T>(
/**
* The handler to use for building models. By default this uses the main thread, but you can use
* [EpoxyAsyncUtil.getAsyncBackgroundHandler] to do model building in the background.
*
* The notify thread of your PagedList (from setNotifyExecutor in the PagedList Builder) must be
* the same as this thread. Otherwise Epoxy will crash.
*/
modelBuildingHandler: Handler = EpoxyController.defaultModelBuildingHandler,
/**
* The handler to use when calculating the diff between built model lists.
* By default this uses the main thread, but you can use
* [EpoxyAsyncUtil.getAsyncBackgroundHandler] to do diffing in the background.
*/
diffingHandler: Handler = EpoxyController.defaultDiffingHandler,
/**
* [PagedListEpoxyController] uses an [DiffUtil.ItemCallback] to detect changes between
* [PagedList]s. By default, it relies on simple object equality but you can provide a custom
* one if you don't use all fields in the object in your models.
*/
itemDiffCallback: DiffUtil.ItemCallback<T> = DEFAULT_ITEM_DIFF_CALLBACK as DiffUtil.ItemCallback<T>
) : EpoxyController(modelBuildingHandler, diffingHandler) {
// this is where we keep the already built models
protected val modelCache = PagedListModelCache(
modelBuilder = { pos, item ->
buildItemModels(pos, item)
},
rebuildCallback = {
requestModelBuild()
},
itemDiffCallback = itemDiffCallback,
modelBuildingHandler = modelBuildingHandler
)
var currentList: PagedList<T>? = null
private set
final override fun buildModels() {
addModels(modelCache.getModels())
}
override fun onModelBound(
holder: EpoxyViewHolder,
boundModel: EpoxyModel<*>,
position: Int,
previouslyBoundModel: EpoxyModel<*>?
) {
modelCache.loadAround(boundModel)
}
/**
* This function adds all built models to the adapter. You can override this method to add extra
* items into the model list or remove some.
*/
open fun addModels(models: List<EpoxyModel<*>>) {
super.add(models)
}
/**
* Builds the model for a given item. This must return a single model for each item. If you want
* to inject headers etc, you can override [addModels] function.
*
* If the `item` is `null`, you should provide the placeholder. If your [PagedList] is configured
* without placeholders, you don't need to handle the `null` case.
*/
abstract fun buildItemModels(currentPosition: Int, items: List<T?>): List<EpoxyModel<*>>
/**
* Submit a new paged list.
*
* A diff will be calculated between this list and the previous list so you may still get calls
* to [buildItemModel] with items from the previous list.
*/
fun submitList(newList: PagedList<T>?) {
currentList = newList
modelCache.submitList(newList)
}
companion object {
/**
* [PagedListEpoxyController] calculates a diff on top of the PagedList to check which
* models are invalidated.
* This is the default [DiffUtil.ItemCallback] which uses object equality.
*/
val DEFAULT_ITEM_DIFF_CALLBACK = object : DiffUtil.ItemCallback<Any>() {
override fun areItemsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem
override fun areContentsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem
}
}
}

View File

@ -0,0 +1,171 @@
/*
* Copyright 2018 The Android Open Source Project
*
* 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.features.home.room.detail.timeline.paging
import android.annotation.SuppressLint
import android.arch.paging.AsyncPagedListDiffer
import android.arch.paging.PagedList
import android.os.Handler
import android.support.v7.recyclerview.extensions.AsyncDifferConfig
import android.support.v7.util.DiffUtil
import android.support.v7.util.ListUpdateCallback
import android.util.Log
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel
import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicBoolean
/**
* A PagedList stream wrapper that caches models built for each item. It tracks changes in paged lists and caches
* models for each item when they are invalidated to avoid rebuilding models for the whole list when PagedList is
* updated.
*/
class PagedListModelCache<T>(
private val modelBuilder: (itemIndex: Int, items: List<T>) -> List<EpoxyModel<*>>,
private val rebuildCallback: () -> Unit,
private val itemDiffCallback: DiffUtil.ItemCallback<T>,
private val diffExecutor: Executor? = null,
private val modelBuildingHandler: Handler
) {
// Int is the index of the pagedList item
// We have to be able to find the pagedlist position coming from an epoxy model to trigger
// LoadAround with accuracy
private val modelCache = linkedMapOf<EpoxyModel<*>, Int>()
private var isCacheStale = AtomicBoolean(true)
/**
* Tracks the last accessed position so that we can report it back to the paged list when models are built.
*/
private var lastPosition: Int? = null
/**
* Observer for the PagedList changes that invalidates the model cache when data is updated.
*/
private val updateCallback = object : ListUpdateCallback {
override fun onChanged(position: Int, count: Int, payload: Any?) {
invalidate()
rebuildCallback()
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
invalidate()
rebuildCallback()
}
override fun onInserted(position: Int, count: Int) {
invalidate()
rebuildCallback()
}
override fun onRemoved(position: Int, count: Int) {
invalidate()
rebuildCallback()
}
}
private val asyncDiffer = @SuppressLint("RestrictedApi")
object : AsyncPagedListDiffer<T>(
updateCallback,
AsyncDifferConfig.Builder<T>(
itemDiffCallback
).also { builder ->
if (diffExecutor != null) {
builder.setBackgroundThreadExecutor(diffExecutor)
}
// we have to reply on this private API, otherwise, paged list might be changed when models are being built,
// potentially creating concurrent modification problems.
builder.setMainThreadExecutor { runnable: Runnable ->
modelBuildingHandler.post(runnable)
}
}.build()
) {
init {
if (modelBuildingHandler != EpoxyController.defaultModelBuildingHandler) {
try {
// looks like AsyncPagedListDiffer in 1.x ignores the config.
// Reflection to the rescue.
val mainThreadExecutorField =
AsyncPagedListDiffer::class.java.getDeclaredField("mMainThreadExecutor")
mainThreadExecutorField.isAccessible = true
mainThreadExecutorField.set(this, Executor {
modelBuildingHandler.post(it)
})
} catch (t: Throwable) {
val msg = "Failed to hijack update handler in AsyncPagedListDiffer." +
"You can only build models on the main thread"
Log.e("PagedListModelCache", msg, t)
throw IllegalStateException(msg, t)
}
}
}
}
fun submitList(pagedList: PagedList<T>?) {
asyncDiffer.submitList(pagedList)
}
fun getModels(): List<EpoxyModel<*>> {
if (isCacheStale.compareAndSet(true, false)) {
asyncDiffer.currentList?.forEachIndexed { position, _ ->
buildModel(position)
}
}
lastPosition?.let {
triggerLoadAround(it)
}
return modelCache.keys.toList()
}
fun loadAround(model: EpoxyModel<*>) {
modelCache[model]?.let { itemPosition ->
triggerLoadAround(itemPosition)
lastPosition = itemPosition
}
}
// PRIVATE METHODS *****************************************************************************
private fun invalidate() {
modelCache.clear()
isCacheStale.set(true)
}
private fun cacheModelsAtPosition(itemPosition: Int, epoxyModels: Set<EpoxyModel<*>>) {
epoxyModels.forEach {
modelCache[it] = itemPosition
}
}
private fun buildModel(pos: Int) {
if (pos >= asyncDiffer.currentList?.size ?: 0) {
return
}
modelBuilder(pos, asyncDiffer.currentList as List<T>).also {
cacheModelsAtPosition(pos, it.toSet())
}
}
private fun triggerLoadAround(position: Int) {
asyncDiffer.currentList?.let {
if (it.size > 0) {
it.loadAround(Math.min(position, it.size - 1))
}
}
}
}

View File

@ -0,0 +1,11 @@
package im.vector.riotredesign.features.home.room.list
import im.vector.matrix.android.api.session.room.model.RoomSummary
sealed class RoomListActions {
data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions()
object RoomDisplayed : RoomListActions()
}

View File

@ -13,10 +13,7 @@ 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.StateView import im.vector.riotredesign.core.platform.StateView
import im.vector.riotredesign.features.home.HomeActions
import im.vector.riotredesign.features.home.HomeNavigator import im.vector.riotredesign.features.home.HomeNavigator
import im.vector.riotredesign.features.home.HomeViewModel
import im.vector.riotredesign.features.home.HomeViewState
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
@ -29,7 +26,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: RoomListViewModel 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,22 +38,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: RoomListViewState) {
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.selectedRoom != null) {
homeNavigator.openRoomDetail(state.selectedRoom.roomId)
viewModel.accept(HomeActions.RoomDisplayed)
} }
} }
private fun renderSuccess(state: HomeViewState) { private fun renderSuccess(state: RoomListViewState) {
if (state.asyncRooms().isNullOrEmpty()) { if (state.asyncRooms().isNullOrEmpty()) {
stateView.state = StateView.State.Empty(getString(R.string.room_list_empty)) stateView.state = StateView.State.Empty(getString(R.string.room_list_empty))
} else { } else {
@ -72,13 +65,14 @@ 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(RoomListActions.SelectRoom(room))
homeNavigator.openRoomDetail(room.roomId, null)
} }
} }

View File

@ -0,0 +1,98 @@
package im.vector.riotredesign.features.home.room.list
import android.support.v4.app.FragmentActivity
import arrow.core.Option
import com.airbnb.mvrx.MvRxViewModelFactory
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.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<RoomListViewState>(initialState) {
companion object : MvRxViewModelFactory<RoomListViewState> {
@JvmStatic
override fun create(activity: FragmentActivity, state: RoomListViewState): RoomListViewModel {
val currentSession = Matrix.getInstance().currentSession
val roomSelectionRepository = activity.get<RoomSelectionRepository>()
val selectedGroupHolder = activity.get<SelectedGroupHolder>()
val visibleRoomHolder = activity.get<VisibleRoomHolder>()
return RoomListViewModel(state, currentSession, selectedGroupHolder, visibleRoomHolder, roomSelectionRepository)
}
}
init {
observeRoomSummaries()
observeVisibleRoom()
}
fun accept(action: RoomListActions) {
when (action) {
is RoomListActions.SelectRoom -> handleSelectRoom(action)
}
}
// PRIVATE METHODS *****************************************************************************
private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state ->
if (state.selectedRoomId != action.roomSummary.roomId) {
roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId)
}
}
private fun observeVisibleRoom() {
visibleRoomHolder.visibleRoom()
.subscribeBy {
setState { copy(selectedRoomId = it) }
}
.disposeOnClear()
}
private fun observeRoomSummaries() {
Observable.combineLatest<List<RoomSummary>, Option<GroupSummary>, RoomSummaries>(
session.rx().liveRoomSummaries(),
selectedGroupHolder.selectedGroup(),
BiFunction { rooms, selectedGroupOption ->
val selectedGroup = selectedGroupOption.orNull()
val filteredDirectRooms = rooms
.filter { it.isDirect }
.filter {
if (selectedGroup == null) {
true
} else {
it.otherMemberIds
.intersect(selectedGroup.userIds)
.isNotEmpty()
}
}
val filteredGroupRooms = rooms
.filter { !it.isDirect }
.filter {
selectedGroup?.roomIds?.contains(it.roomId) ?: true
}
RoomSummaries(filteredDirectRooms, filteredGroupRooms)
}
)
.execute { async ->
copy(
asyncRooms = async
)
}
}
}

View File

@ -0,0 +1,20 @@
package im.vector.riotredesign.features.home.room.list
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
data class RoomListViewState(
val asyncRooms: Async<RoomSummaries> = Uninitialized,
val selectedRoomId: String? = null
) : MvRxState
data class RoomSummaries(
val directRooms: List<RoomSummary>,
val groupRooms: List<RoomSummary>
)
fun RoomSummaries?.isNullOrEmpty(): Boolean {
return this == null || (directRooms.isEmpty() && groupRooms.isEmpty())
}

View File

@ -0,0 +1,20 @@
package im.vector.riotredesign.features.home.room.list
import android.content.SharedPreferences
private const val SHARED_PREFS_SELECTED_ROOM_KEY = "SHARED_PREFS_SELECTED_ROOM_KEY"
class RoomSelectionRepository(private val sharedPreferences: SharedPreferences) {
fun lastSelectedRoom(): String? {
return sharedPreferences.getString(SHARED_PREFS_SELECTED_ROOM_KEY, null)
}
fun saveLastSelectedRoom(roomId: String) {
sharedPreferences.edit()
.putString(SHARED_PREFS_SELECTED_ROOM_KEY, roomId)
.apply()
}
}

View File

@ -2,17 +2,15 @@ package im.vector.riotredesign.features.home.room.list
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.features.home.HomeViewState
class RoomSummaryController(private val callback: Callback? = null class RoomSummaryController(private val callback: Callback? = null
) : TypedEpoxyController<HomeViewState>() { ) : TypedEpoxyController<RoomListViewState>() {
private var isDirectRoomsExpanded = true private var isDirectRoomsExpanded = true
private var isGroupRoomsExpanded = true private var isGroupRoomsExpanded = true
override fun buildModels(viewState: HomeViewState) { override fun buildModels(viewState: RoomListViewState) {
val roomSummaries = viewState.asyncRooms()
RoomCategoryItem( RoomCategoryItem(
title = "DIRECT MESSAGES", title = "DIRECT MESSAGES",
isExpanded = isDirectRoomsExpanded, isExpanded = isDirectRoomsExpanded,
@ -25,16 +23,7 @@ class RoomSummaryController(private val callback: Callback? = null
.addTo(this) .addTo(this)
if (isDirectRoomsExpanded) { if (isDirectRoomsExpanded) {
val filteredDirectRooms = viewState.directRooms.filter { buildRoomModels(roomSummaries?.directRooms ?: emptyList(), viewState.selectedRoomId)
if (viewState.selectedGroup == null) {
true
} else {
it.otherMemberIds
.intersect(viewState.selectedGroup.userIds)
.isNotEmpty()
}
}
buildRoomModels(filteredDirectRooms, viewState.selectedRoom)
} }
RoomCategoryItem( RoomCategoryItem(
@ -49,17 +38,14 @@ class RoomSummaryController(private val callback: Callback? = null
.addTo(this) .addTo(this)
if (isGroupRoomsExpanded) { if (isGroupRoomsExpanded) {
val filteredGroupRooms = viewState.groupRooms.filter { buildRoomModels(roomSummaries?.groupRooms ?: emptyList(), viewState.selectedRoomId)
viewState.selectedGroup?.roomIds?.contains(it.roomId) ?: true
}
buildRoomModels(filteredGroupRooms, viewState.selectedRoom)
} }
} }
private fun buildRoomModels(summaries: List<RoomSummary>, selected: RoomSummary?) { private fun buildRoomModels(summaries: List<RoomSummary>, selectedRoomId: String?) {
summaries.forEach { roomSummary -> summaries.forEach { roomSummary ->
val isSelected = roomSummary.roomId == selected?.roomId val isSelected = roomSummary.roomId == selectedRoomId
RoomSummaryItem( RoomSummaryItem(
roomName = roomSummary.displayName, roomName = roomSummary.displayName,
avatarUrl = roomSummary.avatarUrl, avatarUrl = roomSummary.avatarUrl,

View File

@ -27,7 +27,6 @@
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:paddingBottom="8dp"
android:textSize="15sp" android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/toolbarSubtitleView" app:layout_constraintBottom_toTopOf="@+id/toolbarSubtitleView"
app:layout_constraintEnd_toStartOf="@+id/messageTimeView" app:layout_constraintEnd_toStartOf="@+id/messageTimeView"

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
implementation "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,22 @@
package im.vector.matrix.rx
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.timeline.TimelineData
import io.reactivex.Observable
class RxRoom(private val room: Room) {
fun liveRoomSummary(): Observable<RoomSummary> {
return room.roomSummary.asObservable()
}
fun timeline(eventId: String? = null): Observable<TimelineData> {
return room.timeline(eventId).asObservable()
}
}
fun Room.rx(): RxRoom {
return RxRoom(this)
}

View File

@ -6,7 +6,6 @@ import android.support.test.runner.AndroidJUnit4
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.OkReplayRuleChainNoActivity import im.vector.matrix.android.OkReplayRuleChainNoActivity
import im.vector.matrix.android.api.MatrixOptions
import im.vector.matrix.android.api.auth.Authenticator import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.internal.auth.AuthModule import im.vector.matrix.android.internal.auth.AuthModule
import im.vector.matrix.android.internal.di.MatrixModule import im.vector.matrix.android.internal.di.MatrixModule
@ -26,7 +25,7 @@ internal class AuthenticatorTest : InstrumentedTest, KoinTest {
init { init {
Monarchy.init(context()) Monarchy.init(context())
val matrixModule = MatrixModule(MatrixOptions(context())).definition val matrixModule = MatrixModule(context()).definition
val networkModule = NetworkModule().definition val networkModule = NetworkModule().definition
val authModule = AuthModule().definition val authModule = AuthModule().definition
loadKoinModules(listOf(matrixModule, networkModule, authModule)) loadKoinModules(listOf(matrixModule, networkModule, authModule))

View File

@ -39,7 +39,7 @@ internal class ChunkEntityTest : InstrumentedTest {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false) val fakeEvent = createFakeEvent(false)
chunk.add(fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.events.size shouldEqual 1 chunk.events.size shouldEqual 1
} }
} }
@ -49,8 +49,8 @@ internal class ChunkEntityTest : InstrumentedTest {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false) val fakeEvent = createFakeEvent(false)
chunk.add(fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.add(fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.events.size shouldEqual 1 chunk.events.size shouldEqual 1
} }
} }
@ -60,7 +60,7 @@ internal class ChunkEntityTest : InstrumentedTest {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(true) val fakeEvent = createFakeEvent(true)
chunk.add(fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1 chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1
} }
} }
@ -70,7 +70,7 @@ internal class ChunkEntityTest : InstrumentedTest {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk: ChunkEntity = realm.createObject() val chunk: ChunkEntity = realm.createObject()
val fakeEvent = createFakeEvent(false) val fakeEvent = createFakeEvent(false)
chunk.add(fakeEvent, PaginationDirection.FORWARDS) chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0 chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0
} }
} }
@ -81,7 +81,7 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk: ChunkEntity = realm.createObject() val chunk: ChunkEntity = realm.createObject()
val fakeEvents = createFakeListOfEvents(30) val fakeEvents = createFakeListOfEvents(30)
val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size
chunk.addAll(fakeEvents, PaginationDirection.FORWARDS) chunk.addAll("roomId", fakeEvents, PaginationDirection.FORWARDS)
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual numberOfStateEvents chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual numberOfStateEvents
} }
} }
@ -94,7 +94,7 @@ internal class ChunkEntityTest : InstrumentedTest {
val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size
val lastIsState = fakeEvents.last().isStateEvent() val lastIsState = fakeEvents.last().isStateEvent()
val expectedStateIndex = if (lastIsState) -numberOfStateEvents + 1 else -numberOfStateEvents val expectedStateIndex = if (lastIsState) -numberOfStateEvents + 1 else -numberOfStateEvents
chunk.addAll(fakeEvents, PaginationDirection.BACKWARDS) chunk.addAll("roomId", fakeEvents, PaginationDirection.BACKWARDS)
chunk.lastStateIndex(PaginationDirection.BACKWARDS) shouldEqual expectedStateIndex chunk.lastStateIndex(PaginationDirection.BACKWARDS) shouldEqual expectedStateIndex
} }
} }
@ -104,21 +104,38 @@ internal class ChunkEntityTest : InstrumentedTest {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject() val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject()
chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS) chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
chunk1.merge(chunk2, PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.events.size shouldEqual 60 chunk1.events.size shouldEqual 60
} }
} }
@Test
fun merge_shouldAddOnlyDifferentEvents_whenMergingBackward() {
monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject()
val eventsForChunk1 = createFakeListOfEvents(30)
val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
chunk1.isLast = true
chunk2.isLast = false
chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS)
chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS)
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.events.size shouldEqual 40
chunk1.isLast.shouldBeTrue()
}
}
@Test @Test
fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() { fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject() val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject()
chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false) chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false)
chunk1.merge(chunk2, PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.isUnlinked().shouldBeFalse() chunk1.isUnlinked().shouldBeFalse()
} }
} }
@ -128,9 +145,9 @@ internal class ChunkEntityTest : InstrumentedTest {
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
val chunk1: ChunkEntity = realm.createObject() val chunk1: ChunkEntity = realm.createObject()
val chunk2: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject()
chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk1.merge(chunk2, PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.isUnlinked().shouldBeTrue() chunk1.isUnlinked().shouldBeTrue()
} }
} }
@ -142,9 +159,9 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject()
val prevToken = "prev_token" val prevToken = "prev_token"
chunk1.prevToken = prevToken chunk1.prevToken = prevToken
chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk1.merge(chunk2, PaginationDirection.FORWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS)
chunk1.prevToken shouldEqual prevToken chunk1.prevToken shouldEqual prevToken
} }
} }
@ -156,9 +173,9 @@ internal class ChunkEntityTest : InstrumentedTest {
val chunk2: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject()
val nextToken = "next_token" val nextToken = "next_token"
chunk1.nextToken = nextToken chunk1.nextToken = nextToken
chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true)
chunk1.merge(chunk2, PaginationDirection.BACKWARDS) chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
chunk1.nextToken shouldEqual nextToken chunk1.nextToken shouldEqual nextToken
} }
} }

View File

@ -34,7 +34,7 @@ object RoomDataHelper {
prevToken = Random.nextLong(System.currentTimeMillis()).toString() prevToken = Random.nextLong(System.currentTimeMillis()).toString()
isLast = true isLast = true
} }
chunkEntity.addAll(eventList, PaginationDirection.FORWARDS) chunkEntity.addAll("roomId", eventList, PaginationDirection.FORWARDS)
roomEntity.addOrUpdate(chunkEntity) roomEntity.addOrUpdate(chunkEntity)
} }
} }

View File

@ -6,10 +6,11 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.LiveDataTestObserver import im.vector.matrix.android.LiveDataTestObserver
import im.vector.matrix.android.api.thread.MainThreadExecutor import im.vector.matrix.android.api.thread.MainThreadExecutor
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineHolder import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.PagingRequestHelper import im.vector.matrix.android.internal.util.PagingRequestHelper
import im.vector.matrix.android.testCoroutineDispatchers import im.vector.matrix.android.testCoroutineDispatchers
import io.realm.Realm import io.realm.Realm
@ -43,17 +44,17 @@ internal class TimelineHolderTest : InstrumentedTest {
val boundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, PagingRequestHelper(MainThreadExecutor())) val boundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, PagingRequestHelper(MainThreadExecutor()))
RoomDataHelper.fakeInitialSync(monarchy, roomId) RoomDataHelper.fakeInitialSync(monarchy, roomId)
val timelineHolder = DefaultTimelineHolder(roomId, monarchy, taskExecutor, boundaryCallback, getContextOfEventTask) val timelineHolder = DefaultTimelineService(roomId, monarchy, taskExecutor, boundaryCallback, getContextOfEventTask, RoomMemberExtractor(monarchy, roomId))
val timelineObserver = LiveDataTestObserver.test(timelineHolder.timeline()) val timelineObserver = LiveDataTestObserver.test(timelineHolder.timeline())
timelineObserver.awaitNextValue().assertHasValue() timelineObserver.awaitNextValue().assertHasValue()
var pagedList = timelineObserver.value() var timelineData = timelineObserver.value()
pagedList.size shouldEqual 30 timelineData.events.size shouldEqual 30
(0 until pagedList.size).map { (0 until timelineData.events.size).map {
pagedList.loadAround(it) timelineData.events.loadAround(it)
} }
timelineObserver.awaitNextValue().assertHasValue() timelineObserver.awaitNextValue().assertHasValue()
pagedList = timelineObserver.value() timelineData = timelineObserver.value()
pagedList.size shouldEqual 60 timelineData.events.size shouldEqual 60
} }

View File

@ -12,6 +12,7 @@ import im.vector.matrix.android.internal.di.MatrixModule
import im.vector.matrix.android.internal.di.NetworkModule import im.vector.matrix.android.internal.di.NetworkModule
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import org.koin.standalone.inject import org.koin.standalone.inject
import java.util.concurrent.atomic.AtomicBoolean
class Matrix private constructor(context: Context) : MatrixKoinComponent { class Matrix private constructor(context: Context) : MatrixKoinComponent {
@ -40,9 +41,12 @@ class Matrix private constructor(context: Context) : MatrixKoinComponent {
companion object { companion object {
private lateinit var instance: Matrix private lateinit var instance: Matrix
private val isInit = AtomicBoolean(false)
internal fun initialize(context: Context) { internal fun initialize(context: Context) {
instance = Matrix(context.applicationContext) if (isInit.compareAndSet(false, true)) {
instance = Matrix(context.applicationContext)
}
} }
fun getInstance(): Matrix { fun getInstance(): Matrix {

View File

@ -58,7 +58,7 @@ object MatrixLinkify {
} }
private fun addLinkMovementMethod(textView: TextView) { fun addLinkMovementMethod(textView: TextView) {
val movementMethod = textView.movementMethod val movementMethod = textView.movementMethod
if (movementMethod == null || movementMethod !is LinkMovementMethod) { if (movementMethod == null || movementMethod !is LinkMovementMethod) {
if (textView.linksClickable) { if (textView.linksClickable) {

View File

@ -15,4 +15,12 @@ interface Session : RoomService, GroupService {
@MainThread @MainThread
fun close() fun close()
fun addListener(listener: Listener)
fun removeListener(listener: Listener)
// Not used at the moment
interface Listener
} }

View File

@ -1,12 +0,0 @@
package im.vector.matrix.android.api.session.events.interceptor
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
interface EnrichedEventInterceptor {
fun canEnrich(event: EnrichedEvent): Boolean
fun enrich(event: EnrichedEvent)
}

View File

@ -0,0 +1,12 @@
package im.vector.matrix.android.api.session.events.interceptor
import im.vector.matrix.android.api.session.events.model.TimelineEvent
interface TimelineEventInterceptor {
fun canEnrich(event: TimelineEvent): Boolean
fun enrich(event: TimelineEvent)
}

View File

@ -2,7 +2,7 @@ package im.vector.matrix.android.api.session.events.model
import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMember
data class EnrichedEvent( data class TimelineEvent(
val root: Event, val root: Event,
val localId: String, val localId: String,
val roomMember: RoomMember? val roomMember: RoomMember?

View File

@ -3,9 +3,10 @@ package im.vector.matrix.android.api.session.room
import android.arch.lifecycle.LiveData import android.arch.lifecycle.LiveData
import im.vector.matrix.android.api.session.room.model.MyMembership import im.vector.matrix.android.api.session.room.model.MyMembership
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.timeline.TimelineService
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
interface Room : TimelineHolder, SendService { interface Room : TimelineService, SendService {
val roomId: String val roomId: String

View File

@ -7,15 +7,6 @@ interface RoomService {
fun getRoom(roomId: String): Room? fun getRoom(roomId: String): Room?
fun getAllRooms(): List<Room>
fun liveRooms(): LiveData<List<Room>>
fun liveRoomSummaries(): LiveData<List<RoomSummary>> fun liveRoomSummaries(): LiveData<List<RoomSummary>>
fun lastSelectedRoom(): RoomSummary?
fun saveLastSelectedRoom(roomSummary: RoomSummary)
} }

View File

@ -1,11 +0,0 @@
package im.vector.matrix.android.api.session.room
import android.arch.lifecycle.LiveData
import android.arch.paging.PagedList
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
interface TimelineHolder {
fun timeline(eventId: String? = null): LiveData<PagedList<EnrichedEvent>>
}

View File

@ -0,0 +1,10 @@
package im.vector.matrix.android.api.session.room.timeline
import android.arch.paging.PagedList
import im.vector.matrix.android.api.session.events.model.TimelineEvent
data class TimelineData(
val events: PagedList<TimelineEvent>,
val isLoadingForward: Boolean = false,
val isLoadingBackward: Boolean = false
)

View File

@ -0,0 +1,9 @@
package im.vector.matrix.android.api.session.room.timeline
import android.arch.lifecycle.LiveData
interface TimelineService {
fun timeline(eventId: String? = null): LiveData<TimelineData>
}

View File

@ -4,7 +4,6 @@ import android.arch.lifecycle.LiveData
import android.arch.lifecycle.Observer import android.arch.lifecycle.Observer
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
internal interface LiveEntityObserver { internal interface LiveEntityObserver {
@ -39,11 +38,15 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
if (changeSet == null) { if (changeSet == null) {
return return
} }
val updateIndexes = changeSet.orderedCollectionChangeSet.changes + changeSet.orderedCollectionChangeSet.insertions val insertionIndexes = changeSet.orderedCollectionChangeSet.insertions
val updateIndexes = changeSet.orderedCollectionChangeSet.changes
val deletionIndexes = changeSet.orderedCollectionChangeSet.deletions val deletionIndexes = changeSet.orderedCollectionChangeSet.deletions
process(changeSet.realmResults, updateIndexes, deletionIndexes) val inserted = changeSet.realmResults.filterIndexed { index, _ -> insertionIndexes.contains(index) }
val updated = changeSet.realmResults.filterIndexed { index, _ -> updateIndexes.contains(index) }
val deleted = changeSet.realmResults.filterIndexed { index, _ -> deletionIndexes.contains(index) }
process(inserted, updated, deleted)
} }
abstract fun process(results: RealmResults<T>, updateIndexes: IntArray, deletionIndexes: IntArray) abstract fun process(inserted: List<T>, updated: List<T>, deleted: List<T>)
} }

View File

@ -2,7 +2,9 @@ package im.vector.matrix.android.internal.database.helper
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.mapper.asEntity import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.mapper.updateWith
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.EventEntityFields
@ -11,18 +13,21 @@ import im.vector.matrix.android.internal.session.room.timeline.PaginationDirecti
import io.realm.Sort import io.realm.Sort
internal fun ChunkEntity.deleteOnCascade() { internal fun ChunkEntity.deleteOnCascade() {
assertIsManaged()
this.events.deleteAllFromRealm() this.events.deleteAllFromRealm()
this.deleteFromRealm() this.deleteFromRealm()
} }
// By default if a chunk is empty we consider it unlinked // By default if a chunk is empty we consider it unlinked
internal fun ChunkEntity.isUnlinked(): Boolean { internal fun ChunkEntity.isUnlinked(): Boolean {
assertIsManaged()
return events.where().equalTo(EventEntityFields.IS_UNLINKED, false).findAll().isEmpty() return events.where().equalTo(EventEntityFields.IS_UNLINKED, false).findAll().isEmpty()
} }
internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity, internal fun ChunkEntity.merge(roomId: String,
chunkToMerge: ChunkEntity,
direction: PaginationDirection) { direction: PaginationDirection) {
assertIsManaged()
val isChunkToMergeUnlinked = chunkToMerge.isUnlinked() val isChunkToMergeUnlinked = chunkToMerge.isUnlinked()
val isCurrentChunkUnlinked = this.isUnlinked() val isCurrentChunkUnlinked = this.isUnlinked()
val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked
@ -40,17 +45,18 @@ internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity,
eventsToMerge = chunkToMerge.events eventsToMerge = chunkToMerge.events
} }
eventsToMerge.forEach { eventsToMerge.forEach {
add(it, direction, isUnlinked = isUnlinked) add(roomId, it.asDomain(), direction, isUnlinked = isUnlinked)
} }
} }
internal fun ChunkEntity.addAll(events: List<Event>, internal fun ChunkEntity.addAll(roomId: String,
events: List<Event>,
direction: PaginationDirection, direction: PaginationDirection,
stateIndexOffset: Int = 0, stateIndexOffset: Int = 0,
isUnlinked: Boolean = false) { isUnlinked: Boolean = false) {
assertIsManaged()
events.forEach { event -> events.forEach { event ->
add(event, direction, stateIndexOffset, isUnlinked) add(roomId, event, direction, stateIndexOffset, isUnlinked)
} }
} }
@ -58,27 +64,18 @@ internal fun ChunkEntity.updateDisplayIndexes() {
events.forEachIndexed { index, eventEntity -> eventEntity.displayIndex = index } events.forEachIndexed { index, eventEntity -> eventEntity.displayIndex = index }
} }
internal fun ChunkEntity.add(event: Event, internal fun ChunkEntity.add(roomId: String,
event: Event,
direction: PaginationDirection, direction: PaginationDirection,
stateIndexOffset: Int = 0, stateIndexOffset: Int = 0,
isUnlinked: Boolean = false) { isUnlinked: Boolean = false) {
add(event.asEntity(), direction, stateIndexOffset, isUnlinked)
}
internal fun ChunkEntity.add(eventEntity: EventEntity, assertIsManaged()
direction: PaginationDirection, if (event.eventId.isNullOrEmpty() || events.fastContains(event.eventId)) {
stateIndexOffset: Int = 0,
isUnlinked: Boolean = false) {
if (!isManaged) {
throw IllegalStateException("Chunk entity should be managed to use fast contains")
}
if (eventEntity.eventId.isEmpty() || events.fastContains(eventEntity.eventId)) {
return return
} }
var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset) var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset)
if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(eventEntity.type)) { if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.type)) {
currentStateIndex += 1 currentStateIndex += 1
} else if (direction == PaginationDirection.BACKWARDS && events.isNotEmpty()) { } else if (direction == PaginationDirection.BACKWARDS && events.isNotEmpty()) {
val lastEventType = events.last()?.type ?: "" val lastEventType = events.last()?.type ?: ""
@ -86,13 +83,18 @@ internal fun ChunkEntity.add(eventEntity: EventEntity,
currentStateIndex -= 1 currentStateIndex -= 1
} }
} }
val eventEntity = event.toEntity(roomId)
eventEntity.stateIndex = currentStateIndex eventEntity.updateWith(currentStateIndex, isUnlinked)
eventEntity.isUnlinked = isUnlinked
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size
events.add(position, eventEntity) events.add(position, eventEntity)
} }
private fun ChunkEntity.assertIsManaged() {
if (!isManaged) {
throw IllegalStateException("Chunk entity should be managed to use this function")
}
}
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) { return when (direction) {
PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex

View File

@ -1,7 +1,8 @@
package im.vector.matrix.android.internal.database.helper package im.vector.matrix.android.internal.database.helper
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.database.mapper.asEntity import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.mapper.updateWith
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
@ -28,9 +29,8 @@ internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
if (event.eventId == null) { if (event.eventId == null) {
return@forEach return@forEach
} }
val eventEntity = event.asEntity() val eventEntity = event.toEntity(roomId)
eventEntity.stateIndex = stateIndex eventEntity.updateWith(stateIndex, isUnlinked)
eventEntity.isUnlinked = isUnlinked
untimelinedStateEvents.add(eventEntity) untimelinedStateEvents.add(eventEntity)
} }
} }

View File

@ -8,9 +8,10 @@ import im.vector.matrix.android.internal.database.model.EventEntity
internal object EventMapper { internal object EventMapper {
fun map(event: Event): EventEntity { fun map(event: Event, roomId: String): EventEntity {
val eventEntity = EventEntity() val eventEntity = EventEntity()
eventEntity.eventId = event.eventId ?: "" eventEntity.eventId = event.eventId ?: ""
eventEntity.roomId = event.roomId ?: roomId
eventEntity.content = ContentMapper.map(event.content) eventEntity.content = ContentMapper.map(event.content)
val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent
eventEntity.prevContent = ContentMapper.map(resolvedPrevContent) eventEntity.prevContent = ContentMapper.map(resolvedPrevContent)
@ -32,19 +33,24 @@ internal object EventMapper {
originServerTs = eventEntity.originServerTs, originServerTs = eventEntity.originServerTs,
sender = eventEntity.sender, sender = eventEntity.sender,
stateKey = eventEntity.stateKey, stateKey = eventEntity.stateKey,
roomId = null, roomId = eventEntity.roomId,
unsignedData = UnsignedData(eventEntity.age), unsignedData = UnsignedData(eventEntity.age),
redacts = eventEntity.redacts redacts = eventEntity.redacts
) )
} }
}
internal fun EventEntity.updateWith(stateIndex: Int, isUnlinked: Boolean) {
this.stateIndex = stateIndex
this.isUnlinked = isUnlinked
} }
internal fun EventEntity.asDomain(): Event { internal fun EventEntity.asDomain(): Event {
return EventMapper.map(this) return EventMapper.map(this)
} }
internal fun Event.asEntity(): EventEntity { internal fun Event.toEntity(roomId: String): EventEntity {
return EventMapper.map(this) return EventMapper.map(this, roomId)
} }

View File

@ -8,6 +8,7 @@ import java.util.*
internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(), internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(),
var eventId: String = "", var eventId: String = "",
var roomId: String = "",
var type: String = "", var type: String = "",
var content: String? = null, var content: String? = null,
var prevContent: String? = null, var prevContent: String? = null,
@ -27,9 +28,7 @@ internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUI
BOTH BOTH
} }
companion object { companion object
const val DEFAULT_STATE_INDEX = Int.MIN_VALUE
}
@LinkingObjects("events") @LinkingObjects("events")
val chunk: RealmResults<ChunkEntity>? = null val chunk: RealmResults<ChunkEntity>? = null

View File

@ -5,16 +5,15 @@ import io.realm.RealmObject
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var displayName: String? = "", var displayName: String? = "",
var avatarUrl: String? = "", var avatarUrl: String? = "",
var topic: String? = "", var topic: String? = "",
var lastMessage: EventEntity? = null, var lastMessage: EventEntity? = null,
var heroes: RealmList<String> = RealmList(), var heroes: RealmList<String> = RealmList(),
var joinedMembersCount: Int? = 0, var joinedMembersCount: Int? = 0,
var invitedMembersCount: Int? = 0, var invitedMembersCount: Int? = 0,
var isDirect: Boolean = false, var isDirect: Boolean = false,
var isLatestSelected: Boolean = false, var otherMemberIds: RealmList<String> = RealmList()
var otherMemberIds: RealmList<String> = RealmList()
) : RealmObject() { ) : RealmObject() {
companion object companion object

View File

@ -37,6 +37,10 @@ internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds
.findAll() .findAll()
} }
internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: String): ChunkEntity? {
return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull()
}
internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity { internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity {
return realm.createObject<ChunkEntity>().apply { return realm.createObject<ChunkEntity>().apply {
this.prevToken = prevToken this.prevToken = prevToken

View File

@ -1,10 +1,8 @@
package im.vector.matrix.android.internal.database.query package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.* import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmQuery import io.realm.RealmQuery
@ -22,22 +20,19 @@ internal fun EventEntity.Companion.where(realm: Realm,
linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery<EventEntity> { linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery<EventEntity> {
val query = realm.where<EventEntity>() val query = realm.where<EventEntity>()
if (roomId != null) { if (roomId != null) {
query.beginGroup() query.equalTo(EventEntityFields.ROOM_ID, roomId)
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.ROOM}.${RoomEntityFields.ROOM_ID}", roomId)
.or()
.equalTo("${EventEntityFields.ROOM}.${RoomEntityFields.ROOM_ID}", roomId)
.endGroup()
} }
if (type != null) { if (type != null) {
query.equalTo(EventEntityFields.TYPE, type) query.equalTo(EventEntityFields.TYPE, type)
} }
return when (linkFilterMode) { return when (linkFilterMode) {
LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false) LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false)
UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true) UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true)
BOTH -> query BOTH -> query
} }
} }
internal fun RealmQuery<EventEntity>.next(from: Int? = null, strict: Boolean = true): EventEntity? { internal fun RealmQuery<EventEntity>.next(from: Int? = null, strict: Boolean = true): EventEntity? {
if (from != null) { if (from != null) {
if (strict) { if (strict) {
@ -69,7 +64,6 @@ internal fun RealmList<EventEntity>.find(eventId: String): EventEntity? {
return this.where().equalTo(EventEntityFields.EVENT_ID, eventId).findFirst() return this.where().equalTo(EventEntityFields.EVENT_ID, eventId).findFirst()
} }
internal fun RealmList<EventEntity>. internal fun RealmList<EventEntity>.fastContains(eventId: String): Boolean {
fastContains(eventId: String): Boolean {
return this.find(eventId) != null return this.find(eventId) != null
} }

View File

@ -13,9 +13,3 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n
} }
return query return query
} }
internal fun RoomSummaryEntity.Companion.lastSelected(realm: Realm): RoomSummaryEntity? {
return realm.where<RoomSummaryEntity>()
.equalTo(RoomSummaryEntityFields.IS_LATEST_SELECTED, true)
.findFirst()
}

View File

@ -31,6 +31,7 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
private lateinit var scope: Scope private lateinit var scope: Scope
private val liveEntityUpdaters by inject<List<LiveEntityObserver>>() private val liveEntityUpdaters by inject<List<LiveEntityObserver>>()
private val sessionListeners by inject<SessionListeners>()
private val roomService by inject<RoomService>() private val roomService by inject<RoomService>()
private val groupService by inject<GroupService>() private val groupService by inject<GroupService>()
private val syncThread by inject<SyncThread>() private val syncThread by inject<SyncThread>()
@ -62,6 +63,14 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
isOpen = false isOpen = false
} }
override fun addListener(listener: Session.Listener) {
sessionListeners.addListener(listener)
}
override fun removeListener(listener: Session.Listener) {
sessionListeners.removeListener(listener)
}
// ROOM SERVICE // ROOM SERVICE
override fun getRoom(roomId: String): Room? { override fun getRoom(roomId: String): Room? {
@ -69,31 +78,12 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
return roomService.getRoom(roomId) return roomService.getRoom(roomId)
} }
override fun getAllRooms(): List<Room> {
assert(isOpen)
return roomService.getAllRooms()
}
override fun liveRooms(): LiveData<List<Room>> {
assert(isOpen)
return roomService.liveRooms()
}
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> { override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
assert(isOpen) assert(isOpen)
return roomService.liveRoomSummaries() return roomService.liveRoomSummaries()
} }
override fun lastSelectedRoom(): RoomSummary? {
assert(isOpen)
return roomService.lastSelectedRoom()
}
override fun saveLastSelectedRoom(roomSummary: RoomSummary) {
assert(isOpen)
roomService.saveLastSelectedRoom(roomSummary)
}
// GROUP SERVICE // GROUP SERVICE
override fun getGroup(groupId: String): Group? { override fun getGroup(groupId: String): Group? {

View File

@ -0,0 +1,17 @@
package im.vector.matrix.android.internal.session
import im.vector.matrix.android.api.session.Session
internal class SessionListeners {
private val listeners = ArrayList<Session.Listener>()
fun addListener(listener: Session.Listener) {
listeners.add(listener)
}
fun removeListener(listener: Session.Listener) {
listeners.remove(listener)
}
}

View File

@ -6,7 +6,6 @@ import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
import im.vector.matrix.android.internal.session.group.DefaultGroupService import im.vector.matrix.android.internal.session.group.DefaultGroupService
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.room.DefaultRoomService import im.vector.matrix.android.internal.session.room.DefaultRoomService
@ -14,6 +13,7 @@ import im.vector.matrix.android.internal.session.room.RoomAvatarResolver
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayNameResolver import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayNameResolver
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
import im.vector.matrix.android.internal.util.md5 import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import org.koin.dsl.module.module import org.koin.dsl.module.module
@ -75,7 +75,10 @@ internal class SessionModule(private val sessionParams: SessionParams) {
} }
scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
SessionListeners()
}
scope(DefaultSession.SCOPE) {
val roomSummaryUpdater = RoomSummaryUpdater(get(), get(), get(), get(), sessionParams.credentials) val roomSummaryUpdater = RoomSummaryUpdater(get(), get(), get(), get(), sessionParams.credentials)
val groupSummaryUpdater = GroupSummaryUpdater(get()) val groupSummaryUpdater = GroupSummaryUpdater(get())
val eventsPruner = EventsPruner(get()) val eventsPruner = EventsPruner(get())

View File

@ -15,9 +15,7 @@ internal class GetGroupDataWorker(context: Context,
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
val groupIds: List<String>, val groupIds: List<String>
val updateIndexes: List<Int>,
val deletionIndexes: List<Int>
) )
private val getGroupDataTask by inject<GetGroupDataTask>() private val getGroupDataTask by inject<GetGroupDataTask>()
@ -26,8 +24,7 @@ internal class GetGroupDataWorker(context: Context,
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure() ?: return Result.failure()
val results = params.updateIndexes.map { index -> val results = params.groupIds.map { groupId ->
val groupId = params.groupIds[index]
fetchGroupData(groupId) fetchGroupData(groupId)
} }
val isSuccessful = results.none { it.isFailure() } val isSuccessful = results.none { it.isFailure() }

View File

@ -1,12 +1,15 @@
package im.vector.matrix.android.internal.session.group package im.vector.matrix.android.internal.session.group
import androidx.work.* import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.model.GroupEntity import im.vector.matrix.android.internal.database.model.GroupEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.WorkerParamsFactory
import io.realm.RealmResults
private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER"
@ -19,9 +22,9 @@ internal class GroupSummaryUpdater(monarchy: Monarchy
.setRequiredNetworkType(NetworkType.CONNECTED) .setRequiredNetworkType(NetworkType.CONNECTED)
.build() .build()
override fun process(results: RealmResults<GroupEntity>, updateIndexes: IntArray, deletionIndexes: IntArray) { override fun process(inserted: List<GroupEntity>, updated: List<GroupEntity>, deleted: List<GroupEntity>) {
val groupIds = results.map { it.groupId } val newGroupIds = inserted.map { it.groupId }
val getGroupDataWorkerParams = GetGroupDataWorker.Params(groupIds, updateIndexes.toList(), deletionIndexes.toList()) val getGroupDataWorkerParams = GetGroupDataWorker.Params(newGroupIds)
val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams) val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams)
val sendWork = OneTimeWorkRequestBuilder<GetGroupDataWorker>() val sendWork = OneTimeWorkRequestBuilder<GetGroupDataWorker>()

View File

@ -2,26 +2,24 @@ package im.vector.matrix.android.internal.session.room
import android.arch.lifecycle.LiveData import android.arch.lifecycle.LiveData
import android.arch.lifecycle.Transformations import android.arch.lifecycle.Transformations
import android.arch.paging.PagedList
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
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.events.model.Event
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.SendService import im.vector.matrix.android.api.session.room.SendService
import im.vector.matrix.android.api.session.room.TimelineHolder
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.MyMembership import im.vector.matrix.android.api.session.room.model.MyMembership
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.timeline.TimelineData
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.MatrixKoinComponent import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.session.SessionListeners
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
@ -33,9 +31,8 @@ internal data class DefaultRoom(
) : Room, MatrixKoinComponent { ) : Room, MatrixKoinComponent {
private val loadRoomMembersTask by inject<LoadRoomMembersTask>() private val loadRoomMembersTask by inject<LoadRoomMembersTask>()
private val syncTokenStore by inject<SyncTokenStore>()
private val monarchy by inject<Monarchy>() private val monarchy by inject<Monarchy>()
private val timelineHolder by inject<TimelineHolder> { parametersOf(roomId) } private val timelineService by inject<TimelineService> { parametersOf(roomId) }
private val sendService by inject<SendService> { parametersOf(roomId) } private val sendService by inject<SendService> { parametersOf(roomId) }
private val taskExecutor by inject<TaskExecutor>() private val taskExecutor by inject<TaskExecutor>()
@ -50,25 +47,13 @@ internal data class DefaultRoom(
} }
} }
override fun timeline(eventId: String?): LiveData<PagedList<EnrichedEvent>> { override fun timeline(eventId: String?): LiveData<TimelineData> {
return timelineHolder.timeline(eventId) return timelineService.timeline(eventId)
} }
override fun loadRoomMembersIfNeeded(): Cancelable { override fun loadRoomMembersIfNeeded(): Cancelable {
return if (areAllMembersLoaded()) { val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE)
object : Cancelable {} return loadRoomMembersTask.configureWith(params).executeBy(taskExecutor)
} else {
val token = syncTokenStore.getLastToken()
val params = LoadRoomMembersTask.Params(roomId, token, Membership.LEAVE)
loadRoomMembersTask.configureWith(params).executeBy(taskExecutor)
}
}
private fun areAllMembersLoaded(): Boolean {
return monarchy
.fetchAllCopiedSync { RoomEntity.where(it, roomId) }
.firstOrNull()
?.areAllMembersLoaded ?: false
} }
@ -76,4 +61,5 @@ internal data class DefaultRoom(
return sendService.sendTextMessage(text, callback) return sendService.sendTextMessage(text, callback)
} }
} }

View File

@ -9,19 +9,10 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.lastSelected
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
internal class DefaultRoomService(private val monarchy: Monarchy) : RoomService { internal class DefaultRoomService(private val monarchy: Monarchy) : RoomService {
override fun getAllRooms(): List<Room> {
var rooms: List<Room> = emptyList()
monarchy.doWithRealm { realm ->
rooms = RoomEntity.where(realm).findAll().map { it.asDomain() }
}
return rooms
}
override fun getRoom(roomId: String): Room? { override fun getRoom(roomId: String): Room? {
var room: Room? = null var room: Room? = null
monarchy.doWithRealm { realm -> monarchy.doWithRealm { realm ->
@ -30,34 +21,10 @@ internal class DefaultRoomService(private val monarchy: Monarchy) : RoomService
return room return room
} }
override fun liveRooms(): LiveData<List<Room>> {
return monarchy.findAllMappedWithChanges(
{ realm -> RoomEntity.where(realm) },
{ it.asDomain() }
)
}
override fun liveRoomSummaries(): LiveData<List<RoomSummary>> { override fun liveRoomSummaries(): LiveData<List<RoomSummary>> {
return monarchy.findAllMappedWithChanges( return monarchy.findAllMappedWithChanges(
{ realm -> RoomSummaryEntity.where(realm).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) }, { realm -> RoomSummaryEntity.where(realm).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) },
{ it.asDomain() } { it.asDomain() }
) )
} }
override fun lastSelectedRoom(): RoomSummary? {
var lastSelected: RoomSummary? = null
monarchy.doWithRealm { realm ->
lastSelected = RoomSummaryEntity.lastSelected(realm)?.asDomain()
}
return lastSelected
}
override fun saveLastSelectedRoom(roomSummary: RoomSummary) {
monarchy.writeAsync { realm ->
val lastSelected = RoomSummaryEntity.lastSelected(realm)
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomSummary.roomId).findFirst()
lastSelected?.isLatestSelected = false
roomSummaryEntity?.isLatestSelected = true
}
}
} }

View File

@ -2,8 +2,8 @@ package im.vector.matrix.android.internal.session.room
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.room.SendService import im.vector.matrix.android.api.session.room.SendService
import im.vector.matrix.android.api.session.room.TimelineHolder
import im.vector.matrix.android.api.session.room.send.EventFactory import im.vector.matrix.android.api.session.room.send.EventFactory
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.internal.session.DefaultSession import im.vector.matrix.android.internal.session.DefaultSession
import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
@ -11,7 +11,7 @@ import im.vector.matrix.android.internal.session.room.members.RoomMemberExtracto
import im.vector.matrix.android.internal.session.room.send.DefaultSendService import im.vector.matrix.android.internal.session.room.send.DefaultSendService
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineHolder import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback
@ -32,7 +32,7 @@ class RoomModule {
} }
scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
DefaultLoadRoomMembersTask(get(), get()) as LoadRoomMembersTask DefaultLoadRoomMembersTask(get(), get(), get()) as LoadRoomMembersTask
} }
scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
@ -56,7 +56,7 @@ class RoomModule {
val helper = PagingRequestHelper(Executors.newSingleThreadExecutor()) val helper = PagingRequestHelper(Executors.newSingleThreadExecutor())
val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, get(), get(), get(), helper) val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, get(), get(), get(), helper)
val roomMemberExtractor = RoomMemberExtractor(get(), roomId) val roomMemberExtractor = RoomMemberExtractor(get(), roomId)
DefaultTimelineHolder(roomId, get(), get(), timelineBoundaryCallback, get(), roomMemberExtractor) as TimelineHolder DefaultTimelineService(roomId, get(), get(), timelineBoundaryCallback, get(), roomMemberExtractor) as TimelineService
} }
factory { (roomId: String) -> factory { (roomId: String) ->

View File

@ -17,7 +17,6 @@ import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.members.RoomMembers import im.vector.matrix.android.internal.session.room.members.RoomMembers
import io.realm.Realm import io.realm.Realm
import io.realm.RealmResults
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
internal class RoomSummaryUpdater(monarchy: Monarchy, internal class RoomSummaryUpdater(monarchy: Monarchy,
@ -29,13 +28,10 @@ internal class RoomSummaryUpdater(monarchy: Monarchy,
override val query = Monarchy.Query<RoomEntity> { RoomEntity.where(it) } override val query = Monarchy.Query<RoomEntity> { RoomEntity.where(it) }
override fun process(results: RealmResults<RoomEntity>, updateIndexes: IntArray, deletionIndexes: IntArray) { override fun process(inserted: List<RoomEntity>, updated: List<RoomEntity>, deleted: List<RoomEntity>) {
val rooms = results.map { it.asDomain() } val rooms = (inserted + updated).map { it.asDomain() }
monarchy.writeAsync { realm -> monarchy.writeAsync { realm ->
updateIndexes.forEach { index -> rooms.forEach { updateRoom(realm, it) }
val data = rooms[index]
updateRoom(realm, data)
}
} }
} }

View File

@ -3,33 +3,40 @@ package im.vector.matrix.android.internal.session.room.members
import arrow.core.Try import arrow.core.Try
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.database.helper.addStateEvents import im.vector.matrix.android.internal.database.helper.addStateEvents
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.tryTransactionSync import im.vector.matrix.android.internal.util.tryTransactionSync
internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Boolean> { internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Boolean> {
data class Params( data class Params(
val roomId: String, val roomId: String,
val streamToken: String?,
val excludeMembership: Membership? = null val excludeMembership: Membership? = null
) )
} }
internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI,
private val monarchy: Monarchy private val monarchy: Monarchy,
private val syncTokenStore: SyncTokenStore
) : LoadRoomMembersTask { ) : LoadRoomMembersTask {
override fun execute(params: LoadRoomMembersTask.Params): Try<Boolean> { override fun execute(params: LoadRoomMembersTask.Params): Try<Boolean> {
return executeRequest<RoomMembersResponse> { return if (areAllMembersAlreadyLoaded(params.roomId)) {
apiCall = roomAPI.getMembers(params.roomId, null, null, params.excludeMembership?.value) Try.just(true)
}.flatMap { response -> } else {
insertInDb(response, params.roomId) //TODO use this token
}.map { true } val lastToken = syncTokenStore.getLastToken()
executeRequest<RoomMembersResponse> {
apiCall = roomAPI.getMembers(params.roomId, null, null, params.excludeMembership?.value)
}.flatMap { response ->
insertInDb(response, params.roomId)
}.map { true }
}
} }
private fun insertInDb(response: RoomMembersResponse, roomId: String): Try<RoomMembersResponse> { private fun insertInDb(response: RoomMembersResponse, roomId: String): Try<RoomMembersResponse> {
@ -37,7 +44,7 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI,
.tryTransactionSync { realm -> .tryTransactionSync { realm ->
// We ignore all the already known members // We ignore all the already known members
val roomEntity = RoomEntity.where(realm, roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: throw IllegalStateException("You shouldn't use this method without a room") ?: throw IllegalStateException("You shouldn't use this method without a room")
val roomMembers = RoomMembers(realm, roomId).getLoaded() val roomMembers = RoomMembers(realm, roomId).getLoaded()
val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) } val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) }
@ -48,4 +55,11 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI,
.map { response } .map { response }
} }
private fun areAllMembersAlreadyLoaded(roomId: String): Boolean {
return monarchy
.fetchAllCopiedSync { RoomEntity.where(it, roomId) }
.firstOrNull()
?.areAllMembersLoaded ?: false
}
} }

View File

@ -15,7 +15,12 @@ import io.realm.RealmQuery
internal class RoomMemberExtractor(private val monarchy: Monarchy, internal class RoomMemberExtractor(private val monarchy: Monarchy,
private val roomId: String) { private val roomId: String) {
private val cached = HashMap<String, RoomMember?>()
fun extractFrom(event: EventEntity): RoomMember? { fun extractFrom(event: EventEntity): RoomMember? {
if (cached.containsKey(event.eventId)) {
return cached[event.eventId]
}
val sender = event.sender ?: return null val sender = event.sender ?: return null
// If the event is unlinked we want to fetch unlinked state events // If the event is unlinked we want to fetch unlinked state events
val unlinked = event.isUnlinked val unlinked = event.isUnlinked
@ -23,11 +28,13 @@ internal class RoomMemberExtractor(private val monarchy: Monarchy,
// If prevContent is null we fallback to the Int.MIN state events content() // If prevContent is null we fallback to the Int.MIN state events content()
val content = if (event.stateIndex <= 0) { val content = if (event.stateIndex <= 0) {
baseQuery(monarchy, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent baseQuery(monarchy, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent
?: baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content ?: baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content
} else { } else {
baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content
} }
return ContentMapper.map(content).toModel() val roomMember: RoomMember? = ContentMapper.map(content).toModel()
cached[event.eventId] = roomMember
return roomMember
} }
private fun baseQuery(monarchy: Monarchy, private fun baseQuery(monarchy: Monarchy,

View File

@ -10,7 +10,6 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.WorkerParamsFactory
import io.realm.RealmResults
private const val PRUNE_EVENT_WORKER = "PRUNE_EVENT_WORKER" private const val PRUNE_EVENT_WORKER = "PRUNE_EVENT_WORKER"
@ -19,9 +18,9 @@ internal class EventsPruner(monarchy: Monarchy) :
override val query = Monarchy.Query<EventEntity> { EventEntity.where(it, type = EventType.REDACTION) } override val query = Monarchy.Query<EventEntity> { EventEntity.where(it, type = EventType.REDACTION) }
override fun process(results: RealmResults<EventEntity>, updateIndexes: IntArray, deletionIndexes: IntArray) { override fun process(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
val redactionEvents = results.map { it.asDomain() } val redactionEvents = inserted.map { it.asDomain() }
val pruneEventWorkerParams = PruneEventWorker.Params(redactionEvents, updateIndexes.toList(), deletionIndexes.toList()) val pruneEventWorkerParams = PruneEventWorker.Params(redactionEvents)
val workData = WorkerParamsFactory.toData(pruneEventWorkerParams) val workData = WorkerParamsFactory.toData(pruneEventWorkerParams)
val sendWork = OneTimeWorkRequestBuilder<PruneEventWorker>() val sendWork = OneTimeWorkRequestBuilder<PruneEventWorker>()

View File

@ -13,6 +13,7 @@ import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.MatrixKoinComponent import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.WorkerParamsFactory
import im.vector.matrix.android.internal.util.tryTransactionAsync import im.vector.matrix.android.internal.util.tryTransactionAsync
import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.Realm import io.realm.Realm
import org.koin.standalone.inject import org.koin.standalone.inject
@ -22,9 +23,7 @@ internal class PruneEventWorker(context: Context,
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
val redactionEvents: List<Event>, val redactionEvents: List<Event>
val updateIndexes: List<Int>,
val deletionIndexes: List<Int>
) )
private val monarchy by inject<Monarchy>() private val monarchy by inject<Monarchy>()
@ -33,10 +32,9 @@ internal class PruneEventWorker(context: Context,
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure() ?: return Result.failure()
val result = monarchy.tryTransactionAsync { realm -> val result = monarchy.tryTransactionSync { realm ->
params.updateIndexes.forEach { index -> params.redactionEvents.forEach { event ->
val data = params.redactionEvents[index] pruneEvent(realm, event)
pruneEvent(realm, data)
} }
} }
return result.fold({ Result.retry() }, { Result.success() }) return result.fold({ Result.retry() }, { Result.success() })

View File

@ -1,11 +1,6 @@
package im.vector.matrix.android.internal.session.room.send package im.vector.matrix.android.internal.session.room.send
import androidx.work.BackoffPolicy import androidx.work.*
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
@ -37,8 +32,8 @@ internal class DefaultSendService(private val roomId: String,
monarchy.tryTransactionAsync { realm -> monarchy.tryTransactionAsync { realm ->
val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
?: return@tryTransactionAsync ?: return@tryTransactionAsync
chunkEntity.add(event, PaginationDirection.FORWARDS) chunkEntity.add(roomId, event, PaginationDirection.FORWARDS)
chunkEntity.updateDisplayIndexes() chunkEntity.updateDisplayIndexes()
} }

View File

@ -1,18 +1,17 @@
package im.vector.matrix.android.internal.session.room.timeline package im.vector.matrix.android.internal.session.room.timeline
import arrow.core.Try import arrow.core.Try
import arrow.core.failure
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.FilterUtil import im.vector.matrix.android.internal.util.FilterUtil
internal interface PaginationTask : Task<PaginationTask.Params, TokenChunkEvent> { internal interface PaginationTask : Task<PaginationTask.Params, Boolean> {
data class Params( data class Params(
val roomId: String, val roomId: String,
val from: String?, val from: String,
val direction: PaginationDirection, val direction: PaginationDirection,
val limit: Int val limit: Int
) )
@ -23,17 +22,14 @@ internal class DefaultPaginationTask(private val roomAPI: RoomAPI,
private val tokenChunkEventPersistor: TokenChunkEventPersistor private val tokenChunkEventPersistor: TokenChunkEventPersistor
) : PaginationTask { ) : PaginationTask {
override fun execute(params: PaginationTask.Params): Try<TokenChunkEvent> { override fun execute(params: PaginationTask.Params): Try<Boolean> {
if (params.from == null) {
return RuntimeException("From token shouldn't be null").failure()
}
val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString() val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString()
return executeRequest<PaginationResponse> { return executeRequest<PaginationResponse> {
apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter)
}.flatMap { chunk -> }.flatMap { chunk ->
tokenChunkEventPersistor tokenChunkEventPersistor
.insertInDb(chunk, params.roomId, params.direction) .insertInDb(chunk, params.roomId, params.direction)
.map { chunk }
} }
} }
} }

View File

@ -1,96 +0,0 @@
package im.vector.matrix.android.internal.session.room.timeline
import android.arch.lifecycle.LiveData
import android.arch.paging.LivePagedListBuilder
import android.arch.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.interceptor.EnrichedEventInterceptor
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.room.TimelineHolder
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.Realm
import io.realm.RealmQuery
private const val PAGE_SIZE = 30
internal class DefaultTimelineHolder(private val roomId: String,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val boundaryCallback: TimelineBoundaryCallback,
private val contextOfEventTask: GetContextOfEventTask,
private val roomMemberExtractor: RoomMemberExtractor
) : TimelineHolder {
private val eventInterceptors = ArrayList<EnrichedEventInterceptor>()
override fun timeline(eventId: String?): LiveData<PagedList<EnrichedEvent>> {
clearUnlinkedEvents()
if (eventId != null) {
fetchEventIfNeeded(eventId)
}
val realmDataSourceFactory = monarchy.createDataSourceFactory {
buildDataSourceFactoryQuery(it, eventId)
}
val domainSourceFactory = realmDataSourceFactory
.map { eventEntity ->
val roomMember = roomMemberExtractor.extractFrom(eventEntity)
EnrichedEvent(eventEntity.asDomain(), eventEntity.localId, roomMember)
}
val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(PAGE_SIZE)
.build()
val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig).setBoundaryCallback(boundaryCallback)
return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
}
private fun clearUnlinkedEvents() {
monarchy.tryTransactionSync { realm ->
val unlinkedEvents = EventEntity
.where(realm, roomId = roomId)
.equalTo(EventEntityFields.IS_UNLINKED, true)
.findAll()
unlinkedEvents.deleteAllFromRealm()
}
}
private fun fetchEventIfNeeded(eventId: String) {
if (!isEventPersisted(eventId)) {
val params = GetContextOfEventTask.Params(roomId, eventId)
contextOfEventTask.configureWith(params).executeBy(taskExecutor)
}
}
private fun isEventPersisted(eventId: String): Boolean {
var isEventPersisted = false
monarchy.doWithRealm {
isEventPersisted = EventEntity.where(it, eventId = eventId).findFirst() != null
}
return isEventPersisted
}
private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery<EventEntity> {
val query = if (eventId == null) {
EventEntity
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true)
} else {
EventEntity
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
.`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId))
}
return query.sort(EventEntityFields.DISPLAY_INDEX)
}
}

View File

@ -0,0 +1,128 @@
package im.vector.matrix.android.internal.session.room.timeline
import android.arch.lifecycle.LiveData
import android.arch.paging.LivePagedListBuilder
import android.arch.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.interceptor.TimelineEventInterceptor
import im.vector.matrix.android.api.session.events.model.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineData
import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.LiveDataUtils
import im.vector.matrix.android.internal.util.PagingRequestHelper
import im.vector.matrix.android.internal.util.tryTransactionAsync
import io.realm.Realm
import io.realm.RealmQuery
private const val PAGE_SIZE = 100
private const val PREFETCH_DISTANCE = 30
private const val EVENT_NOT_FOUND_INDEX = -1
internal class DefaultTimelineService(private val roomId: String,
private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val boundaryCallback: TimelineBoundaryCallback,
private val contextOfEventTask: GetContextOfEventTask,
private val roomMemberExtractor: RoomMemberExtractor
) : TimelineService {
private val eventInterceptors = ArrayList<TimelineEventInterceptor>()
override fun timeline(eventId: String?): LiveData<TimelineData> {
clearUnlinkedEvents()
val initialLoadKey = getInitialLoadKey(eventId)
val realmDataSourceFactory = monarchy.createDataSourceFactory {
buildDataSourceFactoryQuery(it, eventId)
}
val domainSourceFactory = realmDataSourceFactory
.map { eventEntity ->
val roomMember = roomMemberExtractor.extractFrom(eventEntity)
TimelineEvent(eventEntity.asDomain(), eventEntity.localId, roomMember)
}
val pagedListConfig = buildPagedListConfig()
val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig)
.setBoundaryCallback(boundaryCallback)
.setInitialLoadKey(initialLoadKey)
val eventsLiveData = monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
return LiveDataUtils.combine(eventsLiveData, boundaryCallback.status) { events, status ->
val isLoadingForward = status.before == PagingRequestHelper.Status.RUNNING
val isLoadingBackward = status.after == PagingRequestHelper.Status.RUNNING
TimelineData(events, isLoadingForward, isLoadingBackward)
}
}
// PRIVATE FUNCTIONS ***************************************************************************
private fun getInitialLoadKey(eventId: String?): Int {
var initialLoadKey = 0
if (eventId != null) {
val indexOfEvent = indexOfEvent(eventId)
if (indexOfEvent == EVENT_NOT_FOUND_INDEX) {
fetchEvent(eventId)
} else {
initialLoadKey = indexOfEvent
}
}
return initialLoadKey
}
private fun fetchEvent(eventId: String) {
val params = GetContextOfEventTask.Params(roomId, eventId)
contextOfEventTask.configureWith(params).executeBy(taskExecutor)
}
private fun buildPagedListConfig(): PagedList.Config {
return PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(PAGE_SIZE)
.setInitialLoadSizeHint(2 * PAGE_SIZE)
.setPrefetchDistance(PREFETCH_DISTANCE)
.build()
}
private fun clearUnlinkedEvents() {
monarchy.tryTransactionAsync { realm ->
val unlinkedEvents = EventEntity
.where(realm, roomId = roomId)
.equalTo(EventEntityFields.IS_UNLINKED, true)
.findAll()
unlinkedEvents.deleteAllFromRealm()
}
}
private fun indexOfEvent(eventId: String): Int {
var displayIndex = EVENT_NOT_FOUND_INDEX
monarchy.doWithRealm {
displayIndex = EventEntity.where(it, eventId = eventId).findFirst()?.displayIndex ?: EVENT_NOT_FOUND_INDEX
}
return displayIndex
}
private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery<EventEntity> {
val query = if (eventId == null) {
EventEntity
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true)
} else {
EventEntity
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
.`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId))
}
return query.sort(EventEntityFields.DISPLAY_INDEX)
}
}

View File

@ -1,60 +1,86 @@
package im.vector.matrix.android.internal.session.room.timeline package im.vector.matrix.android.internal.session.room.timeline
import android.arch.lifecycle.LiveData
import android.arch.paging.PagedList import android.arch.paging.PagedList
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.EnrichedEvent import im.vector.matrix.android.api.session.events.model.TimelineEvent
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.query.findIncludingEvent
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.util.PagingRequestHelper import im.vector.matrix.android.internal.util.PagingRequestHelper
import java.util.*
internal class TimelineBoundaryCallback(private val roomId: String, internal class TimelineBoundaryCallback(private val roomId: String,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val paginationTask: PaginationTask, private val paginationTask: PaginationTask,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val helper: PagingRequestHelper private val helper: PagingRequestHelper
) : PagedList.BoundaryCallback<EnrichedEvent>() { ) : PagedList.BoundaryCallback<TimelineEvent>() {
var limit = 30 var limit = 30
val status = object : LiveData<PagingRequestHelper.StatusReport>() {
init {
value = PagingRequestHelper.StatusReport.createDefault()
}
val listener = PagingRequestHelper.Listener { postValue(it) }
override fun onActive() {
helper.addListener(listener)
}
override fun onInactive() {
helper.removeListener(listener)
}
}
override fun onZeroItemsLoaded() { override fun onZeroItemsLoaded() {
// actually, it's not possible // actually, it's not possible
} }
override fun onItemAtEndLoaded(itemAtEnd: EnrichedEvent) { override fun onItemAtEndLoaded(itemAtEnd: TimelineEvent) {
val token = itemAtEnd.root.eventId?.let { getToken(it, PaginationDirection.BACKWARDS) }
?: return
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) { helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
runPaginationRequest(it, itemAtEnd, PaginationDirection.BACKWARDS) executePaginationTask(it, token, PaginationDirection.BACKWARDS)
} }
} }
override fun onItemAtFrontLoaded(itemAtFront: EnrichedEvent) { override fun onItemAtFrontLoaded(itemAtFront: TimelineEvent) {
val token = itemAtFront.root.eventId?.let { getToken(it, PaginationDirection.FORWARDS) }
?: return
helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) { helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) {
runPaginationRequest(it, itemAtFront, PaginationDirection.FORWARDS) executePaginationTask(it, token, PaginationDirection.FORWARDS)
} }
} }
private fun runPaginationRequest(requestCallback: PagingRequestHelper.Request.Callback, private fun getToken(eventId: String, direction: PaginationDirection): String? {
item: EnrichedEvent,
direction: PaginationDirection) {
var token: String? = null var token: String? = null
monarchy.doWithRealm { realm -> monarchy.doWithRealm { realm ->
if (item.root.eventId == null) { val chunkEntity = ChunkEntity.findIncludingEvent(realm, eventId)
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(item.root.eventId)).firstOrNull()
token = if (direction == PaginationDirection.FORWARDS) chunkEntity?.nextToken else chunkEntity?.prevToken token = if (direction == PaginationDirection.FORWARDS) chunkEntity?.nextToken else chunkEntity?.prevToken
} }
return token
}
private fun executePaginationTask(requestCallback: PagingRequestHelper.Request.Callback,
from: String,
direction: PaginationDirection) {
val params = PaginationTask.Params(roomId = roomId, val params = PaginationTask.Params(roomId = roomId,
from = token, from = from,
direction = direction, direction = direction,
limit = limit) limit = limit)
paginationTask.configureWith(params) paginationTask.configureWith(params)
.dispatchTo(object : MatrixCallback<TokenChunkEvent> { .enableRetry()
override fun onSuccess(data: TokenChunkEvent) { .dispatchTo(object : MatrixCallback<Boolean> {
override fun onSuccess(data: Boolean) {
requestCallback.recordSuccess() requestCallback.recordSuccess()
} }

View File

@ -2,12 +2,7 @@ package im.vector.matrix.android.internal.session.room.timeline
import arrow.core.Try import arrow.core.Try
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.helper.addAll import im.vector.matrix.android.internal.database.helper.*
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.addStateEvents
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.helper.isUnlinked
import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.create import im.vector.matrix.android.internal.database.query.create
@ -21,12 +16,15 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
fun insertInDb(receivedChunk: TokenChunkEvent, fun insertInDb(receivedChunk: TokenChunkEvent,
roomId: String, roomId: String,
direction: PaginationDirection): Try<Unit> { direction: PaginationDirection): Try<Boolean> {
if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty()) {
return Try.just(false)
}
return monarchy return monarchy
.tryTransactionSync { realm -> .tryTransactionSync { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: throw IllegalStateException("You shouldn't use this method without a room") ?: throw IllegalStateException("You shouldn't use this method without a room")
val nextToken: String? val nextToken: String?
val prevToken: String? val prevToken: String?
@ -46,13 +44,13 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
var currentChunk = if (direction == PaginationDirection.FORWARDS) { var currentChunk = if (direction == PaginationDirection.FORWARDS) {
prevChunk?.apply { this.nextToken = nextToken } prevChunk?.apply { this.nextToken = nextToken }
?: ChunkEntity.create(realm, prevToken, nextToken) ?: ChunkEntity.create(realm, prevToken, nextToken)
} else { } else {
nextChunk?.apply { this.prevToken = prevToken } nextChunk?.apply { this.prevToken = prevToken }
?: ChunkEntity.create(realm, prevToken, nextToken) ?: ChunkEntity.create(realm, prevToken, nextToken)
} }
currentChunk.addAll(receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked()) currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
// Then we merge chunks if needed // Then we merge chunks if needed
if (currentChunk != prevChunk && prevChunk != null) { if (currentChunk != prevChunk && prevChunk != null) {
@ -71,6 +69,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
roomEntity.addOrUpdate(currentChunk) roomEntity.addOrUpdate(currentChunk)
roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = currentChunk.isUnlinked()) roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = currentChunk.isUnlinked())
} }
.map { true }
} }
private fun handleMerge(roomEntity: RoomEntity, private fun handleMerge(roomEntity: RoomEntity,
@ -80,11 +79,11 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
// We always merge the bottom chunk into top chunk, so we are always merging backwards // We always merge the bottom chunk into top chunk, so we are always merging backwards
return if (direction == PaginationDirection.BACKWARDS) { return if (direction == PaginationDirection.BACKWARDS) {
currentChunk.merge(otherChunk, PaginationDirection.BACKWARDS) currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(otherChunk) roomEntity.deleteOnCascade(otherChunk)
currentChunk currentChunk
} else { } else {
otherChunk.merge(currentChunk, PaginationDirection.BACKWARDS) otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(currentChunk) roomEntity.deleteOnCascade(currentChunk)
otherChunk otherChunk
} }

View File

@ -15,11 +15,7 @@ import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync import im.vector.matrix.android.internal.session.sync.model.*
import im.vector.matrix.android.internal.session.sync.model.RoomSync
import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.createObject import io.realm.kotlin.createObject
@ -45,9 +41,9 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy) { private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy) {
val rooms = when (handlingStrategy) { val rooms = when (handlingStrategy) {
is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) } is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) }
is HandlingStrategy.INVITED -> handlingStrategy.data.map { handleInvitedRoom(realm, it.key, it.value) } is HandlingStrategy.INVITED -> handlingStrategy.data.map { handleInvitedRoom(realm, it.key, it.value) }
is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftRoom(it.key, it.value) } is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftRoom(it.key, it.value) }
} }
realm.insertOrUpdate(rooms) realm.insertOrUpdate(rooms)
} }
@ -57,7 +53,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
roomSync: RoomSync): RoomEntity { roomSync: RoomSync): RoomEntity {
val roomEntity = RoomEntity.where(realm, roomId).findFirst() val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId) ?: realm.createObject(roomId)
if (roomEntity.membership == MyMembership.INVITED) { if (roomEntity.membership == MyMembership.INVITED) {
roomEntity.chunks.deleteAllFromRealm() roomEntity.chunks.deleteAllFromRealm()
@ -138,7 +134,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
lastChunk?.isLast = false lastChunk?.isLast = false
chunkEntity.isLast = true chunkEntity.isLast = true
chunkEntity.addAll(eventList, PaginationDirection.FORWARDS, stateIndexOffset) chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset)
return chunkEntity return chunkEntity
} }
@ -147,7 +143,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
roomSummary: RoomSyncSummary) { roomSummary: RoomSyncSummary) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: RoomSummaryEntity(roomId) ?: RoomSummaryEntity(roomId)
if (roomSummary.heroes.isNotEmpty()) { if (roomSummary.heroes.isNotEmpty()) {
roomSummaryEntity.heroes.clear() roomSummaryEntity.heroes.clear()

View File

@ -13,6 +13,7 @@ internal data class ConfigurableTask<PARAMS, RESULT>(
val params: PARAMS, val params: PARAMS,
val callbackThread: TaskThread = TaskThread.MAIN, val callbackThread: TaskThread = TaskThread.MAIN,
val executionThread: TaskThread = TaskThread.IO, val executionThread: TaskThread = TaskThread.IO,
val retryCount: Int = 0,
val callback: MatrixCallback<RESULT> = object : MatrixCallback<RESULT> {} val callback: MatrixCallback<RESULT> = object : MatrixCallback<RESULT> {}
) : Task<PARAMS, RESULT> { ) : Task<PARAMS, RESULT> {
@ -33,10 +34,18 @@ internal data class ConfigurableTask<PARAMS, RESULT>(
return copy(callback = matrixCallback) return copy(callback = matrixCallback)
} }
fun enableRetry(retryCount: Int = Int.MAX_VALUE): ConfigurableTask<PARAMS, RESULT> {
return copy(retryCount = retryCount)
}
fun executeBy(taskExecutor: TaskExecutor): Cancelable { fun executeBy(taskExecutor: TaskExecutor): Cancelable {
return taskExecutor.execute(this) return taskExecutor.execute(this)
} }
override fun toString(): String {
return task.javaClass.name
}
} }

View File

@ -1,9 +1,11 @@
package im.vector.matrix.android.internal.task package im.vector.matrix.android.internal.task
import arrow.core.Try
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.util.CancelableCoroutine import im.vector.matrix.android.internal.util.CancelableCoroutine
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
@ -15,14 +17,36 @@ internal class TaskExecutor(private val coroutineDispatchers: MatrixCoroutineDis
val job = GlobalScope.launch(task.callbackThread.toDispatcher()) { val job = GlobalScope.launch(task.callbackThread.toDispatcher()) {
val resultOrFailure = withContext(task.executionThread.toDispatcher()) { val resultOrFailure = withContext(task.executionThread.toDispatcher()) {
Timber.v("Executing ${task.javaClass} on ${Thread.currentThread().name}") Timber.v("Executing $task on ${Thread.currentThread().name}")
task.execute(task.params) retry(task.retryCount) {
task.execute(task.params)
}
} }
resultOrFailure.fold({ task.callback.onFailure(it) }, { task.callback.onSuccess(it) }) resultOrFailure.fold({ task.callback.onFailure(it) }, { task.callback.onSuccess(it) })
} }
return CancelableCoroutine(job) return CancelableCoroutine(job)
} }
private suspend fun <T> retry(
times: Int = Int.MAX_VALUE,
initialDelay: Long = 100, // 0.1 second
maxDelay: Long = 10_000, // 10 second
factor: Double = 2.0,
block: suspend () -> Try<T>): Try<T> {
var currentDelay = initialDelay
repeat(times - 1) {
val blockResult = block()
if (blockResult.isSuccess()) {
return blockResult
} else {
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
}
return block()
}
private fun TaskThread.toDispatcher() = when (this) { private fun TaskThread.toDispatcher() = when (this) {
TaskThread.MAIN -> coroutineDispatchers.main TaskThread.MAIN -> coroutineDispatchers.main
TaskThread.COMPUTATION -> coroutineDispatchers.computation TaskThread.COMPUTATION -> coroutineDispatchers.computation

View File

@ -1,24 +0,0 @@
package im.vector.matrix.android.internal.util
import arrow.core.Try
import kotlinx.coroutines.delay
suspend fun <T> retry(
times: Int = Int.MAX_VALUE,
initialDelay: Long = 100, // 0.1 second
maxDelay: Long = 10_000, // 10 second
factor: Double = 2.0,
block: suspend () -> Try<T>): Try<T> {
var currentDelay = initialDelay
repeat(times - 1) {
val blockResult = block()
if (blockResult.isSuccess()) {
return blockResult
} else {
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
}
return block()
}

View File

@ -0,0 +1,38 @@
package im.vector.matrix.android.internal.util
import android.arch.lifecycle.LiveData
import android.arch.lifecycle.MediatorLiveData
object LiveDataUtils {
fun <FIRST, SECOND, OUT> combine(firstSource: LiveData<FIRST>,
secondSource: LiveData<SECOND>,
mapper: (FIRST, SECOND) -> OUT): LiveData<OUT> {
return MediatorLiveData<OUT>().apply {
var firstValue: FIRST? = null
var secondValue: SECOND? = null
val valueDispatcher = {
firstValue?.let { safeFirst ->
secondValue?.let { safeSecond ->
val mappedValue = mapper(safeFirst, safeSecond)
postValue(mappedValue)
}
}
}
addSource(firstSource) {
firstValue = it
valueDispatcher()
}
addSource(secondSource) {
secondValue = it
valueDispatcher()
}
}
}
}

View File

@ -379,6 +379,11 @@ public class PagingRequestHelper {
@NonNull @NonNull
private final Throwable[] mErrors; private final Throwable[] mErrors;
public static StatusReport createDefault() {
final Throwable[] errors = {};
return new StatusReport(Status.SUCCESS, Status.SUCCESS, Status.SUCCESS, errors);
}
StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after, StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after,
@NonNull Throwable[] errors) { @NonNull Throwable[] errors) {
this.initial = initial; this.initial = initial;