diff --git a/app/build.gradle b/app/build.gradle index 98e9b05aca..a79d178811 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,6 +7,10 @@ kapt { correctErrorTypes = true } +androidExtensions { + experimental = true +} + android { compileSdkVersion 28 defaultConfig { @@ -43,7 +47,7 @@ configurations.all { strategy -> dependencies { def epoxy_version = "2.19.0" - + def arrow_version = "0.8.0" implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(":matrix-sdk-android") @@ -52,25 +56,34 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.android.support:appcompat-v7:28.0.0' 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.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") kapt "com.airbnb.android:epoxy-processor:$epoxy_version" - implementation "com.airbnb.android:epoxy-paging:$epoxy_version" 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' kapt 'com.github.bumptech.glide:compiler:4.8.0' - //todo remove that implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' - + // DI implementation "org.koin:koin-android:$koin_version" implementation "org.koin:koin-android-scope:$koin_version" + // TESTS testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' diff --git a/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt b/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt index 92daa091f8..7a25d46d1e 100644 --- a/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt +++ b/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt @@ -1,7 +1,9 @@ package im.vector.riotredesign.core.di import android.content.Context +import android.content.Context.MODE_PRIVATE import im.vector.riotredesign.core.resources.LocaleProvider +import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository import org.koin.dsl.module.module class AppModule(private val context: Context) { @@ -12,5 +14,13 @@ class AppModule(private val context: Context) { LocaleProvider(context.resources) } + single { + context.getSharedPreferences("im.vector.riot", MODE_PRIVATE) + } + + single { + RoomSelectionRepository(get()) + } + } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/extensions/Fragment.kt b/app/src/main/java/im/vector/riotredesign/core/extensions/Fragment.kt index 059213126c..3d1bb9d2bb 100644 --- a/app/src/main/java/im/vector/riotredesign/core/extensions/Fragment.kt +++ b/app/src/main/java/im/vector/riotredesign/core/extensions/Fragment.kt @@ -10,6 +10,10 @@ fun Fragment.replaceFragment(fragment: Fragment, frameId: Int) { 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) { childFragmentManager.inTransaction { add(frameId, fragment) } } @@ -18,10 +22,6 @@ fun Fragment.replaceChildFragment(fragment: Fragment, frameId: Int) { 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) { childFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/extensions/LiveData.kt b/app/src/main/java/im/vector/riotredesign/core/extensions/LiveData.kt new file mode 100644 index 0000000000..b012483a4a --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/core/extensions/LiveData.kt @@ -0,0 +1,19 @@ +package im.vector.riotredesign.core.extensions + +import android.arch.lifecycle.LifecycleOwner +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.Observer +import im.vector.riotredesign.core.utils.LiveEvent +import im.vector.riotredesign.core.utils.EventObserver + +inline fun LiveData.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) { + this.observe(owner, Observer { observer(it) }) +} + +inline fun LiveData.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) { + this.observe(owner, Observer { it?.run(observer) }) +} + +inline fun LiveData>.observeEvent(owner: LifecycleOwner, crossinline observer: (T) -> Unit) { + this.observe(owner, EventObserver { it.run(observer) }) +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/platform/OnBackPressed.kt b/app/src/main/java/im/vector/riotredesign/core/platform/OnBackPressed.kt new file mode 100644 index 0000000000..00faa0c76e --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/core/platform/OnBackPressed.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt b/app/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt index 88300a4ee1..178996982a 100644 --- a/app/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt @@ -1,17 +1,26 @@ package im.vector.riotredesign.core.platform +import android.os.Bundle +import android.os.Parcelable import com.airbnb.mvrx.BaseMvRxFragment +import com.airbnb.mvrx.MvRx -abstract class RiotFragment : BaseMvRxFragment() { +abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed { val riotActivity: RiotActivity by lazy { activity as RiotActivity } + override fun onBackPressed(): Boolean { + return false + } override fun invalidate() { //no-ops by default } + protected fun setArguments(args: Parcelable? = null) { + arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } + } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/platform/RiotViewModel.kt b/app/src/main/java/im/vector/riotredesign/core/platform/RiotViewModel.kt new file mode 100644 index 0000000000..da438d3dfc --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/core/platform/RiotViewModel.kt @@ -0,0 +1,7 @@ +package im.vector.riotredesign.core.platform + +import com.airbnb.mvrx.BaseMvRxViewModel +import com.airbnb.mvrx.MvRxState + +abstract class RiotViewModel(initialState: S) + : BaseMvRxViewModel(initialState, debugMode = false) \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/utils/Constants.kt b/app/src/main/java/im/vector/riotredesign/core/utils/Constants.kt deleted file mode 100644 index ade18b3617..0000000000 --- a/app/src/main/java/im/vector/riotredesign/core/utils/Constants.kt +++ /dev/null @@ -1,6 +0,0 @@ -package im.vector.riotredesign.core.utils - -object Constants { - - -} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/utils/FragmentArgumentDelegate.kt b/app/src/main/java/im/vector/riotredesign/core/utils/FragmentArgumentDelegate.kt deleted file mode 100644 index 565c421cae..0000000000 --- a/app/src/main/java/im/vector/riotredesign/core/utils/FragmentArgumentDelegate.kt +++ /dev/null @@ -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 : kotlin.properties.ReadWriteProperty { - - 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 : kotlin.properties.ReadWriteProperty { - - private val innerDelegate = FragmentArgumentDelegate() - - 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)!! - } - -} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/utils/LiveEvent.kt b/app/src/main/java/im/vector/riotredesign/core/utils/LiveEvent.kt new file mode 100644 index 0000000000..153e3c3f2d --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/core/utils/LiveEvent.kt @@ -0,0 +1,40 @@ +package im.vector.riotredesign.core.utils + +import android.arch.lifecycle.Observer + +open class LiveEvent(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(private val onEventUnhandledContent: (T) -> Unit) : Observer> { + override fun onChanged(event: LiveEvent?) { + event?.getContentIfNotHandled()?.let { value -> + onEventUnhandledContent(value) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/MainActivity.kt b/app/src/main/java/im/vector/riotredesign/features/MainActivity.kt index 32258059c9..c4363a861e 100644 --- a/app/src/main/java/im/vector/riotredesign/features/MainActivity.kt +++ b/app/src/main/java/im/vector/riotredesign/features/MainActivity.kt @@ -5,14 +5,12 @@ import im.vector.matrix.android.api.Matrix import im.vector.riotredesign.core.platform.RiotActivity import im.vector.riotredesign.features.home.HomeActivity import im.vector.riotredesign.features.login.LoginActivity -import org.koin.android.ext.android.inject class MainActivity : RiotActivity() { private val authenticator = Matrix.getInstance().authenticator() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val intent = if (authenticator.hasActiveSessions()) { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeActions.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeActions.kt deleted file mode 100644 index 4c27edbec8..0000000000 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeActions.kt +++ /dev/null @@ -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() - -} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt index 43856ec871..da6a67319f 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt @@ -3,28 +3,34 @@ package im.vector.riotredesign.features.home import android.content.Context import android.content.Intent import android.os.Bundle +import android.support.v4.app.FragmentManager import android.support.v4.view.GravityCompat -import android.support.v4.widget.DrawerLayout import android.support.v7.app.ActionBarDrawerToggle import android.support.v7.widget.Toolbar import android.view.Gravity import android.view.MenuItem -import android.view.View +import com.airbnb.mvrx.viewModel import im.vector.riotredesign.R +import im.vector.riotredesign.core.extensions.observeEvent import im.vector.riotredesign.core.extensions.replaceFragment +import im.vector.riotredesign.core.platform.OnBackPressed import im.vector.riotredesign.core.platform.RiotActivity import im.vector.riotredesign.core.platform.ToolbarConfigurable 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 org.koin.android.ext.android.inject 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() + override fun onCreate(savedInstanceState: Bundle?) { loadKoinModules(listOf(HomeModule(this).definition)) + homeNavigator.activity = this super.onCreate(savedInstanceState) setContentView(R.layout.activity_home) if (savedInstanceState == null) { @@ -33,6 +39,14 @@ class HomeActivity : RiotActivity(), HomeNavigator, ToolbarConfigurable { replaceFragment(loadingDetail, R.id.homeDetailFragmentContainer) 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) { @@ -46,7 +60,6 @@ class HomeActivity : RiotActivity(), HomeNavigator, ToolbarConfigurable { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - // Android home android.R.id.home -> { drawerLayout.openDrawer(GravityCompat.START) return true @@ -60,29 +73,31 @@ class HomeActivity : RiotActivity(), HomeNavigator, ToolbarConfigurable { if (drawerLayout.isDrawerOpen(Gravity.LEFT)) { drawerLayout.closeDrawer(Gravity.LEFT) } else { - 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() + val handled = recursivelyDispatchOnBackPressed(supportFragmentManager) + if (!handled) { + super.onBackPressed() } - }) - 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 { fun newIntent(context: Context): Intent { return Intent(context, HomeActivity::class.java) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt new file mode 100644 index 0000000000..06509e8a2c --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivityViewModel.kt @@ -0,0 +1,61 @@ +package im.vector.riotredesign.features.home + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MutableLiveData +import android.support.v4.app.FragmentActivity +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import im.vector.matrix.android.api.Matrix +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.rx.rx +import im.vector.riotredesign.core.platform.RiotViewModel +import im.vector.riotredesign.core.utils.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(state) { + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(activity: FragmentActivity, state: EmptyState): HomeActivityViewModel { + val session = Matrix.getInstance().currentSession + val roomSelectionRepository = activity.get() + return HomeActivityViewModel(state, session, roomSelectionRepository) + } + } + + private val _openRoomLiveData = MutableLiveData>() + val openRoomLiveData: LiveData> + get() = _openRoomLiveData + + init { + val lastSelectedRoom = roomSelectionRepository.lastSelectedRoom() + if (lastSelectedRoom == null) { + getTheFirstRoomWhenAvailable() + } else { + _openRoomLiveData.postValue(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() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt index 1ef0bcb856..a0ccd36152 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeDrawerFragment.kt @@ -5,7 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup 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.features.home.group.GroupListFragment import im.vector.riotredesign.features.home.room.list.RoomListFragment @@ -27,9 +27,9 @@ class HomeDrawerFragment : RiotFragment() { super.onActivityCreated(savedInstanceState) if (savedInstanceState == null) { val groupListFragment = GroupListFragment.newInstance() - replaceFragment(groupListFragment, R.id.groupListFragmentContainer) + replaceChildFragment(groupListFragment, R.id.groupListFragmentContainer) val roomListFragment = RoomListFragment.newInstance() - replaceFragment(roomListFragment, R.id.roomListFragmentContainer) + replaceChildFragment(roomListFragment, R.id.roomListFragmentContainer) } } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index 84e8e8bce9..1c30a4ca01 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -1,5 +1,7 @@ package im.vector.riotredesign.features.home +import im.vector.riotredesign.features.home.group.SelectedGroupHolder +import im.vector.riotredesign.features.home.room.VisibleRoomHolder import im.vector.riotredesign.features.home.room.detail.timeline.MessageItemFactory import im.vector.riotredesign.features.home.room.detail.timeline.TextItemFactory import im.vector.riotredesign.features.home.room.detail.timeline.TimelineDateFormatter @@ -22,13 +24,26 @@ class HomeModule(private val homeActivity: HomeActivity) { TextItemFactory() } - factory { - homeActivity as HomeNavigator + single { + HomeNavigator() } factory { (roomId: String) -> TimelineEventController(roomId, get(), get(), get()) } + single { + SelectedGroupHolder() + } + + single { + VisibleRoomHolder() + } + + single { + HomePermalinkHandler(get()) + } + + } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt index 1b4382f1a9..29cb66bc52 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt @@ -1,7 +1,59 @@ 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) + } + } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomePermalinkHandler.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomePermalinkHandler.kt new file mode 100644 index 0000000000..d70885042a --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomePermalinkHandler.kt @@ -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 -> { + + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeViewModel.kt deleted file mode 100644 index 6d16d1d87a..0000000000 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeViewModel.kt +++ /dev/null @@ -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(initialState) { - - companion object : MvRxViewModelFactory { - - @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) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeViewState.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeViewState.kt deleted file mode 100644 index 01c3758e8c..0000000000 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeViewState.kt +++ /dev/null @@ -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> = Uninitialized, - val directRooms: List = emptyList(), - val groupRooms: List = emptyList(), - val selectedRoom: RoomSummary? = null, - val shouldOpenRoomDetail: Boolean = true, - val asyncGroups: Async> = Uninitialized, - val selectedGroup: GroupSummary? = null -) : MvRxState \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListActions.kt b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListActions.kt new file mode 100644 index 0000000000..92f758a759 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListActions.kt @@ -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() + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt index 16e4bbd817..b4799151a1 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt @@ -6,14 +6,11 @@ import android.view.View import android.view.ViewGroup import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Success -import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.fragmentViewModel import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.riotredesign.R import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.StateView -import im.vector.riotredesign.features.home.HomeActions -import im.vector.riotredesign.features.home.HomeViewModel -import im.vector.riotredesign.features.home.HomeViewState import kotlinx.android.synthetic.main.fragment_group_list.* 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 @@ -40,14 +37,14 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback { viewModel.subscribe { renderState(it) } } - private fun renderState(state: HomeViewState) { + private fun renderState(state: GroupListViewState) { when (state.asyncGroups) { is Incomplete -> renderLoading() is Success -> renderSuccess(state) } } - private fun renderSuccess(state: HomeViewState) { + private fun renderSuccess(state: GroupListViewState) { stateView.state = StateView.State.Content groupController.setData(state) } @@ -57,7 +54,7 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback { } override fun onGroupSelected(groupSummary: GroupSummary) { - viewModel.accept(HomeActions.SelectGroup(groupSummary)) + viewModel.accept(GroupListActions.SelectGroup(groupSummary)) } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt new file mode 100644 index 0000000000..ca88f95a2d --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt @@ -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(initialState) { + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(activity: FragmentActivity, state: GroupListViewState): GroupListViewModel { + val currentSession = Matrix.getInstance().currentSession + val selectedGroupHolder = activity.get() + 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) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewState.kt b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewState.kt new file mode 100644 index 0000000000..6594b5bd5a --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewState.kt @@ -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> = Uninitialized, + val selectedGroup: GroupSummary? = null +) : MvRxState \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt index edd7c19e9f..3d34b98df9 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt @@ -2,12 +2,11 @@ package im.vector.riotredesign.features.home.group import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.group.model.GroupSummary -import im.vector.riotredesign.features.home.HomeViewState class GroupSummaryController(private val callback: Callback? = null -) : TypedEpoxyController() { +) : TypedEpoxyController() { - override fun buildModels(viewState: HomeViewState) { + override fun buildModels(viewState: GroupListViewState) { buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/group/SelectedGroupHolder.kt b/app/src/main/java/im/vector/riotredesign/features/home/group/SelectedGroupHolder.kt new file mode 100644 index 0000000000..5dc350d790 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/group/SelectedGroupHolder.kt @@ -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.empty()) + + fun setSelectedGroup(group: GroupSummary?) { + val optionValue = Option.fromNullable(group) + selectedGroupStream.accept(optionValue) + } + + fun selectedGroup(): Observable> { + return selectedGroupStream.hide() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/VisibleRoomHolder.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/VisibleRoomHolder.kt new file mode 100644 index 0000000000..c17ca4291e --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/VisibleRoomHolder.kt @@ -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() + + fun setVisibleRoom(roomId: String) { + visibleRoomStream.accept(roomId) + } + + fun visibleRoom(): Observable { + return visibleRoomStream.hide() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt new file mode 100644 index 0000000000..89175598d6 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -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() + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 7410c13c6f..d3f47e3d07 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -1,48 +1,47 @@ 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.Parcelable import android.support.v7.widget.LinearLayoutManager import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import im.vector.matrix.android.api.Matrix -import im.vector.matrix.android.api.MatrixCallback -import im.vector.matrix.android.api.permalinks.PermalinkParser -import im.vector.matrix.android.api.session.events.model.EnrichedEvent -import im.vector.matrix.android.api.session.events.model.Event -import im.vector.matrix.android.api.session.room.Room -import im.vector.matrix.android.api.session.room.model.RoomSummary +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel import im.vector.riotredesign.R import im.vector.riotredesign.core.platform.RiotFragment 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.HomePermalinkHandler import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController +import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* import org.koin.android.ext.android.inject import org.koin.core.parameter.parametersOf -import timber.log.Timber + +@Parcelize +data class RoomDetailArgs( + val roomId: String, + val eventId: String? = null +) : Parcelable class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { companion object { - fun newInstance(roomId: String, eventId: String? = null): RoomDetailFragment { + fun newInstance(args: RoomDetailArgs): RoomDetailFragment { return RoomDetailFragment().apply { - this.roomId = roomId - this.eventId = eventId + setArguments(args) } } } - private val currentSession = Matrix.getInstance().currentSession - private var roomId: String by UnsafeFragmentArgumentDelegate() - private var eventId: String? by FragmentArgumentDelegate() - private val timelineEventController by inject { parametersOf(roomId) } - private lateinit var room: Room + private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() + private val roomDetailArgs: RoomDetailArgs by args() + + private val timelineEventController by inject { parametersOf(roomDetailArgs.roomId) } + private val homePermalinkHandler by inject() private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -51,21 +50,15 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - room = currentSession.getRoom(roomId)!! setupRecyclerView() setupToolbar() - room.loadRoomMembersIfNeeded() - room.timeline(eventId).observe(this, Observer { renderEvents(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 { + setupSendButton() + roomDetailViewModel.subscribe { renderState(it) } + } - }) - } - } + override fun onResume() { + super.onResume() + roomDetailViewModel.accept(RoomDetailActions.IsDisplayed) } private fun setupToolbar() { @@ -79,13 +72,43 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) recyclerView.layoutManager = layoutManager + recyclerView.setHasFixedSize(true) timelineEventController.addModelBuildListener { it.dispatchTo(scrollOnNewMessageCallback) } recyclerView.setController(timelineEventController) timelineEventController.callback = this } - private fun renderRoomSummary(roomSummary: RoomSummary?) { - roomSummary?.let { + private fun setupSendButton() { + 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 AvatarRenderer.render(it, toolbarAvatarImageView) if (it.topic.isNotEmpty()) { @@ -97,16 +120,10 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { } } - private fun renderEvents(events: PagedList?) { - scrollOnNewMessageCallback.hasBeenUpdated.set(true) - timelineEventController.timeline = events - } - // TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String) { - val permalinkData = PermalinkParser.parse(url) - Timber.v("Permalink data : $permalinkData") + homePermalinkHandler.launch(url) } } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt new file mode 100644 index 0000000000..d76b4cf438 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -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(initialState) { + + private val room = session.getRoom(initialState.roomId)!! + private val roomId = initialState.roomId + private val eventId = initialState.eventId + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(activity: FragmentActivity, state: RoomDetailViewState): RoomDetailViewModel { + val currentSession = Matrix.getInstance().currentSession + val visibleRoomHolder = activity.get() + 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 {}) + } + + private fun observeRoomSummary() { + room.rx().liveRoomSummary() + .execute { async -> + copy(asyncRoomSummary = async) + } + } + + private fun observeTimeline() { + room.rx().timeline(eventId) + .execute { timelineData -> + copy(asyncTimelineData = timelineData) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt new file mode 100644 index 0000000000..1bd9e3fc8e --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt @@ -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 = Uninitialized, + val asyncTimelineData: Async = Uninitialized +) : MvRxState { + + constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt index aec98efe40..1ffbb50198 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -6,10 +6,10 @@ import java.util.concurrent.atomic.AtomicBoolean class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback { - val hasBeenUpdated = AtomicBoolean(false) + var isLocked = AtomicBoolean(true) 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) } } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItem.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItem.kt index 44a9b2f655..cdb5e35e8f 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItem.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItem.kt @@ -3,19 +3,17 @@ package im.vector.riotredesign.features.home.room.detail.timeline import android.view.View import android.widget.ImageView import android.widget.TextView -import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.riotredesign.R import im.vector.riotredesign.core.epoxy.KotlinModel import im.vector.riotredesign.features.home.AvatarRenderer -data class MessageItem( +class MessageItem( val message: CharSequence? = null, val time: CharSequence? = null, val avatarUrl: String?, val memberName: CharSequence? = null, - val showInformation: Boolean = true, - val onUrlClickedListener: ((url: String) -> Unit)? = null + val showInformation: Boolean = true ) : KotlinModel(R.layout.item_event_message) { private val avatarImageView by bind(R.id.messageAvatarImageView) @@ -25,11 +23,7 @@ data class MessageItem( override fun bind() { messageView.text = message - MatrixLinkify.addLinks(messageView, object : MatrixPermalinkSpan.Callback { - override fun onUrlClicked(url: String) { - onUrlClickedListener?.invoke(url) - } - }) + MatrixLinkify.addLinkMovementMethod(messageView) if (showInformation) { avatarImageView.visibility = View.VISIBLE memberNameView.visibility = View.VISIBLE diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt index 407a4e7a3a..2122e5457f 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt @@ -1,6 +1,10 @@ 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.room.model.MessageContent import org.threeten.bp.LocalDateTime @@ -9,8 +13,8 @@ class MessageItemFactory(private val timelineDateFormatter: TimelineDateFormatte private val messagesDisplayedWithInformation = HashSet() - fun create(event: EnrichedEvent, - nextEvent: EnrichedEvent?, + fun create(event: TimelineEvent, + nextEvent: TimelineEvent?, addDaySeparator: Boolean, date: LocalDateTime, callback: TimelineEventController.Callback? @@ -25,14 +29,24 @@ class MessageItemFactory(private val timelineDateFormatter: TimelineDateFormatte if (addDaySeparator || nextRoomMember != roomMember) { 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) return MessageItem( - message = messageContent.body, + message = message, avatarUrl = roomMember.avatarUrl, showInformation = showInformation, time = timelineDateFormatter.formatMessageHour(date), - memberName = roomMember.displayName ?: event.root.sender, - onUrlClickedListener = { callback?.onUrlClicked(it) } + memberName = roomMember.displayName ?: event.root.sender ) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TextItem.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TextItem.kt index 8848b58bd9..4b827a5a99 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TextItem.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TextItem.kt @@ -4,7 +4,7 @@ import android.widget.TextView import im.vector.riotredesign.R import im.vector.riotredesign.core.epoxy.KotlinModel -data class TextItem( +class TextItem( val text: CharSequence? = null ) : KotlinModel(R.layout.item_event_text) { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TextItemFactory.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TextItemFactory.kt index e123d7f8bc..1ca81862d3 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TextItemFactory.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TextItemFactory.kt @@ -1,10 +1,10 @@ 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 { - fun create(event: EnrichedEvent): TextItem? { + fun create(event: TimelineEvent): TextItem? { val text = "${event.root.type} events are not yet handled" return TextItem(text = text) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 73888167a1..0c32025cfb 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -1,93 +1,84 @@ package im.vector.riotredesign.features.home.room.detail.timeline -import android.arch.paging.PagedList import com.airbnb.epoxy.EpoxyAsyncUtil -import com.airbnb.epoxy.EpoxyController -import im.vector.matrix.android.api.session.events.model.EnrichedEvent +import com.airbnb.epoxy.EpoxyModel +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.room.timeline.TimelineData import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.features.home.LoadingItemModel_ +import im.vector.riotredesign.features.home.room.detail.timeline.paging.PagedListEpoxyController class TimelineEventController(private val roomId: String, private val messageItemFactory: MessageItemFactory, private val textItemFactory: TextItemFactory, private val dateFormatter: TimelineDateFormatter -) : EpoxyController( +) : PagedListEpoxyController( EpoxyAsyncUtil.getAsyncBackgroundHandler(), EpoxyAsyncUtil.getAsyncBackgroundHandler() ) { - init { setFilterDuplicates(true) } - private val pagedListCallback = object : PagedList.Callback() { - override fun onChanged(position: Int, count: Int) { - buildSnapshotList() - } - - override fun onInserted(position: Int, count: Int) { - buildSnapshotList() - } - - override fun onRemoved(position: Int, count: Int) { - buildSnapshotList() - } - } - - private var snapshotList: List? = emptyList() - var timeline: PagedList? = null - set(value) { - field?.removeWeakCallback(pagedListCallback) - field = value - field?.addWeakCallback(null, pagedListCallback) - buildSnapshotList() - } + private var isLoadingForward: Boolean = false + private var isLoadingBackward: Boolean = false + private var hasReachedEnd: Boolean = false var callback: Callback? = null - override fun buildModels() { - buildModels(snapshotList) + fun update(timelineData: TimelineData?) { + 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?) { - if (data.isNullOrEmpty()) { - return + + override fun buildItemModels(currentPosition: Int, items: List): List> { + if (items.isNullOrEmpty()) { + return emptyList() } - for (index in 0 until data.size) { - val event = data[index] ?: continue - val nextEvent = if (index + 1 < data.size) data[index + 1] else null + val epoxyModels = ArrayList>() + val event = items[currentPosition] ?: return emptyList() + val nextEvent = if (currentPosition + 1 < items.size) items[currentPosition + 1] else null - val date = event.root.localDateTime() - val nextDate = nextEvent?.root?.localDateTime() - val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() + val date = event.root.localDateTime() + val nextDate = nextEvent?.root?.localDateTime() + val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() - val item = when (event.root.type) { - EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, addDaySeparator, date, callback) - else -> textItemFactory.create(event) - } - item - ?.onBind { timeline?.loadAround(index) } - ?.id(event.localId) - ?.addTo(this) - - if (addDaySeparator) { - val formattedDay = dateFormatter.formatMessageDay(date) - DaySeparatorItem(formattedDay).id(roomId + formattedDay).addTo(this) - } + val item = when (event.root.type) { + EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, addDaySeparator, date, callback) + else -> textItemFactory.create(event) + } + item?.also { + it.id(event.localId) + epoxyModels.add(it) } - //It's a hack at the moment - val isLastEvent = data.last()?.root?.type == EventType.STATE_ROOM_CREATE + if (addDaySeparator) { + val formattedDay = dateFormatter.formatMessageDay(date) + val daySeparatorItem = DaySeparatorItem(formattedDay).id(roomId + formattedDay) + epoxyModels.add(daySeparatorItem) + } + return epoxyModels + } + + override fun addModels(models: List>) { + LoadingItemModel_() + .id(roomId + "forward_loading_item") + .addIf(isLoadingForward, this) + + super.add(models) + LoadingItemModel_() .id(roomId + "backward_loading_item") - .addIf(!isLastEvent, this) + .addIf(!hasReachedEnd, this) } - private fun buildSnapshotList() { - snapshotList = timeline?.snapshot() ?: emptyList() - requestModelBuild() - } interface Callback { fun onUrlClicked(url: String) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListEpoxyController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListEpoxyController.kt new file mode 100644 index 0000000000..b0bdd5a903 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListEpoxyController.kt @@ -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( + /** + * 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 = DEFAULT_ITEM_DIFF_CALLBACK as DiffUtil.ItemCallback +) : 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? = 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>) { + 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): List> + + /** + * 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?) { + 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() { + override fun areItemsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem + + override fun areContentsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem + } + } +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListModelCache.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListModelCache.kt new file mode 100644 index 0000000000..1966ecee54 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListModelCache.kt @@ -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( + private val modelBuilder: (itemIndex: Int, items: List) -> List>, + private val rebuildCallback: () -> Unit, + private val itemDiffCallback: DiffUtil.ItemCallback, + 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, 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( + updateCallback, + AsyncDifferConfig.Builder( + 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?) { + asyncDiffer.submitList(pagedList) + } + + fun getModels(): List> { + 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>) { + epoxyModels.forEach { + modelCache[it] = itemPosition + } + } + + private fun buildModel(pos: Int) { + if (pos >= asyncDiffer.currentList?.size ?: 0) { + return + } + modelBuilder(pos, asyncDiffer.currentList as List).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)) + } + } + } +} diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt new file mode 100644 index 0000000000..17b8ec9b94 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt @@ -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() + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt index 774841e340..f66ee63011 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt @@ -13,10 +13,7 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotredesign.R import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.StateView -import im.vector.riotredesign.features.home.HomeActions 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 org.koin.android.ext.android.inject @@ -29,7 +26,7 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback { } private val homeNavigator by inject() - private val viewModel: HomeViewModel by activityViewModel() + private val homeViewModel: RoomListViewModel by activityViewModel() private lateinit var roomController: RoomSummaryController override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -41,22 +38,18 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback { roomController = RoomSummaryController(this) stateView.contentView = epoxyRecyclerView epoxyRecyclerView.setController(roomController) - viewModel.subscribe { renderState(it) } + homeViewModel.subscribe { renderState(it) } } - private fun renderState(state: HomeViewState) { + private fun renderState(state: RoomListViewState) { when (state.asyncRooms) { is Incomplete -> renderLoading() - is Success -> renderSuccess(state) - is Fail -> renderFailure(state.asyncRooms.error) - } - if (state.shouldOpenRoomDetail && state.selectedRoom != null) { - homeNavigator.openRoomDetail(state.selectedRoom.roomId) - viewModel.accept(HomeActions.RoomDisplayed) + is Success -> renderSuccess(state) + is Fail -> renderFailure(state.asyncRooms.error) } } - private fun renderSuccess(state: HomeViewState) { + private fun renderSuccess(state: RoomListViewState) { if (state.asyncRooms().isNullOrEmpty()) { stateView.state = StateView.State.Empty(getString(R.string.room_list_empty)) } else { @@ -72,13 +65,14 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback { private fun renderFailure(error: Throwable) { val message = when (error) { is Failure.NetworkConnection -> getString(R.string.error_no_network) - else -> getString(R.string.error_common) + else -> getString(R.string.error_common) } stateView.state = StateView.State.Error(message) } override fun onRoomSelected(room: RoomSummary) { - viewModel.accept(HomeActions.SelectRoom(room)) + homeViewModel.accept(RoomListActions.SelectRoom(room)) + homeNavigator.openRoomDetail(room.roomId, null) } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt new file mode 100644 index 0000000000..40a882f89f --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt @@ -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(initialState) { + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(activity: FragmentActivity, state: RoomListViewState): RoomListViewModel { + val currentSession = Matrix.getInstance().currentSession + val roomSelectionRepository = activity.get() + val selectedGroupHolder = activity.get() + val visibleRoomHolder = activity.get() + 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, Option, 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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt new file mode 100644 index 0000000000..2d183a55fe --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt @@ -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 = Uninitialized, + val selectedRoomId: String? = null +) : MvRxState + +data class RoomSummaries( + val directRooms: List, + val groupRooms: List +) + +fun RoomSummaries?.isNullOrEmpty(): Boolean { + return this == null || (directRooms.isEmpty() && groupRooms.isEmpty()) +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSelectionRepository.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSelectionRepository.kt new file mode 100644 index 0000000000..f2a7af753b --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSelectionRepository.kt @@ -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() + } + +} + diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt index 6aeb893c6d..30cdf7f767 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt @@ -2,17 +2,15 @@ package im.vector.riotredesign.features.home.room.list import com.airbnb.epoxy.TypedEpoxyController import im.vector.matrix.android.api.session.room.model.RoomSummary -import im.vector.riotredesign.features.home.HomeViewState class RoomSummaryController(private val callback: Callback? = null -) : TypedEpoxyController() { - +) : TypedEpoxyController() { private var isDirectRoomsExpanded = true private var isGroupRoomsExpanded = true - override fun buildModels(viewState: HomeViewState) { - + override fun buildModels(viewState: RoomListViewState) { + val roomSummaries = viewState.asyncRooms() RoomCategoryItem( title = "DIRECT MESSAGES", isExpanded = isDirectRoomsExpanded, @@ -25,16 +23,7 @@ class RoomSummaryController(private val callback: Callback? = null .addTo(this) if (isDirectRoomsExpanded) { - val filteredDirectRooms = viewState.directRooms.filter { - if (viewState.selectedGroup == null) { - true - } else { - it.otherMemberIds - .intersect(viewState.selectedGroup.userIds) - .isNotEmpty() - } - } - buildRoomModels(filteredDirectRooms, viewState.selectedRoom) + buildRoomModels(roomSummaries?.directRooms ?: emptyList(), viewState.selectedRoomId) } RoomCategoryItem( @@ -49,17 +38,14 @@ class RoomSummaryController(private val callback: Callback? = null .addTo(this) if (isGroupRoomsExpanded) { - val filteredGroupRooms = viewState.groupRooms.filter { - viewState.selectedGroup?.roomIds?.contains(it.roomId) ?: true - } - buildRoomModels(filteredGroupRooms, viewState.selectedRoom) + buildRoomModels(roomSummaries?.groupRooms ?: emptyList(), viewState.selectedRoomId) } } - private fun buildRoomModels(summaries: List, selected: RoomSummary?) { + private fun buildRoomModels(summaries: List, selectedRoomId: String?) { summaries.forEach { roomSummary -> - val isSelected = roomSummary.roomId == selected?.roomId + val isSelected = roomSummary.roomId == selectedRoomId RoomSummaryItem( roomName = roomSummary.displayName, avatarUrl = roomSummary.avatarUrl, diff --git a/app/src/main/res/layout/item_event_message.xml b/app/src/main/res/layout/item_event_message.xml index 9ed6fe9be3..5b2cde0ab5 100644 --- a/app/src/main/res/layout/item_event_message.xml +++ b/app/src/main/res/layout/item_event_message.xml @@ -27,7 +27,6 @@ android:layout_marginEnd="8dp" android:ellipsize="end" android:maxLines="1" - android:paddingBottom="8dp" android:textSize="15sp" app:layout_constraintBottom_toTopOf="@+id/toolbarSubtitleView" app:layout_constraintEnd_toStartOf="@+id/messageTimeView" diff --git a/matrix-sdk-android-rx/build.gradle b/matrix-sdk-android-rx/build.gradle index 8f51158f89..1b5ffeed44 100644 --- a/matrix-sdk-android-rx/build.gradle +++ b/matrix-sdk-android-rx/build.gradle @@ -39,6 +39,9 @@ dependencies { implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' + // Paging + implementation "android.arch.paging:runtime:1.0.1" + testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' diff --git a/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt new file mode 100644 index 0000000000..510d7e018e --- /dev/null +++ b/matrix-sdk-android-rx/src/main/java/im/vector/matrix/rx/RxRoom.kt @@ -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 { + return room.roomSummary.asObservable() + } + + fun timeline(eventId: String? = null): Observable { + return room.timeline(eventId).asObservable() + } + +} + +fun Room.rx(): RxRoom { + return RxRoom(this) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticatorTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticatorTest.kt index e0c1c0f534..fb0c6853de 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticatorTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/auth/AuthenticatorTest.kt @@ -6,7 +6,6 @@ import android.support.test.runner.AndroidJUnit4 import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest 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.internal.auth.AuthModule import im.vector.matrix.android.internal.di.MatrixModule @@ -26,7 +25,7 @@ internal class AuthenticatorTest : InstrumentedTest, KoinTest { init { Monarchy.init(context()) - val matrixModule = MatrixModule(MatrixOptions(context())).definition + val matrixModule = MatrixModule(context()).definition val networkModule = NetworkModule().definition val authModule = AuthModule().definition loadKoinModules(listOf(matrixModule, networkModule, authModule)) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt index 137ec790e1..7460f5b535 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/ChunkEntityTest.kt @@ -39,7 +39,7 @@ internal class ChunkEntityTest : InstrumentedTest { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() val fakeEvent = createFakeEvent(false) - chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.events.size shouldEqual 1 } } @@ -49,8 +49,8 @@ internal class ChunkEntityTest : InstrumentedTest { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() val fakeEvent = createFakeEvent(false) - chunk.add(fakeEvent, PaginationDirection.FORWARDS) - chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) + chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.events.size shouldEqual 1 } } @@ -60,7 +60,7 @@ internal class ChunkEntityTest : InstrumentedTest { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() val fakeEvent = createFakeEvent(true) - chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1 } } @@ -70,7 +70,7 @@ internal class ChunkEntityTest : InstrumentedTest { monarchy.runTransactionSync { realm -> val chunk: ChunkEntity = realm.createObject() val fakeEvent = createFakeEvent(false) - chunk.add(fakeEvent, PaginationDirection.FORWARDS) + chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS) chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0 } } @@ -81,7 +81,7 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk: ChunkEntity = realm.createObject() val fakeEvents = createFakeListOfEvents(30) val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size - chunk.addAll(fakeEvents, PaginationDirection.FORWARDS) + chunk.addAll("roomId", fakeEvents, PaginationDirection.FORWARDS) chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual numberOfStateEvents } } @@ -94,7 +94,7 @@ internal class ChunkEntityTest : InstrumentedTest { val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size val lastIsState = fakeEvents.last().isStateEvent() 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 } } @@ -104,21 +104,38 @@ internal class ChunkEntityTest : InstrumentedTest { monarchy.runTransactionSync { realm -> val chunk1: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject() - chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS) - chunk1.merge(chunk2, PaginationDirection.BACKWARDS) + chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS) + chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) 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 fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() { monarchy.runTransactionSync { realm -> val chunk1: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject() - chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false) - chunk1.merge(chunk2, PaginationDirection.BACKWARDS) + chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = false) + chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) chunk1.isUnlinked().shouldBeFalse() } } @@ -128,9 +145,9 @@ internal class ChunkEntityTest : InstrumentedTest { monarchy.runTransactionSync { realm -> val chunk1: ChunkEntity = realm.createObject() val chunk2: ChunkEntity = realm.createObject() - chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk1.merge(chunk2, PaginationDirection.BACKWARDS) + chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) chunk1.isUnlinked().shouldBeTrue() } } @@ -142,9 +159,9 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk2: ChunkEntity = realm.createObject() val prevToken = "prev_token" chunk1.prevToken = prevToken - chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk1.merge(chunk2, PaginationDirection.FORWARDS) + chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk1.merge("roomId", chunk2, PaginationDirection.FORWARDS) chunk1.prevToken shouldEqual prevToken } } @@ -156,9 +173,9 @@ internal class ChunkEntityTest : InstrumentedTest { val chunk2: ChunkEntity = realm.createObject() val nextToken = "next_token" chunk1.nextToken = nextToken - chunk1.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk2.addAll(createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) - chunk1.merge(chunk2, PaginationDirection.BACKWARDS) + chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS, isUnlinked = true) + chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS) chunk1.nextToken shouldEqual nextToken } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt index 2385d79405..c7279bf2d7 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/RoomDataHelper.kt @@ -34,7 +34,7 @@ object RoomDataHelper { prevToken = Random.nextLong(System.currentTimeMillis()).toString() isLast = true } - chunkEntity.addAll(eventList, PaginationDirection.FORWARDS) + chunkEntity.addAll("roomId", eventList, PaginationDirection.FORWARDS) roomEntity.addOrUpdate(chunkEntity) } } diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt index fc8731abf0..11555dd281 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineHolderTest.kt @@ -6,10 +6,11 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.LiveDataTestObserver import im.vector.matrix.android.api.thread.MainThreadExecutor -import im.vector.matrix.android.internal.task.TaskExecutor -import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineHolder +import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor +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.TokenChunkEventPersistor +import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.PagingRequestHelper import im.vector.matrix.android.testCoroutineDispatchers import io.realm.Realm @@ -43,17 +44,17 @@ internal class TimelineHolderTest : InstrumentedTest { val boundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, PagingRequestHelper(MainThreadExecutor())) 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()) timelineObserver.awaitNextValue().assertHasValue() - var pagedList = timelineObserver.value() - pagedList.size shouldEqual 30 - (0 until pagedList.size).map { - pagedList.loadAround(it) + var timelineData = timelineObserver.value() + timelineData.events.size shouldEqual 30 + (0 until timelineData.events.size).map { + timelineData.events.loadAround(it) } timelineObserver.awaitNextValue().assertHasValue() - pagedList = timelineObserver.value() - pagedList.size shouldEqual 60 + timelineData = timelineObserver.value() + timelineData.events.size shouldEqual 60 } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt index 714ac0f7dc..467329c8e1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/Matrix.kt @@ -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.util.BackgroundDetectionObserver import org.koin.standalone.inject +import java.util.concurrent.atomic.AtomicBoolean class Matrix private constructor(context: Context) : MatrixKoinComponent { @@ -40,9 +41,12 @@ class Matrix private constructor(context: Context) : MatrixKoinComponent { companion object { private lateinit var instance: Matrix + private val isInit = AtomicBoolean(false) internal fun initialize(context: Context) { - instance = Matrix(context.applicationContext) + if (isInit.compareAndSet(false, true)) { + instance = Matrix(context.applicationContext) + } } fun getInstance(): Matrix { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt index 66c1d32ed2..d30a710984 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/MatrixLinkify.kt @@ -58,7 +58,7 @@ object MatrixLinkify { } - private fun addLinkMovementMethod(textView: TextView) { + fun addLinkMovementMethod(textView: TextView) { val movementMethod = textView.movementMethod if (movementMethod == null || movementMethod !is LinkMovementMethod) { if (textView.linksClickable) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt index cd4af40182..d5652ef15b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/Session.kt @@ -15,4 +15,12 @@ interface Session : RoomService, GroupService { @MainThread fun close() + fun addListener(listener: Listener) + + fun removeListener(listener: Listener) + + // Not used at the moment + interface Listener + + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/interceptor/EnrichedEventInterceptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/interceptor/EnrichedEventInterceptor.kt deleted file mode 100644 index c781f29b68..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/interceptor/EnrichedEventInterceptor.kt +++ /dev/null @@ -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) - -} - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/interceptor/TimelineEventInterceptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/interceptor/TimelineEventInterceptor.kt new file mode 100644 index 0000000000..cf7df317de --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/interceptor/TimelineEventInterceptor.kt @@ -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) + +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EnrichedEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/TimelineEvent.kt similarity index 95% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EnrichedEvent.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/TimelineEvent.kt index 5cf64db9c2..97a5eced2c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EnrichedEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/TimelineEvent.kt @@ -2,7 +2,7 @@ package im.vector.matrix.android.api.session.events.model import im.vector.matrix.android.api.session.room.model.RoomMember -data class EnrichedEvent( +data class TimelineEvent( val root: Event, val localId: String, val roomMember: RoomMember? diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index 412131f171..8e17a0a181 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -3,9 +3,10 @@ package im.vector.matrix.android.api.session.room import android.arch.lifecycle.LiveData 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.timeline.TimelineService import im.vector.matrix.android.api.util.Cancelable -interface Room : TimelineHolder, SendService { +interface Room : TimelineService, SendService { val roomId: String diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt index 79170bdbd8..3f02d2032e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/RoomService.kt @@ -7,15 +7,6 @@ interface RoomService { fun getRoom(roomId: String): Room? - fun getAllRooms(): List - - fun liveRooms(): LiveData> - fun liveRoomSummaries(): LiveData> - fun lastSelectedRoom(): RoomSummary? - - fun saveLastSelectedRoom(roomSummary: RoomSummary) - - } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/TimelineHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/TimelineHolder.kt deleted file mode 100644 index fc01a47b59..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/TimelineHolder.kt +++ /dev/null @@ -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> - -} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineData.kt new file mode 100644 index 0000000000..01727cd287 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineData.kt @@ -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, + val isLoadingForward: Boolean = false, + val isLoadingBackward: Boolean = false +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt new file mode 100644 index 0000000000..d1069dad08 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineService.kt @@ -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 + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt index bc81a709ba..5f01f433d3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/RealmLiveEntityObserver.kt @@ -4,7 +4,6 @@ import android.arch.lifecycle.LiveData import android.arch.lifecycle.Observer import com.zhuinden.monarchy.Monarchy import io.realm.RealmObject -import io.realm.RealmResults import java.util.concurrent.atomic.AtomicBoolean internal interface LiveEntityObserver { @@ -39,11 +38,15 @@ internal abstract class RealmLiveEntityObserver(protected val m if (changeSet == null) { return } - val updateIndexes = changeSet.orderedCollectionChangeSet.changes + changeSet.orderedCollectionChangeSet.insertions + val insertionIndexes = changeSet.orderedCollectionChangeSet.insertions + val updateIndexes = changeSet.orderedCollectionChangeSet.changes 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, updateIndexes: IntArray, deletionIndexes: IntArray) + abstract fun process(inserted: List, updated: List, deleted: List) } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index f8d8207a3c..514a015825 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -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.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.EventEntity 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 internal fun ChunkEntity.deleteOnCascade() { + assertIsManaged() this.events.deleteAllFromRealm() this.deleteFromRealm() } // By default if a chunk is empty we consider it unlinked internal fun ChunkEntity.isUnlinked(): Boolean { + assertIsManaged() 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) { - + assertIsManaged() val isChunkToMergeUnlinked = chunkToMerge.isUnlinked() val isCurrentChunkUnlinked = this.isUnlinked() val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked @@ -40,17 +45,18 @@ internal fun ChunkEntity.merge(chunkToMerge: ChunkEntity, eventsToMerge = chunkToMerge.events } eventsToMerge.forEach { - add(it, direction, isUnlinked = isUnlinked) + add(roomId, it.asDomain(), direction, isUnlinked = isUnlinked) } } -internal fun ChunkEntity.addAll(events: List, +internal fun ChunkEntity.addAll(roomId: String, + events: List, direction: PaginationDirection, stateIndexOffset: Int = 0, isUnlinked: Boolean = false) { - + assertIsManaged() 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 } } -internal fun ChunkEntity.add(event: Event, +internal fun ChunkEntity.add(roomId: String, + event: Event, direction: PaginationDirection, stateIndexOffset: Int = 0, isUnlinked: Boolean = false) { - add(event.asEntity(), direction, stateIndexOffset, isUnlinked) -} -internal fun ChunkEntity.add(eventEntity: EventEntity, - direction: PaginationDirection, - 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)) { + assertIsManaged() + if (event.eventId.isNullOrEmpty() || events.fastContains(event.eventId)) { return } - var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset) - if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(eventEntity.type)) { + if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.type)) { currentStateIndex += 1 } else if (direction == PaginationDirection.BACKWARDS && events.isNotEmpty()) { val lastEventType = events.last()?.type ?: "" @@ -86,13 +83,18 @@ internal fun ChunkEntity.add(eventEntity: EventEntity, currentStateIndex -= 1 } } - - eventEntity.stateIndex = currentStateIndex - eventEntity.isUnlinked = isUnlinked + val eventEntity = event.toEntity(roomId) + eventEntity.updateWith(currentStateIndex, isUnlinked) val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size 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 { return when (direction) { PaginationDirection.FORWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING).findFirst()?.stateIndex diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt index 4df958b467..ea4c5a7e68 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/RoomEntityHelper.kt @@ -1,7 +1,8 @@ package im.vector.matrix.android.internal.database.helper 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.RoomEntity @@ -28,9 +29,8 @@ internal fun RoomEntity.addStateEvents(stateEvents: List, if (event.eventId == null) { return@forEach } - val eventEntity = event.asEntity() - eventEntity.stateIndex = stateIndex - eventEntity.isUnlinked = isUnlinked + val eventEntity = event.toEntity(roomId) + eventEntity.updateWith(stateIndex, isUnlinked) untimelinedStateEvents.add(eventEntity) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt index 5d1669b9a5..d46d0f93ff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventMapper.kt @@ -8,9 +8,10 @@ import im.vector.matrix.android.internal.database.model.EventEntity internal object EventMapper { - fun map(event: Event): EventEntity { + fun map(event: Event, roomId: String): EventEntity { val eventEntity = EventEntity() eventEntity.eventId = event.eventId ?: "" + eventEntity.roomId = event.roomId ?: roomId eventEntity.content = ContentMapper.map(event.content) val resolvedPrevContent = event.prevContent ?: event.unsignedData?.prevContent eventEntity.prevContent = ContentMapper.map(resolvedPrevContent) @@ -32,19 +33,24 @@ internal object EventMapper { originServerTs = eventEntity.originServerTs, sender = eventEntity.sender, stateKey = eventEntity.stateKey, - roomId = null, + roomId = eventEntity.roomId, unsignedData = UnsignedData(eventEntity.age), redacts = eventEntity.redacts ) } +} +internal fun EventEntity.updateWith(stateIndex: Int, isUnlinked: Boolean) { + this.stateIndex = stateIndex + this.isUnlinked = isUnlinked } internal fun EventEntity.asDomain(): Event { return EventMapper.map(this) } -internal fun Event.asEntity(): EventEntity { - return EventMapper.map(this) +internal fun Event.toEntity(roomId: String): EventEntity { + return EventMapper.map(this, roomId) } + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt index 46451ee9e2..2ac7c0ff67 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventEntity.kt @@ -8,6 +8,7 @@ import java.util.* internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(), var eventId: String = "", + var roomId: String = "", var type: String = "", var content: String? = null, var prevContent: String? = null, @@ -27,9 +28,7 @@ internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUI BOTH } - companion object { - const val DEFAULT_STATE_INDEX = Int.MIN_VALUE - } + companion object @LinkingObjects("events") val chunk: RealmResults? = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index de2a3b0414..e3a12a53d8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -5,16 +5,15 @@ import io.realm.RealmObject import io.realm.annotations.PrimaryKey internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "", - var displayName: String? = "", - var avatarUrl: String? = "", - var topic: String? = "", - var lastMessage: EventEntity? = null, - var heroes: RealmList = RealmList(), - var joinedMembersCount: Int? = 0, - var invitedMembersCount: Int? = 0, - var isDirect: Boolean = false, - var isLatestSelected: Boolean = false, - var otherMemberIds: RealmList = RealmList() + var displayName: String? = "", + var avatarUrl: String? = "", + var topic: String? = "", + var lastMessage: EventEntity? = null, + var heroes: RealmList = RealmList(), + var joinedMembersCount: Int? = 0, + var invitedMembersCount: Int? = 0, + var isDirect: Boolean = false, + var otherMemberIds: RealmList = RealmList() ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt index 2035af90d3..cf9cea4386 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ChunkEntityQueries.kt @@ -37,6 +37,10 @@ internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds .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 { return realm.createObject().apply { this.prevToken = prevToken diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index 7b5822876d..2a723512f0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -1,10 +1,8 @@ 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.LinkFilterMode.* 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.RealmList import io.realm.RealmQuery @@ -22,22 +20,19 @@ internal fun EventEntity.Companion.where(realm: Realm, linkFilterMode: EventEntity.LinkFilterMode = LINKED_ONLY): RealmQuery { val query = realm.where() if (roomId != null) { - query.beginGroup() - .equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.ROOM}.${RoomEntityFields.ROOM_ID}", roomId) - .or() - .equalTo("${EventEntityFields.ROOM}.${RoomEntityFields.ROOM_ID}", roomId) - .endGroup() + query.equalTo(EventEntityFields.ROOM_ID, roomId) } if (type != null) { query.equalTo(EventEntityFields.TYPE, type) } 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) - BOTH -> query + BOTH -> query } } + internal fun RealmQuery.next(from: Int? = null, strict: Boolean = true): EventEntity? { if (from != null) { if (strict) { @@ -69,7 +64,6 @@ internal fun RealmList.find(eventId: String): EventEntity? { return this.where().equalTo(EventEntityFields.EVENT_ID, eventId).findFirst() } -internal fun RealmList. - fastContains(eventId: String): Boolean { +internal fun RealmList.fastContains(eventId: String): Boolean { return this.find(eventId) != null } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt index 90cc068412..3641842015 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/RoomSummaryEntityQueries.kt @@ -13,9 +13,3 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n } return query } - -internal fun RoomSummaryEntity.Companion.lastSelected(realm: Realm): RoomSummaryEntity? { - return realm.where() - .equalTo(RoomSummaryEntityFields.IS_LATEST_SELECTED, true) - .findFirst() -} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 7047b1dcc0..0c9356ed8c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -31,6 +31,7 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi private lateinit var scope: Scope private val liveEntityUpdaters by inject>() + private val sessionListeners by inject() private val roomService by inject() private val groupService by inject() private val syncThread by inject() @@ -62,6 +63,14 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi isOpen = false } + override fun addListener(listener: Session.Listener) { + sessionListeners.addListener(listener) + } + + override fun removeListener(listener: Session.Listener) { + sessionListeners.removeListener(listener) + } + // ROOM SERVICE override fun getRoom(roomId: String): Room? { @@ -69,31 +78,12 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi return roomService.getRoom(roomId) } - override fun getAllRooms(): List { - assert(isOpen) - return roomService.getAllRooms() - } - - override fun liveRooms(): LiveData> { - assert(isOpen) - return roomService.liveRooms() - } override fun liveRoomSummaries(): LiveData> { assert(isOpen) return roomService.liveRoomSummaries() } - override fun lastSelectedRoom(): RoomSummary? { - assert(isOpen) - return roomService.lastSelectedRoom() - } - - override fun saveLastSelectedRoom(roomSummary: RoomSummary) { - assert(isOpen) - roomService.saveLastSelectedRoom(roomSummary) - } - // GROUP SERVICE override fun getGroup(groupId: String): Group? { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt new file mode 100644 index 0000000000..92c87ee46a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionListeners.kt @@ -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() + + fun addListener(listener: Session.Listener) { + listeners.add(listener) + } + + fun removeListener(listener: Session.Listener) { + listeners.remove(listener) + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 41644ad8cc..d36cbcbf0a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -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.room.RoomService 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.GroupSummaryUpdater 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.members.RoomDisplayNameResolver 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 io.realm.RealmConfiguration import org.koin.dsl.module.module @@ -75,7 +75,10 @@ internal class SessionModule(private val sessionParams: SessionParams) { } scope(DefaultSession.SCOPE) { + SessionListeners() + } + scope(DefaultSession.SCOPE) { val roomSummaryUpdater = RoomSummaryUpdater(get(), get(), get(), get(), sessionParams.credentials) val groupSummaryUpdater = GroupSummaryUpdater(get()) val eventsPruner = EventsPruner(get()) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt index fc5c0d1e53..861e964033 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GetGroupDataWorker.kt @@ -15,9 +15,7 @@ internal class GetGroupDataWorker(context: Context, @JsonClass(generateAdapter = true) internal data class Params( - val groupIds: List, - val updateIndexes: List, - val deletionIndexes: List + val groupIds: List ) private val getGroupDataTask by inject() @@ -26,8 +24,7 @@ internal class GetGroupDataWorker(context: Context, val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() - val results = params.updateIndexes.map { index -> - val groupId = params.groupIds[index] + val results = params.groupIds.map { groupId -> fetchGroupData(groupId) } val isSuccessful = results.none { it.isFailure() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt index be70a16038..7d5d4f3d56 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupSummaryUpdater.kt @@ -1,12 +1,15 @@ 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 im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.model.GroupEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.util.WorkerParamsFactory -import io.realm.RealmResults private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER" @@ -19,9 +22,9 @@ internal class GroupSummaryUpdater(monarchy: Monarchy .setRequiredNetworkType(NetworkType.CONNECTED) .build() - override fun process(results: RealmResults, updateIndexes: IntArray, deletionIndexes: IntArray) { - val groupIds = results.map { it.groupId } - val getGroupDataWorkerParams = GetGroupDataWorker.Params(groupIds, updateIndexes.toList(), deletionIndexes.toList()) + override fun process(inserted: List, updated: List, deleted: List) { + val newGroupIds = inserted.map { it.groupId } + val getGroupDataWorkerParams = GetGroupDataWorker.Params(newGroupIds) val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams) val sendWork = OneTimeWorkRequestBuilder() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 44ff5e80c4..2359c30190 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -2,26 +2,24 @@ package im.vector.matrix.android.internal.session.room import android.arch.lifecycle.LiveData import android.arch.lifecycle.Transformations -import android.arch.paging.PagedList import com.zhuinden.monarchy.Monarchy 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.room.Room 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.MyMembership 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.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.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.query.where 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.sync.SyncTokenStore import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import org.koin.core.parameter.parametersOf @@ -33,9 +31,8 @@ internal data class DefaultRoom( ) : Room, MatrixKoinComponent { private val loadRoomMembersTask by inject() - private val syncTokenStore by inject() private val monarchy by inject() - private val timelineHolder by inject { parametersOf(roomId) } + private val timelineService by inject { parametersOf(roomId) } private val sendService by inject { parametersOf(roomId) } private val taskExecutor by inject() @@ -50,25 +47,13 @@ internal data class DefaultRoom( } } - override fun timeline(eventId: String?): LiveData> { - return timelineHolder.timeline(eventId) + override fun timeline(eventId: String?): LiveData { + return timelineService.timeline(eventId) } override fun loadRoomMembersIfNeeded(): Cancelable { - return if (areAllMembersLoaded()) { - object : Cancelable {} - } 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 + val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE) + return loadRoomMembersTask.configureWith(params).executeBy(taskExecutor) } @@ -76,4 +61,5 @@ internal data class DefaultRoom( return sendService.sendTextMessage(text, callback) } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 90c1065960..e9b0aac43c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -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.RoomSummaryEntity 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 internal class DefaultRoomService(private val monarchy: Monarchy) : RoomService { - override fun getAllRooms(): List { - var rooms: List = emptyList() - monarchy.doWithRealm { realm -> - rooms = RoomEntity.where(realm).findAll().map { it.asDomain() } - } - return rooms - } - override fun getRoom(roomId: String): Room? { var room: Room? = null monarchy.doWithRealm { realm -> @@ -30,34 +21,10 @@ internal class DefaultRoomService(private val monarchy: Monarchy) : RoomService return room } - override fun liveRooms(): LiveData> { - return monarchy.findAllMappedWithChanges( - { realm -> RoomEntity.where(realm) }, - { it.asDomain() } - ) - } - override fun liveRoomSummaries(): LiveData> { return monarchy.findAllMappedWithChanges( { realm -> RoomSummaryEntity.where(realm).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) }, { 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 - } - } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 3ecf6b779e..eaee079849 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -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.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.timeline.TimelineService 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.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.timeline.DefaultGetContextOfEventTask 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.PaginationTask import im.vector.matrix.android.internal.session.room.timeline.TimelineBoundaryCallback @@ -32,7 +32,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - DefaultLoadRoomMembersTask(get(), get()) as LoadRoomMembersTask + DefaultLoadRoomMembersTask(get(), get(), get()) as LoadRoomMembersTask } scope(DefaultSession.SCOPE) { @@ -56,7 +56,7 @@ class RoomModule { val helper = PagingRequestHelper(Executors.newSingleThreadExecutor()) val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, get(), get(), get(), helper) 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) -> diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index 37fe93c198..c0e9863c74 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -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.RoomMembers import io.realm.Realm -import io.realm.RealmResults import io.realm.kotlin.createObject internal class RoomSummaryUpdater(monarchy: Monarchy, @@ -29,13 +28,10 @@ internal class RoomSummaryUpdater(monarchy: Monarchy, override val query = Monarchy.Query { RoomEntity.where(it) } - override fun process(results: RealmResults, updateIndexes: IntArray, deletionIndexes: IntArray) { - val rooms = results.map { it.asDomain() } + override fun process(inserted: List, updated: List, deleted: List) { + val rooms = (inserted + updated).map { it.asDomain() } monarchy.writeAsync { realm -> - updateIndexes.forEach { index -> - val data = rooms[index] - updateRoom(realm, data) - } + rooms.forEach { updateRoom(realm, it) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt index aa0160bfe8..e62014b45e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/LoadRoomMembersTask.kt @@ -3,33 +3,40 @@ package im.vector.matrix.android.internal.session.room.members import arrow.core.Try import com.zhuinden.monarchy.Monarchy 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.model.RoomEntity import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.network.executeRequest 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 internal interface LoadRoomMembersTask : Task { data class Params( val roomId: String, - val streamToken: String?, val excludeMembership: Membership? = null ) } internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, - private val monarchy: Monarchy + private val monarchy: Monarchy, + private val syncTokenStore: SyncTokenStore ) : LoadRoomMembersTask { override fun execute(params: LoadRoomMembersTask.Params): Try { - return executeRequest { - apiCall = roomAPI.getMembers(params.roomId, null, null, params.excludeMembership?.value) - }.flatMap { response -> - insertInDb(response, params.roomId) - }.map { true } + return if (areAllMembersAlreadyLoaded(params.roomId)) { + Try.just(true) + } else { + //TODO use this token + val lastToken = syncTokenStore.getLastToken() + executeRequest { + 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 { @@ -37,7 +44,7 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, .tryTransactionSync { realm -> // We ignore all the already known members 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 eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) } @@ -48,4 +55,11 @@ internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI, .map { response } } + private fun areAllMembersAlreadyLoaded(roomId: String): Boolean { + return monarchy + .fetchAllCopiedSync { RoomEntity.where(it, roomId) } + .firstOrNull() + ?.areAllMembersLoaded ?: false + } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt index 88acac5ef4..e58f6f62cf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt @@ -15,7 +15,12 @@ import io.realm.RealmQuery internal class RoomMemberExtractor(private val monarchy: Monarchy, private val roomId: String) { + private val cached = HashMap() + fun extractFrom(event: EventEntity): RoomMember? { + if (cached.containsKey(event.eventId)) { + return cached[event.eventId] + } val sender = event.sender ?: return null // If the event is unlinked we want to fetch unlinked state events 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() val content = if (event.stateIndex <= 0) { 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 { 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, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt index a80b7ccc9d..ea5d8d643c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt @@ -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.query.where import im.vector.matrix.android.internal.util.WorkerParamsFactory -import io.realm.RealmResults private const val PRUNE_EVENT_WORKER = "PRUNE_EVENT_WORKER" @@ -19,9 +18,9 @@ internal class EventsPruner(monarchy: Monarchy) : override val query = Monarchy.Query { EventEntity.where(it, type = EventType.REDACTION) } - override fun process(results: RealmResults, updateIndexes: IntArray, deletionIndexes: IntArray) { - val redactionEvents = results.map { it.asDomain() } - val pruneEventWorkerParams = PruneEventWorker.Params(redactionEvents, updateIndexes.toList(), deletionIndexes.toList()) + override fun process(inserted: List, updated: List, deleted: List) { + val redactionEvents = inserted.map { it.asDomain() } + val pruneEventWorkerParams = PruneEventWorker.Params(redactionEvents) val workData = WorkerParamsFactory.toData(pruneEventWorkerParams) val sendWork = OneTimeWorkRequestBuilder() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt index 6562fd14fe..90e5cdc4ff 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt @@ -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.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.tryTransactionAsync +import im.vector.matrix.android.internal.util.tryTransactionSync import io.realm.Realm import org.koin.standalone.inject @@ -22,9 +23,7 @@ internal class PruneEventWorker(context: Context, @JsonClass(generateAdapter = true) internal data class Params( - val redactionEvents: List, - val updateIndexes: List, - val deletionIndexes: List + val redactionEvents: List ) private val monarchy by inject() @@ -33,10 +32,9 @@ internal class PruneEventWorker(context: Context, val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() - val result = monarchy.tryTransactionAsync { realm -> - params.updateIndexes.forEach { index -> - val data = params.redactionEvents[index] - pruneEvent(realm, data) + val result = monarchy.tryTransactionSync { realm -> + params.redactionEvents.forEach { event -> + pruneEvent(realm, event) } } return result.fold({ Result.retry() }, { Result.success() }) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 1623df074c..7e96cc27bc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -1,11 +1,6 @@ package im.vector.matrix.android.internal.session.room.send -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager +import androidx.work.* import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.events.model.Event @@ -37,8 +32,8 @@ internal class DefaultSendService(private val roomId: String, monarchy.tryTransactionAsync { realm -> val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) - ?: return@tryTransactionAsync - chunkEntity.add(event, PaginationDirection.FORWARDS) + ?: return@tryTransactionAsync + chunkEntity.add(roomId, event, PaginationDirection.FORWARDS) chunkEntity.updateDisplayIndexes() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt index 5dfb3c3f48..647515a6c1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt @@ -1,18 +1,17 @@ package im.vector.matrix.android.internal.session.room.timeline 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.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.util.FilterUtil -internal interface PaginationTask : Task { +internal interface PaginationTask : Task { data class Params( val roomId: String, - val from: String?, + val from: String, val direction: PaginationDirection, val limit: Int ) @@ -23,17 +22,14 @@ internal class DefaultPaginationTask(private val roomAPI: RoomAPI, private val tokenChunkEventPersistor: TokenChunkEventPersistor ) : PaginationTask { - override fun execute(params: PaginationTask.Params): Try { - if (params.from == null) { - return RuntimeException("From token shouldn't be null").failure() - } + override fun execute(params: PaginationTask.Params): Try { val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString() return executeRequest { apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) }.flatMap { chunk -> tokenChunkEventPersistor .insertInDb(chunk, params.roomId, params.direction) - .map { chunk } } } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt deleted file mode 100644 index 3e03b4fb82..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt +++ /dev/null @@ -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() - - override fun timeline(eventId: String?): LiveData> { - 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 { - 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) - } - - -} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt new file mode 100644 index 0000000000..2a06dbb497 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineService.kt @@ -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() + + override fun timeline(eventId: String?): LiveData { + 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 { + 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) + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt index 3c9cb1bbb6..88282e85e0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineBoundaryCallback.kt @@ -1,60 +1,86 @@ package im.vector.matrix.android.internal.session.room.timeline +import android.arch.lifecycle.LiveData import android.arch.paging.PagedList import com.zhuinden.monarchy.Monarchy 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.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 java.util.* internal class TimelineBoundaryCallback(private val roomId: String, private val taskExecutor: TaskExecutor, private val paginationTask: PaginationTask, private val monarchy: Monarchy, private val helper: PagingRequestHelper -) : PagedList.BoundaryCallback() { +) : PagedList.BoundaryCallback() { var limit = 30 + val status = object : LiveData() { + + 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() { // 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) { - 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) { - runPaginationRequest(it, itemAtFront, PaginationDirection.FORWARDS) + executePaginationTask(it, token, PaginationDirection.FORWARDS) } } - private fun runPaginationRequest(requestCallback: PagingRequestHelper.Request.Callback, - item: EnrichedEvent, - direction: PaginationDirection) { + private fun getToken(eventId: String, direction: PaginationDirection): String? { var token: String? = null monarchy.doWithRealm { realm -> - if (item.root.eventId == null) { - return@doWithRealm - } - val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(item.root.eventId)).firstOrNull() + val chunkEntity = ChunkEntity.findIncludingEvent(realm, eventId) 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, - from = token, - direction = direction, - limit = limit) + from = from, + direction = direction, + limit = limit) paginationTask.configureWith(params) - .dispatchTo(object : MatrixCallback { - override fun onSuccess(data: TokenChunkEvent) { + .enableRetry() + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: Boolean) { requestCallback.recordSuccess() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt index bf4e9b490e..7e5943abe8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -2,12 +2,7 @@ package im.vector.matrix.android.internal.session.room.timeline import arrow.core.Try import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.internal.database.helper.addAll -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.helper.* 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.query.create @@ -21,12 +16,15 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { fun insertInDb(receivedChunk: TokenChunkEvent, roomId: String, - direction: PaginationDirection): Try { + direction: PaginationDirection): Try { + if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty()) { + return Try.just(false) + } return monarchy .tryTransactionSync { realm -> 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 prevToken: String? @@ -46,13 +44,13 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { var currentChunk = if (direction == PaginationDirection.FORWARDS) { prevChunk?.apply { this.nextToken = nextToken } - ?: ChunkEntity.create(realm, prevToken, nextToken) + ?: ChunkEntity.create(realm, prevToken, nextToken) } else { 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 if (currentChunk != prevChunk && prevChunk != null) { @@ -71,6 +69,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) { roomEntity.addOrUpdate(currentChunk) roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = currentChunk.isUnlinked()) } + .map { true } } 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 return if (direction == PaginationDirection.BACKWARDS) { - currentChunk.merge(otherChunk, PaginationDirection.BACKWARDS) + currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS) roomEntity.deleteOnCascade(otherChunk) currentChunk } else { - otherChunk.merge(currentChunk, PaginationDirection.BACKWARDS) + otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS) roomEntity.deleteOnCascade(currentChunk) otherChunk } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index d5305c0484..b71160b20e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -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.where 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.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 im.vector.matrix.android.internal.session.sync.model.* import io.realm.Realm import io.realm.kotlin.createObject @@ -45,9 +41,9 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, private fun handleRoomSync(realm: Realm, handlingStrategy: 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.LEFT -> handlingStrategy.data.map { handleLeftRoom(it.key, it.value) } + is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftRoom(it.key, it.value) } } realm.insertOrUpdate(rooms) } @@ -57,7 +53,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, roomSync: RoomSync): RoomEntity { val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) if (roomEntity.membership == MyMembership.INVITED) { roomEntity.chunks.deleteAllFromRealm() @@ -138,7 +134,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, lastChunk?.isLast = false chunkEntity.isLast = true - chunkEntity.addAll(eventList, PaginationDirection.FORWARDS, stateIndexOffset) + chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset) return chunkEntity } @@ -147,7 +143,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, roomSummary: RoomSyncSummary) { val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() - ?: RoomSummaryEntity(roomId) + ?: RoomSummaryEntity(roomId) if (roomSummary.heroes.isNotEmpty()) { roomSummaryEntity.heroes.clear() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt index e5cc7b332f..faf2fb3af3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt @@ -13,6 +13,7 @@ internal data class ConfigurableTask( val params: PARAMS, val callbackThread: TaskThread = TaskThread.MAIN, val executionThread: TaskThread = TaskThread.IO, + val retryCount: Int = 0, val callback: MatrixCallback = object : MatrixCallback {} ) : Task { @@ -33,10 +34,18 @@ internal data class ConfigurableTask( return copy(callback = matrixCallback) } + fun enableRetry(retryCount: Int = Int.MAX_VALUE): ConfigurableTask { + return copy(retryCount = retryCount) + } + fun executeBy(taskExecutor: TaskExecutor): Cancelable { return taskExecutor.execute(this) } + override fun toString(): String { + return task.javaClass.name + } + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt index fce0cd7921..67354642de 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt @@ -1,9 +1,11 @@ package im.vector.matrix.android.internal.task +import arrow.core.Try import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.util.CancelableCoroutine import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber @@ -15,14 +17,36 @@ internal class TaskExecutor(private val coroutineDispatchers: MatrixCoroutineDis val job = GlobalScope.launch(task.callbackThread.toDispatcher()) { val resultOrFailure = withContext(task.executionThread.toDispatcher()) { - Timber.v("Executing ${task.javaClass} on ${Thread.currentThread().name}") - task.execute(task.params) + Timber.v("Executing $task on ${Thread.currentThread().name}") + retry(task.retryCount) { + task.execute(task.params) + } } resultOrFailure.fold({ task.callback.onFailure(it) }, { task.callback.onSuccess(it) }) } return CancelableCoroutine(job) } + private suspend fun 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): Try { + + 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) { TaskThread.MAIN -> coroutineDispatchers.main TaskThread.COMPUTATION -> coroutineDispatchers.computation diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CoroutineRetry.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CoroutineRetry.kt deleted file mode 100644 index 28520a69d5..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CoroutineRetry.kt +++ /dev/null @@ -1,24 +0,0 @@ -package im.vector.matrix.android.internal.util - -import arrow.core.Try -import kotlinx.coroutines.delay - -suspend fun 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): Try { - - 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() -} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/LiveDataUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/LiveDataUtils.kt new file mode 100644 index 0000000000..55de4d34f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/LiveDataUtils.kt @@ -0,0 +1,38 @@ +package im.vector.matrix.android.internal.util + +import android.arch.lifecycle.LiveData +import android.arch.lifecycle.MediatorLiveData + +object LiveDataUtils { + + fun combine(firstSource: LiveData, + secondSource: LiveData, + mapper: (FIRST, SECOND) -> OUT): LiveData { + + return MediatorLiveData().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() + } + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/PagingRequestHelper.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/PagingRequestHelper.java index 7e77fead88..fa1ad08692 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/PagingRequestHelper.java +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/PagingRequestHelper.java @@ -379,6 +379,11 @@ public class PagingRequestHelper { @NonNull 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, @NonNull Throwable[] errors) { this.initial = initial;