mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-09 00:29:00 +01:00
Merge branch 'feature/navigation' into develop
This commit is contained in:
commit
5e89627867
@ -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'
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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) }
|
||||||
}
|
}
|
@ -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) })
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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) } }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
@ -1,6 +0,0 @@
|
|||||||
package im.vector.riotredesign.core.utils
|
|
||||||
|
|
||||||
object Constants {
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -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)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()) {
|
||||||
|
@ -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()
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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 -> {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
@ -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()
|
||||||
|
|
||||||
|
}
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
|
|
||||||
|
}
|
@ -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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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())
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
@ -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"
|
||||||
|
@ -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'
|
||||||
|
@ -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)
|
||||||
|
}
|
@ -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))
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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?
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -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>>
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
||||||
|
)
|
@ -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>
|
||||||
|
|
||||||
|
}
|
@ -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>)
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
|
@ -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? {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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())
|
||||||
|
@ -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() }
|
||||||
|
@ -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>()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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) ->
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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,
|
||||||
|
@ -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>()
|
||||||
|
@ -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() })
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user