forked from GitHub-Mirror/riotX-android
Merge feature/replace_paged_list into develop
This commit is contained in:
commit
d110dac0a6
@ -144,7 +144,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
|
||||
def epoxy_version = "3.0.0"
|
||||
def epoxy_version = "3.3.0"
|
||||
def arrow_version = "0.8.2"
|
||||
def coroutines_version = "1.0.1"
|
||||
def markwon_version = '3.0.0-SNAPSHOT'
|
||||
@ -159,13 +159,10 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0-alpha03'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.core:core-ktx:1.0.1'
|
||||
|
||||
// Paging
|
||||
implementation 'androidx.paging:paging-runtime:2.0.0'
|
||||
|
||||
implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1'
|
||||
|
||||
// Log
|
||||
@ -192,7 +189,7 @@ dependencies {
|
||||
|
||||
// UI
|
||||
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||
implementation 'com.google.android.material:material:1.1.0-alpha02'
|
||||
implementation 'com.google.android.material:material:1.1.0-alpha04'
|
||||
implementation 'me.gujun.android:span:1.7'
|
||||
implementation "ru.noties.markwon:core:$markwon_version"
|
||||
implementation "ru.noties.markwon:html:$markwon_version"
|
||||
|
@ -19,6 +19,8 @@ package im.vector.riotredesign
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.multidex.MultiDex
|
||||
import com.airbnb.epoxy.EpoxyAsyncUtil
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
import com.facebook.stetho.Stetho
|
||||
import com.github.piasy.biv.BigImageViewer
|
||||
import com.github.piasy.biv.loader.glide.GlideImageLoader
|
||||
@ -50,6 +52,8 @@ class Riot : Application() {
|
||||
|
||||
AndroidThreeTen.init(this)
|
||||
BigImageViewer.initialize(GlideImageLoader.with(applicationContext))
|
||||
EpoxyController.defaultDiffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||
EpoxyController.defaultModelBuildingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||
val appModule = AppModule(applicationContext).definition
|
||||
val homeModule = HomeModule().definition
|
||||
startKoin(listOf(appModule, homeModule), logger = EmptyLogger())
|
||||
|
@ -16,8 +16,19 @@
|
||||
|
||||
package im.vector.riotredesign.core.glide;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.bumptech.glide.GlideBuilder;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
|
||||
@GlideModule
|
||||
public final class MyAppGlideModule extends AppGlideModule {}
|
||||
public final class MyAppGlideModule extends AppGlideModule {
|
||||
|
||||
@Override
|
||||
public void applyOptions(Context context, GlideBuilder builder) {
|
||||
builder.setLogLevel(Log.ERROR);
|
||||
}
|
||||
|
||||
}
|
@ -19,11 +19,10 @@ package im.vector.riotredesign.features.home
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.DrawableImageViewTarget
|
||||
import com.bumptech.glide.request.target.Target
|
||||
@ -40,8 +39,11 @@ import im.vector.riotredesign.core.glide.GlideRequests
|
||||
/**
|
||||
* This helper centralise ways to retrieve avatar into ImageView or even generic Target<Drawable>
|
||||
*/
|
||||
|
||||
object AvatarRenderer {
|
||||
|
||||
private const val THUMBNAIL_SIZE = 250
|
||||
|
||||
@UiThread
|
||||
fun render(roomMember: RoomMember, imageView: ImageView) {
|
||||
render(roomMember.avatarUrl, roomMember.displayName, imageView)
|
||||
@ -54,7 +56,7 @@ object AvatarRenderer {
|
||||
|
||||
@UiThread
|
||||
fun render(avatarUrl: String?, name: String?, imageView: ImageView) {
|
||||
render(imageView.context, GlideApp.with(imageView), avatarUrl, name, imageView.height, DrawableImageViewTarget(imageView))
|
||||
render(imageView.context, GlideApp.with(imageView), avatarUrl, name, DrawableImageViewTarget(imageView))
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@ -62,45 +64,18 @@ object AvatarRenderer {
|
||||
glideRequest: GlideRequests,
|
||||
avatarUrl: String?,
|
||||
name: String?,
|
||||
size: Int,
|
||||
target: Target<Drawable>) {
|
||||
if (name.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
val placeholder = buildPlaceholderDrawable(context, name)
|
||||
buildGlideRequest(glideRequest, avatarUrl, size)
|
||||
val placeholder = getPlaceholderDrawable(context, name)
|
||||
buildGlideRequest(glideRequest, avatarUrl)
|
||||
.placeholder(placeholder)
|
||||
.into(target)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun getCachedOrPlaceholder(context: Context,
|
||||
glideRequest: GlideRequests,
|
||||
avatarUrl: String?,
|
||||
text: String,
|
||||
size: Int): Drawable {
|
||||
val future = buildGlideRequest(glideRequest, avatarUrl, size).onlyRetrieveFromCache(true).submit()
|
||||
return try {
|
||||
future.get()
|
||||
} catch (exception: Exception) {
|
||||
buildPlaceholderDrawable(context, text)
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE API *********************************************************************************
|
||||
|
||||
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?, size: Int): GlideRequest<Drawable> {
|
||||
val resolvedUrl = Matrix.getInstance().currentSession!!
|
||||
.contentUrlResolver()
|
||||
.resolveThumbnail(avatarUrl, size, size, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
|
||||
return glideRequest
|
||||
.load(resolvedUrl)
|
||||
.apply(RequestOptions.circleCropTransform())
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
}
|
||||
|
||||
private fun buildPlaceholderDrawable(context: Context, text: String): Drawable {
|
||||
@AnyThread
|
||||
fun getPlaceholderDrawable(context: Context, text: String): Drawable {
|
||||
val avatarColor = ContextCompat.getColor(context, R.color.pale_teal)
|
||||
return if (text.isEmpty()) {
|
||||
TextDrawable.builder().buildRound("", avatarColor)
|
||||
@ -110,7 +85,18 @@ object AvatarRenderer {
|
||||
val firstLetter = text[firstLetterIndex].toString().toUpperCase()
|
||||
TextDrawable.builder().buildRound(firstLetter, avatarColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// PRIVATE API *********************************************************************************
|
||||
|
||||
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> {
|
||||
val resolvedUrl = Matrix.getInstance().currentSession!!.contentUrlResolver()
|
||||
.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
|
||||
return glideRequest
|
||||
.load(resolvedUrl)
|
||||
.apply(RequestOptions.circleCropTransform())
|
||||
}
|
||||
|
||||
}
|
@ -19,16 +19,9 @@ package im.vector.riotredesign.features.home
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
import im.vector.riotredesign.features.home.group.GroupSummaryController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.CallItemFactory
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.DefaultItemFactory
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.MessageItemFactory
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomHistoryVisibilityItemFactory
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomMemberItemFactory
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomNameItemFactory
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.RoomTopicItemFactory
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.*
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.riotredesign.features.home.room.list.RoomSummaryController
|
||||
import im.vector.riotredesign.features.html.EventHtmlRenderer
|
||||
@ -57,28 +50,28 @@ class HomeModule {
|
||||
|
||||
// Fragment scopes
|
||||
|
||||
scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) ->
|
||||
factory { (fragment: Fragment) ->
|
||||
val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get())
|
||||
val timelineDateFormatter = TimelineDateFormatter(get())
|
||||
val timelineMediaSizeProvider = TimelineMediaSizeProvider()
|
||||
val messageItemFactory = MessageItemFactory(get(), timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer)
|
||||
|
||||
val timelineItemFactory = TimelineItemFactory(messageItemFactory = messageItemFactory,
|
||||
roomNameItemFactory = RoomNameItemFactory(get()),
|
||||
roomTopicItemFactory = RoomTopicItemFactory(get()),
|
||||
roomMemberItemFactory = RoomMemberItemFactory(get()),
|
||||
roomHistoryVisibilityItemFactory = RoomHistoryVisibilityItemFactory(get()),
|
||||
callItemFactory = CallItemFactory(get()),
|
||||
defaultItemFactory = DefaultItemFactory()
|
||||
roomNameItemFactory = RoomNameItemFactory(get()),
|
||||
roomTopicItemFactory = RoomTopicItemFactory(get()),
|
||||
roomMemberItemFactory = RoomMemberItemFactory(get()),
|
||||
roomHistoryVisibilityItemFactory = RoomHistoryVisibilityItemFactory(get()),
|
||||
callItemFactory = CallItemFactory(get()),
|
||||
defaultItemFactory = DefaultItemFactory()
|
||||
)
|
||||
TimelineEventController(timelineDateFormatter, timelineItemFactory, timelineMediaSizeProvider)
|
||||
}
|
||||
|
||||
scope(ROOM_LIST_SCOPE) {
|
||||
factory {
|
||||
RoomSummaryController(get())
|
||||
}
|
||||
|
||||
scope(GROUP_LIST_SCOPE) {
|
||||
factory {
|
||||
GroupSummaryController()
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,8 @@ class HomeNavigator {
|
||||
addToBackstack: Boolean = false) {
|
||||
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
|
||||
activity?.let {
|
||||
val args = RoomDetailArgs(roomId, eventId)
|
||||
//TODO enable eventId permalink. It doesn't work enough at the moment.
|
||||
val args = RoomDetailArgs(roomId)
|
||||
val roomDetailFragment = RoomDetailFragment.newInstance(args)
|
||||
it.drawerLayout?.closeDrawer(GravityCompat.START)
|
||||
if (addToBackstack) {
|
||||
|
@ -16,12 +16,14 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
|
||||
sealed class RoomDetailActions {
|
||||
|
||||
data class SendMessage(val text: String) : RoomDetailActions()
|
||||
object IsDisplayed : RoomDetailActions()
|
||||
data class EventDisplayed(val event: TimelineEvent, val index: Int) : RoomDetailActions()
|
||||
data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions()
|
||||
data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions()
|
||||
|
||||
}
|
@ -24,7 +24,6 @@ import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.R
|
||||
@ -35,7 +34,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
import im.vector.riotredesign.features.home.HomePermalinkHandler
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.animation.TimelineItemAnimator
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
|
||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
||||
import im.vector.riotredesign.features.media.MediaViewerActivity
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
@ -80,7 +79,6 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
setupRecyclerView()
|
||||
setupToolbar()
|
||||
setupSendButton()
|
||||
timelineEventController.requestModelBuild()
|
||||
roomDetailViewModel.subscribe { renderState(it) }
|
||||
}
|
||||
|
||||
@ -105,12 +103,17 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
|
||||
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
|
||||
recyclerView.layoutManager = layoutManager
|
||||
recyclerView.itemAnimator = TimelineItemAnimator()
|
||||
recyclerView.itemAnimator = null
|
||||
recyclerView.setHasFixedSize(true)
|
||||
timelineEventController.addModelBuildListener {
|
||||
it.dispatchTo(stateRestorer)
|
||||
it.dispatchTo(scrollOnNewMessageCallback)
|
||||
}
|
||||
|
||||
recyclerView.addOnScrollListener(
|
||||
EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction ->
|
||||
roomDetailViewModel.process(RoomDetailActions.LoadMore(direction))
|
||||
})
|
||||
recyclerView.setController(timelineEventController)
|
||||
timelineEventController.callback = this
|
||||
}
|
||||
@ -119,29 +122,15 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
sendButton.setOnClickListener {
|
||||
val textMessage = composerEditText.text.toString()
|
||||
if (textMessage.isNotBlank()) {
|
||||
composerEditText.text = null
|
||||
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
|
||||
composerEditText.text = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
timelineEventController.setTimeline(state.timeline)
|
||||
}
|
||||
|
||||
private fun renderRoomSummary(state: RoomDetailViewState) {
|
||||
@ -157,14 +146,14 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
// TimelineEventController.Callback ************************************************************
|
||||
// TimelineEventController.Callback ************************************************************
|
||||
|
||||
override fun onUrlClicked(url: String) {
|
||||
homePermalinkHandler.launch(url)
|
||||
}
|
||||
|
||||
override fun onEventVisible(event: TimelineEvent, index: Int) {
|
||||
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event, index))
|
||||
override fun onEventVisible(event: TimelineEvent) {
|
||||
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
|
||||
}
|
||||
|
||||
override fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) {
|
||||
|
@ -23,9 +23,9 @@ 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.extensions.lastMinBy
|
||||
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import org.koin.android.ext.android.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
@ -38,11 +38,13 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
private val room = session.getRoom(initialState.roomId)!!
|
||||
private val roomId = initialState.roomId
|
||||
private val eventId = initialState.eventId
|
||||
|
||||
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
|
||||
private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES)
|
||||
|
||||
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
|
||||
|
||||
const val PAGINATION_COUNT = 50
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? {
|
||||
val currentSession = viewModelContext.activity.get<Session>()
|
||||
@ -53,9 +55,10 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
|
||||
init {
|
||||
observeRoomSummary()
|
||||
observeTimeline()
|
||||
observeDisplayedEvents()
|
||||
observeEventDisplayedActions()
|
||||
room.loadRoomMembersIfNeeded()
|
||||
timeline.start()
|
||||
setState { copy(timeline = this@RoomDetailViewModel.timeline) }
|
||||
}
|
||||
|
||||
fun process(action: RoomDetailActions) {
|
||||
@ -63,6 +66,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
is RoomDetailActions.SendMessage -> handleSendMessage(action)
|
||||
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
|
||||
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
|
||||
is RoomDetailActions.LoadMore -> handleLoadMore(action)
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,14 +84,18 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
visibleRoomHolder.post(roomId)
|
||||
}
|
||||
|
||||
private fun observeDisplayedEvents() {
|
||||
private fun handleLoadMore(action: RoomDetailActions.LoadMore) {
|
||||
timeline.paginate(action.direction, PAGINATION_COUNT)
|
||||
}
|
||||
|
||||
private fun observeEventDisplayedActions() {
|
||||
// We are buffering scroll events for one second
|
||||
// and keep the most recent one to set the read receipt on.
|
||||
displayedEventsObservable.hide()
|
||||
displayedEventsObservable
|
||||
.buffer(1, TimeUnit.SECONDS)
|
||||
.filter { it.isNotEmpty() }
|
||||
.subscribeBy(onNext = { actions ->
|
||||
val mostRecentEvent = actions.lastMinBy { it.index }
|
||||
val mostRecentEvent = actions.maxBy { it.event.displayIndex }
|
||||
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
|
||||
room.setReadReceipt(eventId, callback = object : MatrixCallback<Void> {})
|
||||
}
|
||||
@ -102,12 +110,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeTimeline() {
|
||||
room.rx().timeline(eventId)
|
||||
.execute { timelineData ->
|
||||
copy(asyncTimelineData = timelineData)
|
||||
}
|
||||
override fun onCleared() {
|
||||
timeline.dispose()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -20,11 +20,13 @@ 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.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineData
|
||||
|
||||
data class RoomDetailViewState(
|
||||
val roomId: String,
|
||||
val eventId: String?,
|
||||
val timeline: Timeline? = null,
|
||||
val asyncRoomSummary: Async<RoomSummary> = Uninitialized,
|
||||
val asyncTimelineData: Async<TimelineData> = Uninitialized
|
||||
) : MvRxState {
|
||||
|
@ -18,14 +18,11 @@ package im.vector.riotredesign.features.home.room.detail
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback {
|
||||
|
||||
var isLocked = AtomicBoolean(true)
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
if (isLocked.compareAndSet(false, true) && position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) {
|
||||
if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) {
|
||||
layoutManager.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
@ -16,46 +16,94 @@
|
||||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.EpoxyAsyncUtil
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineData
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.riotredesign.core.epoxy.LoadingItemModel_
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||
import im.vector.riotredesign.core.extensions.localDateTime
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.helper.*
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||
import im.vector.riotredesign.features.home.room.detail.timeline.paging.PagedListEpoxyController
|
||||
import im.vector.riotredesign.features.media.MediaContentRenderer
|
||||
|
||||
class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
private val timelineItemFactory: TimelineItemFactory,
|
||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider
|
||||
) : PagedListEpoxyController<TimelineEvent>(
|
||||
EpoxyAsyncUtil.getAsyncBackgroundHandler(),
|
||||
EpoxyAsyncUtil.getAsyncBackgroundHandler()
|
||||
) {
|
||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
|
||||
private val backgroundHandler: Handler = TimelineAsyncHelper.getBackgroundHandler()
|
||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener {
|
||||
|
||||
private var isLoadingForward: Boolean = false
|
||||
private var isLoadingBackward: Boolean = false
|
||||
private var hasReachedEnd: Boolean = true
|
||||
interface Callback {
|
||||
fun onEventVisible(event: TimelineEvent)
|
||||
fun onUrlClicked(url: String)
|
||||
fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View)
|
||||
}
|
||||
|
||||
private val modelCache = arrayListOf<List<EpoxyModel<*>>>()
|
||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||
private var inSubmitList: Boolean = false
|
||||
private var timeline: Timeline? = null
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
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)
|
||||
private val listUpdateCallback = object : ListUpdateCallback {
|
||||
|
||||
@Synchronized
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
(position until (position + count)).forEach {
|
||||
modelCache[it] = emptyList()
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
val model = modelCache.removeAt(fromPosition)
|
||||
modelCache.add(toPosition, model)
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
if (modelCache.isNotEmpty() && position == modelCache.size) {
|
||||
modelCache[position - 1] = emptyList()
|
||||
}
|
||||
(0 until count).forEach {
|
||||
modelCache.add(position, emptyList())
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
assertUpdateCallbacksAllowed()
|
||||
(0 until count).forEach {
|
||||
modelCache.removeAt(position)
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
fun setTimeline(timeline: Timeline?) {
|
||||
if (this.timeline != timeline) {
|
||||
this.timeline = timeline
|
||||
this.timeline?.listener = this
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
@ -63,13 +111,55 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
timelineMediaSizeProvider.recyclerView = recyclerView
|
||||
}
|
||||
|
||||
override fun buildItemModels(currentPosition: Int, items: List<TimelineEvent?>): List<EpoxyModel<*>> {
|
||||
if (items.isNullOrEmpty()) {
|
||||
return emptyList()
|
||||
override fun buildModels() {
|
||||
LoadingItemModel_()
|
||||
.id("forward_loading_item")
|
||||
.addWhen(Timeline.Direction.FORWARDS)
|
||||
|
||||
|
||||
val timelineModels = getModels()
|
||||
add(timelineModels)
|
||||
|
||||
LoadingItemModel_()
|
||||
.id("backward_loading_item")
|
||||
.addWhen(Timeline.Direction.BACKWARDS)
|
||||
}
|
||||
|
||||
// Timeline.LISTENER ***************************************************************************
|
||||
|
||||
override fun onUpdated(snapshot: List<TimelineEvent>) {
|
||||
submitSnapshot(snapshot)
|
||||
}
|
||||
|
||||
private fun submitSnapshot(newSnapshot: List<TimelineEvent>) {
|
||||
backgroundHandler.post {
|
||||
inSubmitList = true
|
||||
val diffCallback = TimelineEventDiffUtilCallback(currentSnapshot, newSnapshot)
|
||||
currentSnapshot = newSnapshot
|
||||
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||
diffResult.dispatchUpdatesTo(listUpdateCallback)
|
||||
inSubmitList = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertUpdateCallbacksAllowed() {
|
||||
require(inSubmitList || Looper.myLooper() == backgroundHandler.looper)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun getModels(): List<EpoxyModel<*>> {
|
||||
(0 until modelCache.size).forEach { position ->
|
||||
if (modelCache[position].isEmpty()) {
|
||||
modelCache[position] = buildItemModels(position, currentSnapshot)
|
||||
}
|
||||
}
|
||||
return modelCache.flatten()
|
||||
}
|
||||
|
||||
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): List<EpoxyModel<*>> {
|
||||
val epoxyModels = ArrayList<EpoxyModel<*>>()
|
||||
val event = items[currentPosition] ?: return emptyList()
|
||||
val nextEvent = if (currentPosition + 1 < items.size) items[currentPosition + 1] else null
|
||||
val event = items[currentPosition]
|
||||
val nextEvent = items.nextDisplayableEvent(currentPosition)
|
||||
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
@ -77,7 +167,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
|
||||
timelineItemFactory.create(event, nextEvent, callback).also {
|
||||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event, currentPosition))
|
||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||
epoxyModels.add(it)
|
||||
}
|
||||
if (addDaySeparator) {
|
||||
@ -88,35 +178,22 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
return epoxyModels
|
||||
}
|
||||
|
||||
override fun addModels(models: List<EpoxyModel<*>>) {
|
||||
LoadingItemModel_()
|
||||
.id("forward_loading_item")
|
||||
.addIf(isLoadingForward, this)
|
||||
|
||||
super.add(models)
|
||||
|
||||
LoadingItemModel_()
|
||||
.id("backward_loading_item")
|
||||
.addIf(!hasReachedEnd, this)
|
||||
}
|
||||
|
||||
|
||||
interface Callback {
|
||||
fun onEventVisible(event: TimelineEvent, index: Int)
|
||||
fun onUrlClicked(url: String)
|
||||
fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View)
|
||||
private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) {
|
||||
val shouldAdd = timeline?.let {
|
||||
it.hasMoreToLoad(direction)
|
||||
} ?: false
|
||||
addIf(shouldAdd, this@TimelineEventController)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?,
|
||||
private val event: TimelineEvent,
|
||||
private val currentPosition: Int)
|
||||
private val event: TimelineEvent)
|
||||
: RiotEpoxyModel.OnVisibilityStateChangedListener {
|
||||
|
||||
override fun onVisibilityStateChanged(visibilityState: Int) {
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
callback?.onEventVisible(event, currentPosition)
|
||||
callback?.onEventVisible(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,9 +24,9 @@ class TimelineItemAnimator : DefaultItemAnimator() {
|
||||
|
||||
init {
|
||||
addDuration = ANIM_DURATION_IN_MILLIS
|
||||
removeDuration = ANIM_DURATION_IN_MILLIS
|
||||
moveDuration = ANIM_DURATION_IN_MILLIS
|
||||
changeDuration = ANIM_DURATION_IN_MILLIS
|
||||
removeDuration = 0
|
||||
moveDuration = 0
|
||||
changeDuration = 0
|
||||
}
|
||||
|
||||
}
|
@ -42,8 +42,6 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
private val timelineDateFormatter: TimelineDateFormatter,
|
||||
private val htmlRenderer: EventHtmlRenderer) {
|
||||
|
||||
private val messagesDisplayedWithInformation = HashSet<String?>()
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
callback: TimelineEventController.Callback?
|
||||
@ -58,15 +56,12 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
||||
?: false
|
||||
|
||||
if (addDaySeparator
|
||||
val showInformation = addDaySeparator
|
||||
|| nextRoomMember != roomMember
|
||||
|| nextEvent?.root?.type != EventType.MESSAGE
|
||||
|| isNextMessageReceivedMoreThanOneHourAgo) {
|
||||
messagesDisplayedWithInformation.add(event.root.eventId)
|
||||
}
|
||||
|| isNextMessageReceivedMoreThanOneHourAgo
|
||||
|
||||
val messageContent: MessageContent = event.root.content.toModel() ?: return null
|
||||
val showInformation = messagesDisplayedWithInformation.contains(event.root.eventId)
|
||||
val time = timelineDateFormatter.formatMessageHour(date)
|
||||
val avatarUrl = roomMember?.avatarUrl
|
||||
val memberName = roomMember?.displayName ?: event.root.sender
|
||||
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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.helper
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
|
||||
class EndlessRecyclerViewScrollListener(private val layoutManager: LinearLayoutManager,
|
||||
private val visibleThreshold: Int,
|
||||
private val onLoadMore: (Timeline.Direction) -> Unit
|
||||
) : RecyclerView.OnScrollListener() {
|
||||
|
||||
// The total number of items in the dataset after the last load
|
||||
private var previousTotalItemCount = 0
|
||||
// True if we are still waiting for the last set of data to load.
|
||||
private var loadingBackwards = true
|
||||
private var loadingForwards = true
|
||||
|
||||
// This happens many times a second during a scroll, so be wary of the code you place here.
|
||||
// We are given a few useful parameters to help us work out if we need to load some more data,
|
||||
// but first we check if we are waiting for the previous load to finish.
|
||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
val totalItemCount = layoutManager.itemCount
|
||||
|
||||
// The minimum amount of items to have below your current scroll position
|
||||
// before loading more.
|
||||
// If the total item count is zero and the previous isn't, assume the
|
||||
// list is invalidated and should be reset back to initial state
|
||||
if (totalItemCount < previousTotalItemCount) {
|
||||
previousTotalItemCount = totalItemCount
|
||||
if (totalItemCount == 0) {
|
||||
loadingForwards = true
|
||||
loadingBackwards = true
|
||||
}
|
||||
}
|
||||
// If it’s still loading, we check to see if the dataset count has
|
||||
// changed, if so we conclude it has finished loading
|
||||
if (totalItemCount > previousTotalItemCount) {
|
||||
loadingBackwards = false
|
||||
loadingForwards = false
|
||||
previousTotalItemCount = totalItemCount
|
||||
}
|
||||
// If it isn’t currently loading, we check to see if we have reached
|
||||
// the visibleThreshold and need to reload more data.
|
||||
if (!loadingBackwards && lastVisibleItemPosition + visibleThreshold > totalItemCount) {
|
||||
loadingBackwards = true
|
||||
onLoadMore(Timeline.Direction.BACKWARDS)
|
||||
}
|
||||
if (!loadingForwards && firstVisibleItemPosition < visibleThreshold) {
|
||||
loadingForwards = true
|
||||
onLoadMore(Timeline.Direction.FORWARDS)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * 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.helper
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
|
||||
private const val THREAD_NAME = "Timeline_Building_Thread"
|
||||
|
||||
object TimelineAsyncHelper {
|
||||
|
||||
private var backgroundHandler: Handler? = null
|
||||
|
||||
fun getBackgroundHandler(): Handler {
|
||||
return backgroundHandler ?: createBackgroundHandler().also { backgroundHandler = it }
|
||||
}
|
||||
|
||||
private fun createBackgroundHandler(): Handler {
|
||||
val handlerThread = HandlerThread(THREAD_NAME)
|
||||
handlerThread.start()
|
||||
return Handler(handlerThread.looper)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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.helper
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
|
||||
object TimelineDisplayableEvents {
|
||||
|
||||
val DISPLAYABLE_TYPES = listOf(
|
||||
EventType.MESSAGE,
|
||||
EventType.STATE_ROOM_NAME,
|
||||
EventType.STATE_ROOM_TOPIC,
|
||||
EventType.STATE_ROOM_MEMBER,
|
||||
EventType.STATE_HISTORY_VISIBILITY,
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER,
|
||||
EventType.ENCRYPTED,
|
||||
EventType.ENCRYPTION,
|
||||
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
|
||||
EventType.STICKER,
|
||||
EventType.STATE_ROOM_CREATE
|
||||
)
|
||||
}
|
||||
|
||||
fun TimelineEvent.isDisplayable(): Boolean {
|
||||
return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.type) && !root.content.isNullOrEmpty()
|
||||
}
|
||||
|
||||
fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
|
||||
return this.filter {
|
||||
it.isDisplayable()
|
||||
}
|
||||
}
|
||||
|
||||
fun List<TimelineEvent>.nextDisplayableEvent(index: Int): TimelineEvent? {
|
||||
return if (index == size - 1) {
|
||||
null
|
||||
} else {
|
||||
subList(index + 1, this.size).firstOrNull { it.isDisplayable() }
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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.helper
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
|
||||
class TimelineEventDiffUtilCallback(private val oldList: List<TimelineEvent>,
|
||||
private val newList: List<TimelineEvent>) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize(): Int {
|
||||
return oldList.size
|
||||
}
|
||||
|
||||
override fun getNewListSize(): Int {
|
||||
return newList.size
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList[oldItemPosition]
|
||||
val newItem = newList[newItemPosition]
|
||||
return oldItem.localId == newItem.localId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = oldList[oldItemPosition]
|
||||
val newItem = newList[newItemPosition]
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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 androidx.paging.PagedList
|
||||
import android.os.Handler
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.EpoxyViewHolder
|
||||
|
||||
/**
|
||||
* An [EpoxyController] that can work with a [PagedList].
|
||||
*
|
||||
* Internally, it caches the model for each item in the [PagedList]. You should override
|
||||
* [buildItemModel] method to build the model for the given item. Since [PagedList] might include
|
||||
* `null` items if placeholders are enabled, this method needs to handle `null` values in the list.
|
||||
*
|
||||
* By default, the model for each item is added to the model list. To change this behavior (to
|
||||
* filter items or inject extra items), you can override [addModels] function and manually add built
|
||||
* models.
|
||||
*
|
||||
* @param T The type of the items in the [PagedList].
|
||||
*/
|
||||
abstract class PagedListEpoxyController<T>(
|
||||
/**
|
||||
* The handler to use for building models. By default this uses the main thread, but you can use
|
||||
* [EpoxyAsyncUtil.getAsyncBackgroundHandler] to do model building in the background.
|
||||
*
|
||||
* The notify thread of your PagedList (from setNotifyExecutor in the PagedList Builder) must be
|
||||
* the same as this thread. Otherwise Epoxy will crash.
|
||||
*/
|
||||
modelBuildingHandler: Handler = EpoxyController.defaultModelBuildingHandler,
|
||||
/**
|
||||
* The handler to use when calculating the diff between built model lists.
|
||||
* By default this uses the main thread, but you can use
|
||||
* [EpoxyAsyncUtil.getAsyncBackgroundHandler] to do diffing in the background.
|
||||
*/
|
||||
diffingHandler: Handler = EpoxyController.defaultDiffingHandler,
|
||||
/**
|
||||
* [PagedListEpoxyController] uses an [DiffUtil.ItemCallback] to detect changes between
|
||||
* [PagedList]s. By default, it relies on simple object equality but you can provide a custom
|
||||
* one if you don't use all fields in the object in your models.
|
||||
*/
|
||||
itemDiffCallback: DiffUtil.ItemCallback<T> = DEFAULT_ITEM_DIFF_CALLBACK as DiffUtil.ItemCallback<T>
|
||||
) : EpoxyController(modelBuildingHandler, diffingHandler) {
|
||||
// this is where we keep the already built models
|
||||
protected val modelCache = PagedListModelCache(
|
||||
modelBuilder = { pos, item ->
|
||||
buildItemModels(pos, item)
|
||||
},
|
||||
rebuildCallback = {
|
||||
requestModelBuild()
|
||||
},
|
||||
itemDiffCallback = itemDiffCallback,
|
||||
modelBuildingHandler = modelBuildingHandler
|
||||
)
|
||||
|
||||
var currentList: PagedList<T>? = null
|
||||
private set
|
||||
|
||||
final override fun buildModels() {
|
||||
addModels(modelCache.getModels())
|
||||
}
|
||||
|
||||
override fun onModelBound(
|
||||
holder: EpoxyViewHolder,
|
||||
boundModel: EpoxyModel<*>,
|
||||
position: Int,
|
||||
previouslyBoundModel: EpoxyModel<*>?
|
||||
) {
|
||||
modelCache.loadAround(boundModel)
|
||||
}
|
||||
|
||||
/**
|
||||
* This function adds all built models to the adapter. You can override this method to add extra
|
||||
* items into the model list or remove some.
|
||||
*/
|
||||
open fun addModels(models: List<EpoxyModel<*>>) {
|
||||
super.add(models)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the model for a given item. This must return a single model for each item. If you want
|
||||
* to inject headers etc, you can override [addModels] function.
|
||||
*
|
||||
* If the `item` is `null`, you should provide the placeholder. If your [PagedList] is configured
|
||||
* without placeholders, you don't need to handle the `null` case.
|
||||
*/
|
||||
abstract fun buildItemModels(currentPosition: Int, items: List<T?>): List<EpoxyModel<*>>
|
||||
|
||||
/**
|
||||
* Submit a new paged list.
|
||||
*
|
||||
* A diff will be calculated between this list and the previous list so you may still get calls
|
||||
* to [buildItemModel] with items from the previous list.
|
||||
*/
|
||||
fun submitList(newList: PagedList<T>?) {
|
||||
currentList = newList
|
||||
modelCache.submitList(newList)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* [PagedListEpoxyController] calculates a diff on top of the PagedList to check which
|
||||
* models are invalidated.
|
||||
* This is the default [DiffUtil.ItemCallback] which uses object equality.
|
||||
*/
|
||||
val DEFAULT_ITEM_DIFF_CALLBACK = object : DiffUtil.ItemCallback<Any>() {
|
||||
override fun areItemsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem
|
||||
|
||||
override fun areContentsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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.os.Handler
|
||||
import androidx.paging.AsyncPagedListDiffer
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import com.airbnb.epoxy.EpoxyModel
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* A PagedList stream wrapper that caches models built for each item. It tracks changes in paged lists and caches
|
||||
* models for each item when they are invalidated to avoid rebuilding models for the whole list when PagedList is
|
||||
* updated.
|
||||
*/
|
||||
class PagedListModelCache<T>(
|
||||
private val modelBuilder: (itemIndex: Int, items: List<T>) -> List<EpoxyModel<*>>,
|
||||
private val rebuildCallback: () -> Unit,
|
||||
private val itemDiffCallback: DiffUtil.ItemCallback<T>,
|
||||
private val diffExecutor: Executor? = null,
|
||||
private val modelBuildingHandler: Handler
|
||||
) {
|
||||
|
||||
|
||||
// Int is the index of the pagedList item
|
||||
// We have to be able to find the pagedlist position coming from an epoxy model to trigger
|
||||
// LoadAround with accuracy
|
||||
private val modelCache = linkedMapOf<EpoxyModel<*>, Int>()
|
||||
private var isCacheStale = AtomicBoolean(true)
|
||||
|
||||
/**
|
||||
* Tracks the last accessed position so that we can report it back to the paged list when models are built.
|
||||
*/
|
||||
private var lastPosition: Int? = null
|
||||
|
||||
/**
|
||||
* Observer for the PagedList changes that invalidates the model cache when data is updated.
|
||||
*/
|
||||
private val updateCallback = object : ListUpdateCallback {
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
invalidate()
|
||||
rebuildCallback()
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
invalidate()
|
||||
rebuildCallback()
|
||||
}
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
invalidate()
|
||||
rebuildCallback()
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
invalidate()
|
||||
rebuildCallback()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private val asyncDiffer = AsyncPagedListDiffer<T>(
|
||||
updateCallback,
|
||||
AsyncDifferConfig.Builder<T>(
|
||||
itemDiffCallback
|
||||
).also { builder ->
|
||||
if (diffExecutor != null) {
|
||||
builder.setBackgroundThreadExecutor(diffExecutor)
|
||||
}
|
||||
// we have to reply on this private API, otherwise, paged list might be changed when models are being built,
|
||||
// potentially creating concurrent modification problems.
|
||||
builder.setMainThreadExecutor { runnable: Runnable ->
|
||||
modelBuildingHandler.post(runnable)
|
||||
}
|
||||
}.build()
|
||||
)
|
||||
|
||||
fun submitList(pagedList: PagedList<T>?) {
|
||||
asyncDiffer.submitList(pagedList)
|
||||
}
|
||||
|
||||
fun getModels(): List<EpoxyModel<*>> {
|
||||
if (isCacheStale.compareAndSet(true, false)) {
|
||||
asyncDiffer.currentList?.forEachIndexed { position, _ ->
|
||||
buildModel(position)
|
||||
}
|
||||
}
|
||||
lastPosition?.let {
|
||||
triggerLoadAround(it)
|
||||
}
|
||||
return modelCache.keys.toList()
|
||||
}
|
||||
|
||||
fun loadAround(model: EpoxyModel<*>) {
|
||||
modelCache[model]?.let { itemPosition ->
|
||||
triggerLoadAround(itemPosition)
|
||||
lastPosition = itemPosition
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun invalidate() {
|
||||
modelCache.clear()
|
||||
isCacheStale.set(true)
|
||||
}
|
||||
|
||||
private fun cacheModelsAtPosition(itemPosition: Int, epoxyModels: Set<EpoxyModel<*>>) {
|
||||
epoxyModels.forEach {
|
||||
modelCache[it] = itemPosition
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildModel(pos: Int) {
|
||||
if (pos >= asyncDiffer.currentList?.size ?: 0) {
|
||||
return
|
||||
}
|
||||
modelBuilder(pos, asyncDiffer.currentList as List<T>).also {
|
||||
cacheModelsAtPosition(pos, it.toSet())
|
||||
}
|
||||
}
|
||||
|
||||
private fun triggerLoadAround(position: Int) {
|
||||
asyncDiffer.currentList?.let {
|
||||
if (it.size > 0) {
|
||||
it.loadAround(Math.min(position, it.size - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -37,8 +37,6 @@ import java.lang.ref.WeakReference
|
||||
* It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
|
||||
*/
|
||||
|
||||
private const val PILL_AVATAR_SIZE = 80
|
||||
|
||||
class PillImageSpan(private val glideRequests: GlideRequests,
|
||||
private val context: Context,
|
||||
private val userId: String,
|
||||
@ -55,7 +53,7 @@ class PillImageSpan(private val glideRequests: GlideRequests,
|
||||
@UiThread
|
||||
fun bind(textView: TextView) {
|
||||
tv = WeakReference(textView)
|
||||
AvatarRenderer.render(context, glideRequests, user?.avatarUrl, displayName, PILL_AVATAR_SIZE, target)
|
||||
AvatarRenderer.render(context, glideRequests, user?.avatarUrl, displayName, target)
|
||||
}
|
||||
|
||||
// ReplacementSpan *****************************************************************************
|
||||
@ -108,7 +106,7 @@ class PillImageSpan(private val glideRequests: GlideRequests,
|
||||
textStartPadding = textPadding
|
||||
setChipMinHeightResource(R.dimen.pill_min_height)
|
||||
setChipIconSizeResource(R.dimen.pill_avatar_size)
|
||||
chipIcon = AvatarRenderer.getCachedOrPlaceholder(context, glideRequests, user?.avatarUrl, displayName, PILL_AVATAR_SIZE)
|
||||
chipIcon = AvatarRenderer.getPlaceholderDrawable(context, displayName)
|
||||
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"/>
|
||||
android:layout_height="0dp" />
|
@ -3,7 +3,7 @@
|
||||
|
||||
<!-- DARK THEME COLORS -->
|
||||
|
||||
<style name="AppTheme.Base.Dark" parent="Theme.AppCompat.NoActionBar">
|
||||
<style name="AppTheme.Base.Dark" parent="Theme.MaterialComponents.NoActionBar.Bridge">
|
||||
<item name="colorPrimaryDark">@color/primary_color_dark_dark</item>
|
||||
<item name="colorPrimary">@color/primary_color_dark</item>
|
||||
<item name="colorAccent">@color/accent_color_dark</item>
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
<!-- LIGHT THEME COLORS -->
|
||||
|
||||
<style name="AppTheme.Base.Light" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<style name="AppTheme.Base.Light" parent="Theme.MaterialComponents.Light.NoActionBar.Bridge">
|
||||
<item name="colorPrimaryDark">@color/primary_color_dark_light</item>
|
||||
<item name="colorPrimary">@color/primary_color_light</item>
|
||||
<item name="colorAccent">@color/accent_color_light</item>
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
<!-- STATUS.IM THEME COLORS -->
|
||||
|
||||
<style name="AppTheme.Base.Status" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<style name="AppTheme.Base.Status" parent="Theme.MaterialComponents.Light.NoActionBar.Bridge">
|
||||
<item name="colorPrimaryDark">@color/primary_color_dark_status</item>
|
||||
<item name="colorPrimary">@color/primary_color_status</item>
|
||||
<item name="colorAccent">@color/accent_color_status</item>
|
||||
@ -21,7 +21,8 @@
|
||||
|
||||
<!-- default background color -->
|
||||
<item name="android:colorBackground">@color/riot_primary_background_color_status</item>
|
||||
<item name="vctr_bottom_nav_background_color">@color/riot_primary_background_color_status</item>
|
||||
<item name="vctr_bottom_nav_background_color">@color/riot_primary_background_color_status
|
||||
</item>
|
||||
|
||||
<!-- waiting view background -->
|
||||
<item name="vctr_waiting_background_color">#AAAAAAAA</item>
|
||||
@ -108,7 +109,8 @@
|
||||
<item name="vctr_tabbar_background">@drawable/vector_tabbar_background_status</item>
|
||||
|
||||
<item name="vctr_pill_background_user_id">@drawable/pill_background_user_id_status</item>
|
||||
<item name="vctr_pill_background_room_alias">@drawable/pill_background_room_alias_status</item>
|
||||
<item name="vctr_pill_background_room_alias">@drawable/pill_background_room_alias_status
|
||||
</item>
|
||||
|
||||
<item name="vctr_pill_text_color_user_id">@color/riot_primary_text_color_status</item>
|
||||
<item name="vctr_pill_text_color_room_alias">@android:color/white</item>
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.20'
|
||||
ext.kotlin_version = '1.3.21'
|
||||
ext.koin_version = '1.0.2'
|
||||
repositories {
|
||||
google()
|
||||
@ -10,7 +10,7 @@ buildscript {
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
} }
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.3.1'
|
||||
classpath 'com.android.tools.build:gradle:3.3.2'
|
||||
classpath "com.airbnb.okreplay:gradle-plugin:1.4.0"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.6.2'
|
||||
|
@ -18,7 +18,6 @@ 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) {
|
||||
@ -27,10 +26,6 @@ class RxRoom(private val room: Room) {
|
||||
return room.roomSummary.asObservable()
|
||||
}
|
||||
|
||||
fun timeline(eventId: String? = null): Observable<TimelineData> {
|
||||
return room.timeline(eventId).asObservable()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Room.rx(): RxRoom {
|
||||
|
@ -28,6 +28,7 @@ android {
|
||||
targetSdkVersion 28
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
multiDexEnabled true
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
||||
@ -77,7 +78,7 @@ static def gitRevisionDate() {
|
||||
dependencies {
|
||||
|
||||
def arrow_version = "0.8.0"
|
||||
def support_version = '1.1.0-alpha01'
|
||||
def support_version = '1.1.0-alpha03'
|
||||
def moshi_version = '1.8.0'
|
||||
def lifecycle_version = '2.0.0'
|
||||
def coroutines_version = "1.0.1"
|
||||
@ -96,7 +97,7 @@ dependencies {
|
||||
// Network
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
|
||||
implementation 'com.novoda:merlin:1.1.6'
|
||||
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
||||
@ -106,11 +107,8 @@ dependencies {
|
||||
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'
|
||||
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
||||
|
||||
// Paging
|
||||
implementation 'androidx.paging:paging-runtime:2.0.0'
|
||||
|
||||
// Work
|
||||
implementation "android.arch.work:work-runtime-ktx:1.0.0-beta02"
|
||||
implementation "android.arch.work:work-runtime-ktx:1.0.0"
|
||||
|
||||
// FP
|
||||
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
||||
@ -137,17 +135,17 @@ dependencies {
|
||||
//testImplementation 'org.robolectric:shadows-support-v4:3.0'
|
||||
testImplementation "io.mockk:mockk:1.8.13.kotlin13"
|
||||
testImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
testImplementation "androidx.arch.core:core-testing:$lifecycle_version"
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
androidTestImplementation "org.koin:koin-test:$koin_version"
|
||||
androidTestImplementation 'androidx.test:core:1.1.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.1.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.1.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
|
||||
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
androidTestImplementation "io.mockk:mockk-android:1.8.13.kotlin13"
|
||||
androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version"
|
||||
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
|
||||
}
|
||||
|
@ -16,10 +16,9 @@
|
||||
|
||||
package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
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.helper.add
|
||||
import im.vector.matrix.android.internal.database.helper.addAll
|
||||
import im.vector.matrix.android.internal.database.helper.isUnlinked
|
||||
@ -27,6 +26,9 @@ import im.vector.matrix.android.internal.database.helper.lastStateIndex
|
||||
import im.vector.matrix.android.internal.database.helper.merge
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
|
||||
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeMessageEvent
|
||||
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeRoomMemberEvent
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.kotlin.createObject
|
||||
@ -35,9 +37,10 @@ import org.amshove.kluent.shouldBeTrue
|
||||
import org.amshove.kluent.shouldEqual
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.random.Random
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class ChunkEntityTest : InstrumentedTest {
|
||||
|
||||
private lateinit var monarchy: Monarchy
|
||||
@ -54,7 +57,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
fun add_shouldAdd_whenNotAlreadyIncluded() {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
val chunk: ChunkEntity = realm.createObject()
|
||||
val fakeEvent = createFakeEvent(false)
|
||||
val fakeEvent = createFakeMessageEvent()
|
||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.events.size shouldEqual 1
|
||||
}
|
||||
@ -64,7 +67,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
fun add_shouldNotAdd_whenAlreadyIncluded() {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
val chunk: ChunkEntity = realm.createObject()
|
||||
val fakeEvent = createFakeEvent(false)
|
||||
val fakeEvent = createFakeMessageEvent()
|
||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.events.size shouldEqual 1
|
||||
@ -75,7 +78,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
fun add_shouldStateIndexIncremented_whenStateEventIsAddedForward() {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
val chunk: ChunkEntity = realm.createObject()
|
||||
val fakeEvent = createFakeEvent(true)
|
||||
val fakeEvent = createFakeRoomMemberEvent()
|
||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1
|
||||
}
|
||||
@ -85,7 +88,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
fun add_shouldStateIndexNotIncremented_whenNoStateEventIsAdded() {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
val chunk: ChunkEntity = realm.createObject()
|
||||
val fakeEvent = createFakeEvent(false)
|
||||
val fakeEvent = createFakeMessageEvent()
|
||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0
|
||||
}
|
||||
@ -134,13 +137,13 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
val chunk2: ChunkEntity = realm.createObject()
|
||||
val eventsForChunk1 = createFakeListOfEvents(30)
|
||||
val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
|
||||
chunk1.isLast = true
|
||||
chunk2.isLast = false
|
||||
chunk1.isLastForward = true
|
||||
chunk2.isLastForward = 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()
|
||||
chunk1.isLastForward.shouldBeTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,15 +199,4 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun createFakeListOfEvents(size: Int = 10): List<Event> {
|
||||
return (0 until size).map { createFakeEvent(Random.nextBoolean()) }
|
||||
}
|
||||
|
||||
private fun createFakeEvent(asStateEvent: Boolean = false): Event {
|
||||
val eventId = Random.nextLong(System.currentTimeMillis()).toString()
|
||||
val type = if (asStateEvent) EventType.STATE_ROOM_NAME else EventType.MESSAGE
|
||||
return Event(type, eventId)
|
||||
}
|
||||
|
||||
}
|
@ -25,7 +25,7 @@ import kotlin.random.Random
|
||||
|
||||
internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask {
|
||||
|
||||
override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEvent> {
|
||||
override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
|
||||
val tokenChunkEvent = FakeTokenChunkEvent(
|
||||
Random.nextLong(System.currentTimeMillis()).toString(),
|
||||
@ -33,7 +33,6 @@ internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: T
|
||||
fakeEvents
|
||||
)
|
||||
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, PaginationDirection.BACKWARDS)
|
||||
.map { tokenChunkEvent }
|
||||
}
|
||||
|
||||
|
||||
|
@ -23,7 +23,7 @@ import kotlin.random.Random
|
||||
|
||||
internal class FakePaginationTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask {
|
||||
|
||||
override fun execute(params: PaginationTask.Params): Try<Boolean> {
|
||||
override fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
|
||||
val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents)
|
||||
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction)
|
||||
|
@ -17,9 +17,15 @@
|
||||
package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
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.RoomMember
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
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.model.ChunkEntity
|
||||
@ -30,27 +36,56 @@ import kotlin.random.Random
|
||||
|
||||
object RoomDataHelper {
|
||||
|
||||
private const val FAKE_TEST_SENDER = "@sender:test.org"
|
||||
private val EVENT_FACTORIES = hashMapOf(
|
||||
0 to { createFakeMessageEvent() },
|
||||
1 to { createFakeRoomMemberEvent() }
|
||||
)
|
||||
|
||||
fun createFakeListOfEvents(size: Int = 10): List<Event> {
|
||||
return (0 until size).map { createFakeEvent(Random.nextBoolean()) }
|
||||
return (0 until size).mapNotNull {
|
||||
val nextInt = Random.nextInt(EVENT_FACTORIES.size)
|
||||
EVENT_FACTORIES[nextInt]?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun createFakeEvent(asStateEvent: Boolean = false): Event {
|
||||
val eventId = Random.nextLong(System.currentTimeMillis()).toString()
|
||||
val type = if (asStateEvent) EventType.STATE_ROOM_NAME else EventType.MESSAGE
|
||||
return Event(type, eventId)
|
||||
fun createFakeEvent(type: String,
|
||||
content: Content? = null,
|
||||
prevContent: Content? = null,
|
||||
sender: String = FAKE_TEST_SENDER,
|
||||
stateKey: String = FAKE_TEST_SENDER
|
||||
): Event {
|
||||
return Event(
|
||||
type = type,
|
||||
eventId = Random.nextLong().toString(),
|
||||
content = content,
|
||||
prevContent = prevContent,
|
||||
sender = sender,
|
||||
stateKey = stateKey
|
||||
)
|
||||
}
|
||||
|
||||
fun createFakeMessageEvent(): Event {
|
||||
val message = MessageTextContent(MessageType.MSGTYPE_TEXT, "Fake message #${Random.nextLong()}").toContent()
|
||||
return createFakeEvent(EventType.MESSAGE, message)
|
||||
}
|
||||
|
||||
fun createFakeRoomMemberEvent(): Event {
|
||||
val roomMember = RoomMember(Membership.JOIN, "Fake name #${Random.nextLong()}").toContent()
|
||||
return createFakeEvent(EventType.STATE_ROOM_MEMBER, roomMember)
|
||||
}
|
||||
|
||||
fun fakeInitialSync(monarchy: Monarchy, roomId: String) {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
val roomEntity = realm.createObject<RoomEntity>(roomId)
|
||||
roomEntity.membership = MyMembership.JOINED
|
||||
val eventList = createFakeListOfEvents(30)
|
||||
val eventList = createFakeListOfEvents(10)
|
||||
val chunkEntity = realm.createObject<ChunkEntity>().apply {
|
||||
nextToken = null
|
||||
prevToken = Random.nextLong(System.currentTimeMillis()).toString()
|
||||
isLast = true
|
||||
isLastForward = true
|
||||
}
|
||||
chunkEntity.addAll("roomId", eventList, PaginationDirection.FORWARDS)
|
||||
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS)
|
||||
roomEntity.addOrUpdate(chunkEntity)
|
||||
}
|
||||
}
|
||||
|
@ -16,61 +16,75 @@
|
||||
|
||||
package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.test.annotation.UiThreadTest
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.LiveDataTestObserver
|
||||
import im.vector.matrix.android.MainThreadExecutor
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
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.DefaultTimeline
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
||||
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
|
||||
import io.realm.RealmConfiguration
|
||||
import org.amshove.kluent.shouldEqual
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
internal class TimelineHolderTest : InstrumentedTest {
|
||||
internal class TimelineTest : InstrumentedTest {
|
||||
|
||||
companion object {
|
||||
private const val ROOM_ID = "roomId"
|
||||
}
|
||||
|
||||
@get:Rule val testRule = InstantTaskExecutorRule()
|
||||
private lateinit var monarchy: Monarchy
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
Realm.init(context())
|
||||
val testConfiguration = RealmConfiguration.Builder().name("test-realm").build()
|
||||
Realm.deleteRealm(testConfiguration)
|
||||
monarchy = Monarchy.Builder().setRealmConfiguration(testConfiguration).build()
|
||||
RoomDataHelper.fakeInitialSync(monarchy, ROOM_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
@UiThreadTest
|
||||
fun backPaginate_shouldLoadMoreEvents_whenLoadAroundIsCalled() {
|
||||
val roomId = "roomId"
|
||||
private fun createTimeline(initialEventId: String? = null): Timeline {
|
||||
val taskExecutor = TaskExecutor(testCoroutineDispatchers)
|
||||
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
|
||||
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
|
||||
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
|
||||
val boundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, PagingRequestHelper(MainThreadExecutor()))
|
||||
val roomMemberExtractor = RoomMemberExtractor(ROOM_ID)
|
||||
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
||||
return DefaultTimeline(ROOM_ID, initialEventId, monarchy.realmConfiguration, taskExecutor, getContextOfEventTask, timelineEventFactory, paginationTask, null)
|
||||
}
|
||||
|
||||
RoomDataHelper.fakeInitialSync(monarchy, roomId)
|
||||
val timelineHolder = DefaultTimelineService(roomId, monarchy, taskExecutor, boundaryCallback, getContextOfEventTask, RoomMemberExtractor(monarchy, roomId))
|
||||
val timelineObserver = LiveDataTestObserver.test(timelineHolder.timeline())
|
||||
timelineObserver.awaitNextValue().assertHasValue()
|
||||
var timelineData = timelineObserver.value()
|
||||
timelineData.events.size shouldEqual 30
|
||||
(0 until timelineData.events.size).map {
|
||||
timelineData.events.loadAround(it)
|
||||
@Test
|
||||
fun backPaginate_shouldLoadMoreEvents_whenPaginateIsCalled() {
|
||||
val timeline = createTimeline()
|
||||
timeline.start()
|
||||
val paginationCount = 30
|
||||
var initialLoad = 0
|
||||
val latch = CountDownLatch(2)
|
||||
var timelineEvents: List<TimelineEvent> = emptyList()
|
||||
timeline.listener = object : Timeline.Listener {
|
||||
override fun onUpdated(snapshot: List<TimelineEvent>) {
|
||||
if (snapshot.isNotEmpty()) {
|
||||
if (initialLoad == 0) {
|
||||
initialLoad = snapshot.size
|
||||
}
|
||||
timelineEvents = snapshot
|
||||
latch.countDown()
|
||||
timeline.paginate(Timeline.Direction.BACKWARDS, paginationCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
timelineObserver.awaitNextValue().assertHasValue()
|
||||
timelineData = timelineObserver.value()
|
||||
timelineData.events.size shouldEqual 60
|
||||
latch.await()
|
||||
timelineEvents.size shouldEqual initialLoad + paginationCount
|
||||
timeline.dispose()
|
||||
}
|
||||
|
||||
|
@ -50,11 +50,9 @@ class Matrix private constructor(context: Context) : MatrixKoinComponent {
|
||||
val authModule = AuthModule().definition
|
||||
MatrixKoinHolder.instance.loadModules(listOf(matrixModule, networkModule, authModule))
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
|
||||
val lastActiveSession = authenticator.getLastActiveSession()
|
||||
if (lastActiveSession != null) {
|
||||
currentSession = lastActiveSession.apply {
|
||||
open()
|
||||
}
|
||||
authenticator.getLastActiveSession()?.also {
|
||||
currentSession = it
|
||||
it.open()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,18 @@ inline fun <reified T> Content?.toModel(): T? {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This methods is a facility method to map a model to a json Content
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T> T?.toContent(): Content? {
|
||||
return this?.let {
|
||||
val moshi = MoshiProvider.providesMoshi()
|
||||
val moshiAdapter = moshi.adapter(T::class.java)
|
||||
return moshiAdapter.toJsonValue(it) as Content
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic event class with all possible fields for events.
|
||||
* The content and prevContent json fields can easily be mapped to a model with [toModel] method.
|
||||
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * 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.matrix.android.api.session.room.timeline
|
||||
|
||||
/**
|
||||
* A Timeline instance represents a contiguous sequence of events in a room.
|
||||
* <p>
|
||||
* There are two kinds of timeline:
|
||||
* <p>
|
||||
* - live timelines: they process live events from the sync. You can paginate
|
||||
* backwards but not forwards.
|
||||
* <p>
|
||||
* - past timelines: they start in the past from an `initialEventId`. You can paginate
|
||||
* backwards and forwards.
|
||||
*
|
||||
*/
|
||||
interface Timeline {
|
||||
|
||||
var listener: Timeline.Listener?
|
||||
|
||||
/**
|
||||
* This should be called before any other method after creating the timeline. It ensures the underlying database is open
|
||||
*/
|
||||
fun start()
|
||||
|
||||
/**
|
||||
* This should be called when you don't need the timeline. It ensures the underlying database get closed.
|
||||
*/
|
||||
fun dispose()
|
||||
|
||||
/**
|
||||
* Check if the timeline can be enriched by paginating.
|
||||
* @param the direction to check in
|
||||
* @return true if timeline can be enriched
|
||||
*/
|
||||
fun hasMoreToLoad(direction: Direction): Boolean
|
||||
|
||||
/**
|
||||
* This is the main method to enrich the timeline with new data.
|
||||
* It will call the onUpdated method from [Listener] when the data will be processed.
|
||||
* It also ensures only one pagination by direction is launched at a time, so you can safely call this multiple time in a row.
|
||||
*/
|
||||
fun paginate(direction: Direction, count: Int)
|
||||
|
||||
|
||||
interface Listener {
|
||||
/**
|
||||
* Call when the timeline has been updated through pagination or sync.
|
||||
* @param snapshot the most uptodate snapshot
|
||||
*/
|
||||
fun onUpdated(snapshot: List<TimelineEvent>)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to paginate in one or another direction.
|
||||
*/
|
||||
enum class Direction {
|
||||
/**
|
||||
* It represents future events.
|
||||
*/
|
||||
FORWARDS,
|
||||
/**
|
||||
* It represents past events.
|
||||
*/
|
||||
BACKWARDS
|
||||
}
|
||||
|
||||
}
|
@ -27,6 +27,7 @@ import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
data class TimelineEvent(
|
||||
val root: Event,
|
||||
val localId: String,
|
||||
val displayIndex: Int,
|
||||
val roomMember: RoomMember?
|
||||
) {
|
||||
|
||||
|
@ -16,20 +16,18 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.timeline
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
|
||||
/**
|
||||
* This interface defines methods to interact with the timeline. It's implemented at the room level.
|
||||
*/
|
||||
interface TimelineService {
|
||||
|
||||
/**
|
||||
* This is the main method of the service. It allows to listen for live [TimelineData].
|
||||
* It's automatically refreshed as soon as timeline data gets updated, through sync or pagination.
|
||||
*
|
||||
* @param eventId: an optional eventId to start loading timeline around.
|
||||
* @return the [LiveData] of [TimelineData]
|
||||
* Instantiate a [Timeline] with an optional initial eventId, to be used with permalink.
|
||||
* You can filter the type you want to grab with the allowedTypes param.
|
||||
* @param eventId the optional initial eventId.
|
||||
* @param allowedTypes the optional filter types
|
||||
* @return the instantiated timeline
|
||||
*/
|
||||
fun timeline(eventId: String? = null): LiveData<TimelineData>
|
||||
fun createTimeline(eventId: String?, allowedTypes: List<String>? = null): Timeline
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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.matrix.android.api.util
|
||||
|
||||
class CancelableBag : Cancelable {
|
||||
|
||||
private val cancelableList = ArrayList<Cancelable>()
|
||||
|
||||
fun add(cancelable: Cancelable) {
|
||||
cancelableList.add(cancelable)
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
cancelableList.forEach { it.cancel() }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Cancelable.addTo(cancelables: CancelableBag) {
|
||||
cancelables.add(this)
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * 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.matrix.android.internal.database
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import io.realm.*
|
||||
|
||||
class RealmLiveData<T : RealmModel>(private val realmConfiguration: RealmConfiguration,
|
||||
private val query: (Realm) -> RealmQuery<T>) : LiveData<RealmResults<T>>() {
|
||||
|
||||
private val listener = RealmChangeListener<RealmResults<T>> { results ->
|
||||
value = results
|
||||
}
|
||||
|
||||
private var realm: Realm? = null
|
||||
private var results: RealmResults<T>? = null
|
||||
|
||||
override fun onActive() {
|
||||
val realm = Realm.getInstance(realmConfiguration)
|
||||
val results = query.invoke(realm).findAll()
|
||||
value = results
|
||||
results.addChangeListener(listener)
|
||||
this.realm = realm
|
||||
this.results = results
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
results?.removeChangeListener(listener)
|
||||
results = null
|
||||
realm?.close()
|
||||
realm = null
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicReference
|
||||
internal interface LiveEntityObserver {
|
||||
fun start()
|
||||
fun dispose()
|
||||
fun isStarted(): Boolean
|
||||
}
|
||||
|
||||
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val monarchy: Monarchy)
|
||||
@ -55,7 +56,9 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE
|
||||
override fun isStarted(): Boolean {
|
||||
return isStarted.get()
|
||||
}
|
||||
|
||||
private fun onChanged(realmResults: RealmResults<T>, changeSet: OrderedCollectionChangeSet) {
|
||||
val insertionIndexes = changeSet.insertions
|
||||
@ -64,9 +67,9 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val m
|
||||
val inserted = realmResults.filterIndexed { index, _ -> insertionIndexes.contains(index) }
|
||||
val updated = realmResults.filterIndexed { index, _ -> updateIndexes.contains(index) }
|
||||
val deleted = realmResults.filterIndexed { index, _ -> deletionIndexes.contains(index) }
|
||||
process(inserted, updated, deleted)
|
||||
processChanges(inserted, updated, deleted)
|
||||
}
|
||||
|
||||
abstract fun process(inserted: List<T>, updated: List<T>, deleted: List<T>)
|
||||
protected abstract fun processChanges(inserted: List<T>, updated: List<T>, deleted: List<T>)
|
||||
|
||||
}
|
@ -20,7 +20,6 @@ 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.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
|
||||
@ -29,18 +28,18 @@ import im.vector.matrix.android.internal.extensions.assertIsManaged
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
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.deleteOnCascade() {
|
||||
assertIsManaged()
|
||||
this.events.deleteAllFromRealm()
|
||||
this.deleteFromRealm()
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.merge(roomId: String,
|
||||
chunkToMerge: ChunkEntity,
|
||||
direction: PaginationDirection) {
|
||||
@ -55,11 +54,16 @@ internal fun ChunkEntity.merge(roomId: String,
|
||||
val eventsToMerge: List<EventEntity>
|
||||
if (direction == PaginationDirection.FORWARDS) {
|
||||
this.nextToken = chunkToMerge.nextToken
|
||||
this.isLast = chunkToMerge.isLast
|
||||
eventsToMerge = chunkToMerge.events.reversed()
|
||||
this.isLastForward = chunkToMerge.isLastForward
|
||||
this.forwardsStateIndex = chunkToMerge.forwardsStateIndex
|
||||
this.forwardsDisplayIndex = chunkToMerge.forwardsDisplayIndex
|
||||
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
||||
} else {
|
||||
this.prevToken = chunkToMerge.prevToken
|
||||
eventsToMerge = chunkToMerge.events
|
||||
this.isLastBackward = chunkToMerge.isLastBackward
|
||||
this.backwardsStateIndex = chunkToMerge.backwardsStateIndex
|
||||
this.backwardsDisplayIndex = chunkToMerge.backwardsDisplayIndex
|
||||
eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
}
|
||||
eventsToMerge.forEach {
|
||||
add(roomId, it.asDomain(), direction, isUnlinked = isUnlinked)
|
||||
@ -70,7 +74,7 @@ internal fun ChunkEntity.addAll(roomId: String,
|
||||
events: List<Event>,
|
||||
direction: PaginationDirection,
|
||||
stateIndexOffset: Int = 0,
|
||||
// Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk)
|
||||
// Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk)
|
||||
isUnlinked: Boolean = false) {
|
||||
assertIsManaged()
|
||||
events.forEach { event ->
|
||||
@ -78,10 +82,6 @@ internal fun ChunkEntity.addAll(roomId: String,
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.updateDisplayIndexes() {
|
||||
events.forEachIndexed { index, eventEntity -> eventEntity.displayIndex = index }
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.add(roomId: String,
|
||||
event: Event,
|
||||
direction: PaginationDirection,
|
||||
@ -92,24 +92,44 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
if (event.eventId.isNullOrEmpty() || events.fastContains(event.eventId)) {
|
||||
return
|
||||
}
|
||||
var currentDisplayIndex = lastDisplayIndex(direction, 0)
|
||||
if (direction == PaginationDirection.FORWARDS) {
|
||||
currentDisplayIndex += 1
|
||||
forwardsDisplayIndex = currentDisplayIndex
|
||||
} else {
|
||||
currentDisplayIndex -= 1
|
||||
backwardsDisplayIndex = currentDisplayIndex
|
||||
}
|
||||
var currentStateIndex = lastStateIndex(direction, defaultValue = stateIndexOffset)
|
||||
if (direction == PaginationDirection.FORWARDS && EventType.isStateEvent(event.type)) {
|
||||
currentStateIndex += 1
|
||||
forwardsStateIndex = currentStateIndex
|
||||
} else if (direction == PaginationDirection.BACKWARDS && events.isNotEmpty()) {
|
||||
val lastEventType = events.last()?.type ?: ""
|
||||
if (EventType.isStateEvent(lastEventType)) {
|
||||
currentStateIndex -= 1
|
||||
backwardsStateIndex = currentStateIndex
|
||||
}
|
||||
}
|
||||
val eventEntity = event.toEntity(roomId)
|
||||
eventEntity.updateWith(currentStateIndex, isUnlinked)
|
||||
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size
|
||||
events.add(position, eventEntity)
|
||||
val eventEntity = event.toEntity(roomId).apply {
|
||||
this.stateIndex = currentStateIndex
|
||||
this.isUnlinked = isUnlinked
|
||||
this.displayIndex = currentDisplayIndex
|
||||
}
|
||||
// We are not using the order of the list, but will be sorting with displayIndex field
|
||||
events.add(eventEntity)
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
||||
return when (direction) {
|
||||
PaginationDirection.FORWARDS -> forwardsDisplayIndex
|
||||
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
|
||||
} ?: defaultValue
|
||||
}
|
||||
|
||||
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
|
||||
PaginationDirection.BACKWARDS -> events.where().sort(EventEntityFields.STATE_INDEX, Sort.ASCENDING).findFirst()?.stateIndex
|
||||
PaginationDirection.FORWARDS -> forwardsStateIndex
|
||||
PaginationDirection.BACKWARDS -> backwardsStateIndex
|
||||
} ?: defaultValue
|
||||
}
|
@ -18,7 +18,6 @@ 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.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
|
||||
import im.vector.matrix.android.internal.database.query.fastContains
|
||||
@ -31,7 +30,6 @@ internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
|
||||
}
|
||||
|
||||
internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
|
||||
chunkEntity.updateDisplayIndexes()
|
||||
if (!chunks.contains(chunkEntity)) {
|
||||
chunks.add(chunkEntity)
|
||||
}
|
||||
@ -47,8 +45,10 @@ internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
|
||||
if (event.eventId == null || (filterDuplicates && fastContains(event.eventId))) {
|
||||
return@forEach
|
||||
}
|
||||
val eventEntity = event.toEntity(roomId)
|
||||
eventEntity.updateWith(stateIndex, isUnlinked)
|
||||
val eventEntity = event.toEntity(roomId).apply {
|
||||
this.stateIndex = stateIndex
|
||||
this.isUnlinked = isUnlinked
|
||||
}
|
||||
untimelinedStateEvents.add(eventEntity)
|
||||
}
|
||||
}
|
||||
|
@ -57,11 +57,6 @@ internal object EventMapper {
|
||||
|
||||
}
|
||||
|
||||
internal fun EventEntity.updateWith(stateIndex: Int, isUnlinked: Boolean) {
|
||||
this.stateIndex = stateIndex
|
||||
this.isUnlinked = isUnlinked
|
||||
}
|
||||
|
||||
internal fun EventEntity.asDomain(): Event {
|
||||
return EventMapper.map(this)
|
||||
}
|
||||
|
@ -19,12 +19,18 @@ package im.vector.matrix.android.internal.database.model
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmResults
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.LinkingObjects
|
||||
|
||||
internal open class ChunkEntity(var prevToken: String? = null,
|
||||
var nextToken: String? = null,
|
||||
var isLast: Boolean = false,
|
||||
var events: RealmList<EventEntity> = RealmList()
|
||||
internal open class ChunkEntity(@Index var prevToken: String? = null,
|
||||
@Index var nextToken: String? = null,
|
||||
var events: RealmList<EventEntity> = RealmList(),
|
||||
@Index var isLastForward: Boolean = false,
|
||||
@Index var isLastBackward: Boolean = false,
|
||||
var backwardsDisplayIndex: Int? = null,
|
||||
var forwardsDisplayIndex: Int? = null,
|
||||
var backwardsStateIndex: Int? = null,
|
||||
var forwardsStateIndex: Int? = null
|
||||
) : RealmObject() {
|
||||
|
||||
@LinkingObjects("chunks")
|
||||
|
@ -18,24 +18,25 @@ package im.vector.matrix.android.internal.database.model
|
||||
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmResults
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.LinkingObjects
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import java.util.*
|
||||
|
||||
internal open class EventEntity(@PrimaryKey var localId: String = UUID.randomUUID().toString(),
|
||||
var eventId: String = "",
|
||||
@Index var eventId: String = "",
|
||||
var roomId: String = "",
|
||||
var type: String = "",
|
||||
@Index var type: String = "",
|
||||
var content: String? = null,
|
||||
var prevContent: String? = null,
|
||||
var stateKey: String? = null,
|
||||
@Index var stateKey: String? = null,
|
||||
var originServerTs: Long? = null,
|
||||
var sender: String? = null,
|
||||
@Index var sender: String? = null,
|
||||
var age: Long? = 0,
|
||||
var redacts: String? = null,
|
||||
var stateIndex: Int = 0,
|
||||
var displayIndex: Int = 0,
|
||||
var isUnlinked: Boolean = false
|
||||
@Index var stateIndex: Int = 0,
|
||||
@Index var displayIndex: Int = 0,
|
||||
@Index var isUnlinked: Boolean = false
|
||||
) : RealmObject() {
|
||||
|
||||
enum class LinkFilterMode {
|
||||
|
@ -43,7 +43,7 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:
|
||||
|
||||
internal fun ChunkEntity.Companion.findLastLiveChunkFromRoom(realm: Realm, roomId: String): ChunkEntity? {
|
||||
return where(realm, roomId)
|
||||
.equalTo(ChunkEntityFields.IS_LAST, true)
|
||||
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@ internal fun EventEntity.Companion.latestEvent(realm: Realm,
|
||||
query?.not()?.`in`(EventEntityFields.TYPE, excludedTypes.toTypedArray())
|
||||
}
|
||||
return query
|
||||
?.sort(EventEntityFields.DISPLAY_INDEX)
|
||||
?.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
?.findFirst()
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ internal fun RealmQuery<EventEntity>.next(from: Int? = null, strict: Boolean = t
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
internal fun RealmQuery<EventEntity>.last(since: Int? = null, strict: Boolean = false): EventEntity? {
|
||||
internal fun RealmQuery<EventEntity>.prev(since: Int? = null, strict: Boolean = false): EventEntity? {
|
||||
if (since != null) {
|
||||
if (strict) {
|
||||
this.lessThan(EventEntityFields.STATE_INDEX, since)
|
||||
|
@ -35,7 +35,6 @@ import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayN
|
||||
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
|
||||
import im.vector.matrix.android.internal.session.user.DefaultUserService
|
||||
import im.vector.matrix.android.internal.session.user.UserEntityUpdater
|
||||
import im.vector.matrix.android.internal.session.user.UserModule
|
||||
import im.vector.matrix.android.internal.util.md5
|
||||
import io.realm.RealmConfiguration
|
||||
import org.koin.dsl.module.module
|
||||
@ -84,13 +83,17 @@ internal class SessionModule(private val sessionParams: SessionParams) {
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
RoomDisplayNameResolver(get(), get(), sessionParams.credentials)
|
||||
RoomDisplayNameResolver(get(), get(), get(), sessionParams.credentials)
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
RoomAvatarResolver(get(), sessionParams.credentials)
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
RoomSummaryUpdater(get(), get(), get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
DefaultRoomService(get(), get()) as RoomService
|
||||
}
|
||||
@ -112,11 +115,10 @@ internal class SessionModule(private val sessionParams: SessionParams) {
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
val roomSummaryUpdater = RoomSummaryUpdater(get(), get(), get(), get(), sessionParams.credentials)
|
||||
val groupSummaryUpdater = GroupSummaryUpdater(get())
|
||||
val eventsPruner = EventsPruner(get())
|
||||
val userEntityUpdater = UserEntityUpdater(get(), get(), get())
|
||||
listOf<LiveEntityObserver>(roomSummaryUpdater, groupSummaryUpdater, eventsPruner, userEntityUpdater)
|
||||
listOf<LiveEntityObserver>(groupSummaryUpdater, eventsPruner, userEntityUpdater)
|
||||
}
|
||||
|
||||
|
||||
|
@ -23,6 +23,7 @@ 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.EventEntity
|
||||
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
|
||||
@ -38,7 +39,7 @@ internal class GroupSummaryUpdater(monarchy: Monarchy
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
override fun process(inserted: List<GroupEntity>, updated: List<GroupEntity>, deleted: List<GroupEntity>) {
|
||||
override fun processChanges(inserted: List<GroupEntity>, updated: List<GroupEntity>, deleted: List<GroupEntity>) {
|
||||
val newGroupIds = inserted.map { it.groupId }
|
||||
val getGroupDataWorkerParams = GetGroupDataWorker.Params(newGroupIds)
|
||||
val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams)
|
||||
|
@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.room.read.ReadService
|
||||
import im.vector.matrix.android.api.session.room.send.SendService
|
||||
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.RealmLiveData
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
|
||||
@ -45,18 +46,16 @@ internal class DefaultRoom(
|
||||
|
||||
|
||||
) : Room,
|
||||
TimelineService by timelineService,
|
||||
SendService by sendService,
|
||||
ReadService by readService {
|
||||
TimelineService by timelineService,
|
||||
SendService by sendService,
|
||||
ReadService by readService {
|
||||
|
||||
override val roomSummary: LiveData<RoomSummary> by lazy {
|
||||
val liveData = monarchy
|
||||
.findAllMappedWithChanges(
|
||||
{ realm -> RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME) },
|
||||
{ from -> from.asDomain() })
|
||||
|
||||
Transformations.map(liveData) {
|
||||
it.first()
|
||||
val liveRealmData = RealmLiveData<RoomSummaryEntity>(monarchy.realmConfiguration) { realm ->
|
||||
RoomSummaryEntity.where(realm, roomId).isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
|
||||
}
|
||||
Transformations.map(liveRealmData) { results ->
|
||||
results.map { it.asDomain() }.first()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ import im.vector.matrix.android.api.session.room.model.RoomAvatarContent
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.query.last
|
||||
import im.vector.matrix.android.internal.database.query.prev
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.room.members.RoomMembers
|
||||
|
||||
@ -41,7 +41,7 @@ internal class RoomAvatarResolver(private val monarchy: Monarchy,
|
||||
var res: String? = null
|
||||
monarchy.doWithRealm { realm ->
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).last()?.asDomain()
|
||||
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_AVATAR).prev()?.asDomain()
|
||||
res = roomName?.content.toModel<RoomAvatarContent>()?.avatarUrl
|
||||
if (!res.isNullOrEmpty()) {
|
||||
return@doWithRealm
|
||||
|
@ -28,10 +28,8 @@ import im.vector.matrix.android.internal.session.room.send.EventFactory
|
||||
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
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.util.PagingRequestHelper
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val monarchy: Monarchy,
|
||||
@ -43,10 +41,9 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||
private val taskExecutor: TaskExecutor) {
|
||||
|
||||
fun instantiate(roomId: String): Room {
|
||||
val helper = PagingRequestHelper(Executors.newSingleThreadExecutor())
|
||||
val timelineBoundaryCallback = TimelineBoundaryCallback(roomId, taskExecutor, paginationTask, monarchy, helper)
|
||||
val roomMemberExtractor = RoomMemberExtractor(monarchy, roomId)
|
||||
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineBoundaryCallback, contextOfEventTask, roomMemberExtractor)
|
||||
val roomMemberExtractor = RoomMemberExtractor(roomId)
|
||||
val timelineEventFactory = TimelineEventFactory(roomMemberExtractor)
|
||||
val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask)
|
||||
val sendService = DefaultSendService(roomId, eventFactory, monarchy)
|
||||
val readService = DefaultReadService(roomId, monarchy, setReadMarkersTask, taskExecutor)
|
||||
return DefaultRoom(
|
||||
|
@ -22,11 +22,7 @@ import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTas
|
||||
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
||||
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
||||
import im.vector.matrix.android.internal.session.room.send.EventFactory
|
||||
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.GetContextOfEventTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
||||
import im.vector.matrix.android.internal.session.room.timeline.*
|
||||
import org.koin.dsl.module.module
|
||||
import retrofit2.Retrofit
|
||||
|
||||
@ -41,7 +37,7 @@ class RoomModule {
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
DefaultLoadRoomMembersTask(get(), get(), get()) as LoadRoomMembersTask
|
||||
DefaultLoadRoomMembersTask(get(), get(), get(), get()) as LoadRoomMembersTask
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
@ -57,7 +53,7 @@ class RoomModule {
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
DefaultSetReadMarkersTask(get(), get(),get()) as SetReadMarkersTask
|
||||
DefaultSetReadMarkersTask(get(), get(), get()) as SetReadMarkersTask
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
|
@ -1,74 +1,79 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * 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.
|
||||
*
|
||||
* 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.matrix.android.internal.session.room
|
||||
|
||||
import android.content.Context
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.query.last
|
||||
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||
import im.vector.matrix.android.internal.database.query.prev
|
||||
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 im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
|
||||
internal class RoomSummaryUpdater(monarchy: Monarchy,
|
||||
internal class RoomSummaryUpdater(private val credentials: Credentials,
|
||||
private val roomDisplayNameResolver: RoomDisplayNameResolver,
|
||||
private val roomAvatarResolver: RoomAvatarResolver,
|
||||
private val context: Context,
|
||||
private val credentials: Credentials
|
||||
) : RealmLiveEntityObserver<RoomEntity>(monarchy) {
|
||||
private val roomAvatarResolver: RoomAvatarResolver) {
|
||||
|
||||
override val query = Monarchy.Query<RoomEntity> { RoomEntity.where(it) }
|
||||
fun update(realm: Realm,
|
||||
roomId: String,
|
||||
roomSummary: RoomSyncSummary? = null,
|
||||
unreadNotifications: RoomSyncUnreadNotifications? = null) {
|
||||
|
||||
override fun process(inserted: List<RoomEntity>, updated: List<RoomEntity>, deleted: List<RoomEntity>) {
|
||||
val rooms = (inserted + updated).map { it.roomId }
|
||||
monarchy.writeAsync { realm ->
|
||||
rooms.forEach { updateRoom(realm, it) }
|
||||
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
if (roomSummary != null) {
|
||||
if (roomSummary.heroes.isNotEmpty()) {
|
||||
roomSummaryEntity.heroes.clear()
|
||||
roomSummaryEntity.heroes.addAll(roomSummary.heroes)
|
||||
}
|
||||
if (roomSummary.invitedMembersCount != null) {
|
||||
roomSummaryEntity.invitedMembersCount = roomSummary.invitedMembersCount
|
||||
}
|
||||
if (roomSummary.joinedMembersCount != null) {
|
||||
roomSummaryEntity.joinedMembersCount = roomSummary.joinedMembersCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRoom(realm: Realm, roomId: String?) {
|
||||
if (roomId == null) {
|
||||
return
|
||||
if (unreadNotifications?.highlightCount != null) {
|
||||
roomSummaryEntity.highlightCount = unreadNotifications.highlightCount
|
||||
}
|
||||
if (unreadNotifications?.notificationCount != null) {
|
||||
roomSummaryEntity.notificationCount = unreadNotifications.notificationCount
|
||||
}
|
||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
val lastEvent = EventEntity.latestEvent(realm, roomId, includedTypes = listOf(EventType.MESSAGE))
|
||||
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).last()?.asDomain()
|
||||
|
||||
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()?.asDomain()
|
||||
val otherRoomMembers = RoomMembers(realm, roomId).getLoaded().filterKeys { it != credentials.userId }
|
||||
|
||||
roomSummary.displayName = roomDisplayNameResolver.resolve(context, roomId).toString()
|
||||
roomSummary.avatarUrl = roomAvatarResolver.resolve(roomId)
|
||||
roomSummary.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
|
||||
roomSummary.lastMessage = lastEvent
|
||||
roomSummary.otherMemberIds.clear()
|
||||
roomSummary.otherMemberIds.addAll(otherRoomMembers.keys)
|
||||
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
|
||||
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
|
||||
roomSummaryEntity.topic = lastTopicEvent?.content.toModel<RoomTopicContent>()?.topic
|
||||
roomSummaryEntity.lastMessage = lastEvent
|
||||
roomSummaryEntity.otherMemberIds.clear()
|
||||
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers.keys)
|
||||
}
|
||||
|
||||
}
|
@ -24,9 +24,11 @@ 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.room.RoomSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||
import io.realm.kotlin.createObject
|
||||
|
||||
internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Boolean> {
|
||||
|
||||
@ -38,17 +40,17 @@ internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Boolea
|
||||
|
||||
internal class DefaultLoadRoomMembersTask(private val roomAPI: RoomAPI,
|
||||
private val monarchy: Monarchy,
|
||||
private val syncTokenStore: SyncTokenStore
|
||||
private val syncTokenStore: SyncTokenStore,
|
||||
private val roomSummaryUpdater: RoomSummaryUpdater
|
||||
) : LoadRoomMembersTask {
|
||||
|
||||
override fun execute(params: LoadRoomMembersTask.Params): Try<Boolean> {
|
||||
return if (areAllMembersAlreadyLoaded(params.roomId)) {
|
||||
Try.just(true)
|
||||
} else {
|
||||
//TODO use this token
|
||||
val lastToken = syncTokenStore.getLastToken()
|
||||
executeRequest<RoomMembersResponse> {
|
||||
apiCall = roomAPI.getMembers(params.roomId, null, null, params.excludeMembership?.value)
|
||||
apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value)
|
||||
}.flatMap { response ->
|
||||
insertInDb(response, params.roomId)
|
||||
}.map { true }
|
||||
@ -60,22 +62,24 @@ 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")
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
val roomMembers = RoomMembers(realm, roomId).getLoaded()
|
||||
val eventsToInsert = response.roomMemberEvents.filter { !roomMembers.containsKey(it.stateKey) }
|
||||
|
||||
roomEntity.addStateEvents(eventsToInsert)
|
||||
roomEntity.areAllMembersLoaded = true
|
||||
|
||||
roomSummaryUpdater.update(realm, roomId)
|
||||
}
|
||||
.map { response }
|
||||
}
|
||||
|
||||
private fun areAllMembersAlreadyLoaded(roomId: String): Boolean {
|
||||
return monarchy
|
||||
.fetchAllCopiedSync { RoomEntity.where(it, roomId) }
|
||||
.firstOrNull()
|
||||
?.areAllMembersLoaded ?: false
|
||||
.fetchAllCopiedSync { RoomEntity.where(it, roomId) }
|
||||
.firstOrNull()
|
||||
?.areAllMembersLoaded ?: false
|
||||
}
|
||||
|
||||
}
|
@ -30,13 +30,14 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.query.last
|
||||
import im.vector.matrix.android.internal.database.query.prev
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
|
||||
/**
|
||||
* This class computes room display name
|
||||
*/
|
||||
internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
|
||||
internal class RoomDisplayNameResolver(private val context: Context,
|
||||
private val monarchy: Monarchy,
|
||||
private val roomMemberDisplayNameResolver: RoomMemberDisplayNameResolver,
|
||||
private val credentials: Credentials
|
||||
) {
|
||||
@ -44,11 +45,10 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
|
||||
/**
|
||||
* Compute the room display name
|
||||
*
|
||||
* @param context
|
||||
* @param roomId: the roomId to resolve the name of.
|
||||
* @return the room display name
|
||||
*/
|
||||
fun resolve(context: Context, roomId: String): CharSequence {
|
||||
fun resolve(roomId: String): CharSequence {
|
||||
// this algorithm is the one defined in
|
||||
// https://github.com/matrix-org/matrix-js-sdk/blob/develop/lib/models/room.js#L617
|
||||
// calculateRoomName(room, userId)
|
||||
@ -58,19 +58,19 @@ internal class RoomDisplayNameResolver(private val monarchy: Monarchy,
|
||||
var name: CharSequence? = null
|
||||
monarchy.doWithRealm { realm ->
|
||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
||||
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).last()?.asDomain()
|
||||
val roomName = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).prev()?.asDomain()
|
||||
name = roomName?.content.toModel<RoomNameContent>()?.name
|
||||
if (!name.isNullOrEmpty()) {
|
||||
return@doWithRealm
|
||||
}
|
||||
|
||||
val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).last()?.asDomain()
|
||||
val canonicalAlias = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev()?.asDomain()
|
||||
name = canonicalAlias?.content.toModel<RoomCanonicalAliasContent>()?.canonicalAlias
|
||||
if (!name.isNullOrEmpty()) {
|
||||
return@doWithRealm
|
||||
}
|
||||
|
||||
val aliases = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).last()?.asDomain()
|
||||
val aliases = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()?.asDomain()
|
||||
name = aliases?.content.toModel<RoomAliasesContent>()?.aliases?.firstOrNull()
|
||||
if (!name.isNullOrEmpty()) {
|
||||
return@doWithRealm
|
||||
|
@ -16,20 +16,19 @@
|
||||
|
||||
package im.vector.matrix.android.internal.session.room.members
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMember
|
||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||
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.last
|
||||
import im.vector.matrix.android.internal.database.query.next
|
||||
import im.vector.matrix.android.internal.database.query.prev
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
|
||||
internal class RoomMemberExtractor(private val monarchy: Monarchy,
|
||||
private val roomId: String) {
|
||||
internal class RoomMemberExtractor(private val roomId: String) {
|
||||
|
||||
private val cached = HashMap<String, RoomMember?>()
|
||||
|
||||
@ -44,30 +43,25 @@ internal class RoomMemberExtractor(private val monarchy: Monarchy,
|
||||
// When stateIndex is negative, we try to get the next stateEvent prevContent()
|
||||
// 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(event.realm, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent
|
||||
?: baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content
|
||||
} else {
|
||||
baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content
|
||||
baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content
|
||||
}
|
||||
val roomMember: RoomMember? = ContentMapper.map(content).toModel()
|
||||
cached[cacheKey] = roomMember
|
||||
return roomMember
|
||||
}
|
||||
|
||||
private fun baseQuery(monarchy: Monarchy,
|
||||
private fun baseQuery(realm: Realm,
|
||||
roomId: String,
|
||||
sender: String,
|
||||
isUnlinked: Boolean): RealmQuery<EventEntity> {
|
||||
|
||||
lateinit var query: RealmQuery<EventEntity>
|
||||
val filterMode = if (isUnlinked) EventEntity.LinkFilterMode.UNLINKED_ONLY else EventEntity.LinkFilterMode.LINKED_ONLY
|
||||
monarchy.doWithRealm { realm ->
|
||||
query = EventEntity
|
||||
.where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode)
|
||||
.equalTo(EventEntityFields.STATE_KEY, sender)
|
||||
}
|
||||
return query
|
||||
return EventEntity
|
||||
.where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode)
|
||||
.equalTo(EventEntityFields.STATE_KEY, sender)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -34,7 +34,7 @@ internal class EventsPruner(monarchy: Monarchy) :
|
||||
|
||||
override val query = Monarchy.Query<EventEntity> { EventEntity.where(it, type = EventType.REDACTION) }
|
||||
|
||||
override fun process(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
|
||||
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
|
||||
val redactionEvents = inserted.map { it.asDomain() }
|
||||
val pruneEventWorkerParams = PruneEventWorker.Params(redactionEvents)
|
||||
val workData = WorkerParamsFactory.toData(pruneEventWorkerParams)
|
||||
|
@ -56,8 +56,8 @@ internal class DefaultSetReadMarkersTask(private val roomAPI: RoomAPI,
|
||||
markers[READ_MARKER] = params.fullyReadEventId
|
||||
}
|
||||
if (params.readReceiptEventId != null
|
||||
&& MatrixPatterns.isEventId(params.readReceiptEventId)
|
||||
&& !isEventRead(params.roomId, params.readReceiptEventId)) {
|
||||
&& MatrixPatterns.isEventId(params.readReceiptEventId)
|
||||
&& !isEventRead(params.roomId, params.readReceiptEventId)) {
|
||||
|
||||
updateNotificationCountIfNecessary(params.roomId, params.readReceiptEventId)
|
||||
markers[READ_RECEIPT] = params.readReceiptEventId
|
||||
@ -76,7 +76,7 @@ internal class DefaultSetReadMarkersTask(private val roomAPI: RoomAPI,
|
||||
val isLatestReceived = EventEntity.latestEvent(realm, roomId)?.eventId == eventId
|
||||
if (isLatestReceived) {
|
||||
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
?: return@tryTransactionAsync
|
||||
?: return@tryTransactionAsync
|
||||
roomSummary.notificationCount = 0
|
||||
roomSummary.highlightCount = 0
|
||||
}
|
||||
@ -87,13 +87,14 @@ internal class DefaultSetReadMarkersTask(private val roomAPI: RoomAPI,
|
||||
var isEventRead = false
|
||||
monarchy.doWithRealm {
|
||||
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
|
||||
?: return@doWithRealm
|
||||
?: return@doWithRealm
|
||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
|
||||
?: return@doWithRealm
|
||||
?: return@doWithRealm
|
||||
val readReceiptIndex = liveChunk.events.find(readReceipt.eventId)?.displayIndex
|
||||
?: Int.MAX_VALUE
|
||||
val eventToCheckIndex = liveChunk.events.find(eventId)?.displayIndex ?: -1
|
||||
isEventRead = eventToCheckIndex >= readReceiptIndex
|
||||
?: Int.MIN_VALUE
|
||||
val eventToCheckIndex = liveChunk.events.find(eventId)?.displayIndex
|
||||
?: Int.MAX_VALUE
|
||||
isEventRead = eventToCheckIndex <= readReceiptIndex
|
||||
}
|
||||
return isEventRead
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.send.SendService
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.database.helper.add
|
||||
import im.vector.matrix.android.internal.database.helper.updateDisplayIndexes
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
@ -49,7 +48,6 @@ internal class DefaultSendService(private val roomId: String,
|
||||
val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||
?: return@tryTransactionAsync
|
||||
chunkEntity.add(roomId, event, PaginationDirection.FORWARDS)
|
||||
chunkEntity.updateDisplayIndexes()
|
||||
}
|
||||
|
||||
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
||||
|
@ -17,12 +17,12 @@
|
||||
package im.vector.matrix.android.internal.session.room.timeline
|
||||
|
||||
import arrow.core.Try
|
||||
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 GetContextOfEventTask : Task<GetContextOfEventTask.Params, TokenChunkEvent> {
|
||||
internal interface GetContextOfEventTask : Task<GetContextOfEventTask.Params, TokenChunkEventPersistor.Result> {
|
||||
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
@ -35,12 +35,12 @@ internal class DefaultGetContextOfEventTask(private val roomAPI: RoomAPI,
|
||||
private val tokenChunkEventPersistor: TokenChunkEventPersistor
|
||||
) : GetContextOfEventTask {
|
||||
|
||||
override fun execute(params: GetContextOfEventTask.Params): Try<EventContextResponse> {
|
||||
override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||
val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString()
|
||||
return executeRequest<EventContextResponse> {
|
||||
apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter)
|
||||
}.flatMap { response ->
|
||||
tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS).map { response }
|
||||
tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.FilterUtil
|
||||
|
||||
|
||||
internal interface PaginationTask : Task<PaginationTask.Params, Boolean> {
|
||||
internal interface PaginationTask : Task<PaginationTask.Params, TokenChunkEventPersistor.Result> {
|
||||
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
@ -38,7 +38,7 @@ internal class DefaultPaginationTask(private val roomAPI: RoomAPI,
|
||||
private val tokenChunkEventPersistor: TokenChunkEventPersistor
|
||||
) : PaginationTask {
|
||||
|
||||
override fun execute(params: PaginationTask.Params): Try<Boolean> {
|
||||
override fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||
val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString()
|
||||
return executeRequest<PaginationResponse> {
|
||||
apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter)
|
||||
|
@ -0,0 +1,448 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* *
|
||||
* * 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.matrix.android.internal.session.room.timeline
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.util.CancelableBag
|
||||
import im.vector.matrix.android.api.util.addTo
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
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.findIncludingEvent
|
||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import io.realm.Sort
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
|
||||
private const val INITIAL_LOAD_SIZE = 20
|
||||
private const val MIN_FETCHING_COUNT = 30
|
||||
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
|
||||
private const val THREAD_NAME = "TIMELINE_DB_THREAD"
|
||||
|
||||
internal class DefaultTimeline(
|
||||
private val roomId: String,
|
||||
private val initialEventId: String? = null,
|
||||
private val realmConfiguration: RealmConfiguration,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val contextOfEventTask: GetContextOfEventTask,
|
||||
private val timelineEventFactory: TimelineEventFactory,
|
||||
private val paginationTask: PaginationTask,
|
||||
private val allowedTypes: List<String>?
|
||||
) : Timeline {
|
||||
|
||||
override var listener: Timeline.Listener? = null
|
||||
set(value) {
|
||||
field = value
|
||||
backgroundHandler.get()?.post {
|
||||
val snapshot = snapshot()
|
||||
mainHandler.post { listener?.onUpdated(snapshot) }
|
||||
}
|
||||
}
|
||||
|
||||
private val isStarted = AtomicBoolean(false)
|
||||
private val isReady = AtomicBoolean(false)
|
||||
private val backgroundHandlerThread = AtomicReference<HandlerThread>()
|
||||
private val backgroundHandler = AtomicReference<Handler>()
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val backgroundRealm = AtomicReference<Realm>()
|
||||
private val cancelableBag = CancelableBag()
|
||||
|
||||
private lateinit var liveEvents: RealmResults<EventEntity>
|
||||
|
||||
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
||||
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
|
||||
private val isLive = initialEventId == null
|
||||
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
|
||||
|
||||
private val backwardsPaginationState = AtomicReference(PaginationState())
|
||||
private val forwardsPaginationState = AtomicReference(PaginationState())
|
||||
|
||||
private val eventsChangeListener = OrderedRealmCollectionChangeListener<RealmResults<EventEntity>> { _, changeSet ->
|
||||
// TODO HANDLE CHANGES
|
||||
changeSet.insertionRanges.forEach { range ->
|
||||
val (startDisplayIndex, direction) = if (range.startIndex == 0) {
|
||||
Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS)
|
||||
} else {
|
||||
Pair(liveEvents[range.startIndex]!!.displayIndex, Timeline.Direction.BACKWARDS)
|
||||
}
|
||||
val state = getPaginationState(direction)
|
||||
if (state.isPaginating) {
|
||||
// We are getting new items from pagination
|
||||
val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedCount)
|
||||
if (shouldPostSnapshot) {
|
||||
postSnapshot()
|
||||
}
|
||||
} else {
|
||||
// We are getting new items from sync
|
||||
buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
|
||||
postSnapshot()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public methods ******************************************************************************
|
||||
|
||||
override fun paginate(direction: Timeline.Direction, count: Int) {
|
||||
backgroundHandler.get()?.post {
|
||||
if (!canPaginate(direction)) {
|
||||
return@post
|
||||
}
|
||||
Timber.v("Paginate $direction of $count items")
|
||||
val startDisplayIndex = if (direction == Timeline.Direction.BACKWARDS) prevDisplayIndex else nextDisplayIndex
|
||||
val shouldPostSnapshot = paginateInternal(startDisplayIndex, direction, count)
|
||||
if (shouldPostSnapshot) {
|
||||
postSnapshot()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
if (isStarted.compareAndSet(false, true)) {
|
||||
Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId")
|
||||
val handlerThread = HandlerThread(THREAD_NAME + hashCode())
|
||||
handlerThread.start()
|
||||
val handler = Handler(handlerThread.looper)
|
||||
this.backgroundHandlerThread.set(handlerThread)
|
||||
this.backgroundHandler.set(handler)
|
||||
handler.post {
|
||||
val realm = Realm.getInstance(realmConfiguration)
|
||||
backgroundRealm.set(realm)
|
||||
clearUnlinkedEvents(realm)
|
||||
isReady.set(true)
|
||||
liveEvents = buildEventQuery(realm)
|
||||
.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
.findAll()
|
||||
.also { it.addChangeListener(eventsChangeListener) }
|
||||
handleInitialLoad()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
if (isStarted.compareAndSet(true, false)) {
|
||||
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
|
||||
backgroundHandler.get()?.post {
|
||||
cancelableBag.cancel()
|
||||
liveEvents.removeAllChangeListeners()
|
||||
backgroundRealm.getAndSet(null).also {
|
||||
it.close()
|
||||
}
|
||||
backgroundHandler.set(null)
|
||||
backgroundHandlerThread.getAndSet(null)?.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasMoreToLoad(direction: Timeline.Direction): Boolean {
|
||||
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
||||
}
|
||||
|
||||
// Private methods *****************************************************************************
|
||||
|
||||
private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
|
||||
val localRealm = Realm.getInstance(realmConfiguration)
|
||||
val eventEntity = buildEventQuery(localRealm).findFirst(direction) ?: return false
|
||||
val hasMoreInCache = if (direction == Timeline.Direction.FORWARDS) {
|
||||
val firstEvent = builtEvents.firstOrNull() ?: return true
|
||||
firstEvent.displayIndex < eventEntity.displayIndex
|
||||
} else {
|
||||
val lastEvent = builtEvents.lastOrNull() ?: return true
|
||||
lastEvent.displayIndex > eventEntity.displayIndex
|
||||
}
|
||||
localRealm.close()
|
||||
return hasMoreInCache
|
||||
}
|
||||
|
||||
private fun hasReachedEnd(direction: Timeline.Direction): Boolean {
|
||||
val localRealm = Realm.getInstance(realmConfiguration)
|
||||
val currentChunk = findCurrentChunk(localRealm) ?: return false
|
||||
val hasReachedEnd = if (direction == Timeline.Direction.FORWARDS) {
|
||||
currentChunk.isLastForward
|
||||
} else {
|
||||
val eventEntity = buildEventQuery(localRealm).findFirst(direction)
|
||||
currentChunk.isLastBackward || eventEntity?.type == EventType.STATE_ROOM_CREATE
|
||||
}
|
||||
localRealm.close()
|
||||
return hasReachedEnd
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This has to be called on TimelineThread as it access realm live results
|
||||
* @return true if snapshot should be posted
|
||||
*/
|
||||
private fun paginateInternal(startDisplayIndex: Int,
|
||||
direction: Timeline.Direction,
|
||||
count: Int): Boolean {
|
||||
updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) }
|
||||
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong())
|
||||
val shouldFetchMore = builtCount < count && !hasReachedEnd(direction)
|
||||
if (shouldFetchMore) {
|
||||
val newRequestedCount = count - builtCount
|
||||
updatePaginationState(direction) { it.copy(requestedCount = newRequestedCount) }
|
||||
val fetchingCount = Math.max(MIN_FETCHING_COUNT, newRequestedCount)
|
||||
executePaginationTask(direction, fetchingCount)
|
||||
} else {
|
||||
updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) }
|
||||
}
|
||||
return !shouldFetchMore
|
||||
}
|
||||
|
||||
private fun snapshot(): List<TimelineEvent> {
|
||||
return builtEvents.toList()
|
||||
}
|
||||
|
||||
private fun canPaginate(direction: Timeline.Direction): Boolean {
|
||||
return isReady.get() && !getPaginationState(direction).isPaginating && hasMoreToLoad(direction)
|
||||
}
|
||||
|
||||
private fun getPaginationState(direction: Timeline.Direction): PaginationState {
|
||||
return when (direction) {
|
||||
Timeline.Direction.FORWARDS -> forwardsPaginationState.get()
|
||||
Timeline.Direction.BACKWARDS -> backwardsPaginationState.get()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePaginationState(direction: Timeline.Direction, update: (PaginationState) -> PaginationState) {
|
||||
val stateReference = when (direction) {
|
||||
Timeline.Direction.FORWARDS -> forwardsPaginationState
|
||||
Timeline.Direction.BACKWARDS -> backwardsPaginationState
|
||||
}
|
||||
val currentValue = stateReference.get()
|
||||
val newValue = update(currentValue)
|
||||
stateReference.set(newValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* This has to be called on TimelineThread as it access realm live results
|
||||
*/
|
||||
private fun handleInitialLoad() {
|
||||
var shouldFetchInitialEvent = false
|
||||
val initialDisplayIndex = if (isLive) {
|
||||
liveEvents.firstOrNull()?.displayIndex
|
||||
} else {
|
||||
val initialEvent = liveEvents.where().equalTo(EventEntityFields.EVENT_ID, initialEventId).findFirst()
|
||||
shouldFetchInitialEvent = initialEvent == null
|
||||
initialEvent?.displayIndex
|
||||
} ?: DISPLAY_INDEX_UNKNOWN
|
||||
|
||||
prevDisplayIndex = initialDisplayIndex
|
||||
nextDisplayIndex = initialDisplayIndex
|
||||
if (initialEventId != null && shouldFetchInitialEvent) {
|
||||
fetchEvent(initialEventId)
|
||||
} else {
|
||||
val count = Math.min(INITIAL_LOAD_SIZE, liveEvents.size)
|
||||
if (isLive) {
|
||||
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
|
||||
} else {
|
||||
paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2)
|
||||
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2)
|
||||
}
|
||||
}
|
||||
postSnapshot()
|
||||
}
|
||||
|
||||
/**
|
||||
* This has to be called on TimelineThread as it access realm live results
|
||||
*/
|
||||
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
|
||||
val token = getTokenLive(direction) ?: return
|
||||
val params = PaginationTask.Params(roomId = roomId,
|
||||
from = token,
|
||||
direction = direction.toPaginationDirection(),
|
||||
limit = limit)
|
||||
|
||||
Timber.v("Should fetch $limit items $direction")
|
||||
paginationTask.configureWith(params)
|
||||
.enableRetry()
|
||||
.dispatchTo(object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
||||
if (data == TokenChunkEventPersistor.Result.SUCCESS) {
|
||||
Timber.v("Success fetching $limit items $direction from pagination request")
|
||||
} else {
|
||||
// Database won't be updated, so we force pagination request
|
||||
backgroundHandler.get()?.post {
|
||||
executePaginationTask(direction, limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.v("Failure fetching $limit items $direction from pagination request")
|
||||
}
|
||||
})
|
||||
.executeBy(taskExecutor)
|
||||
.addTo(cancelableBag)
|
||||
}
|
||||
|
||||
/**
|
||||
* This has to be called on TimelineThread as it access realm live results
|
||||
*/
|
||||
|
||||
private fun getTokenLive(direction: Timeline.Direction): String? {
|
||||
val chunkEntity = getLiveChunk() ?: return null
|
||||
return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This has to be called on TimelineThread as it access realm live results
|
||||
*/
|
||||
private fun getLiveChunk(): ChunkEntity? {
|
||||
return liveEvents.firstOrNull()?.chunk?.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* This has to be called on TimelineThread as it access realm live results
|
||||
* @return number of items who have been added
|
||||
*/
|
||||
private fun buildTimelineEvents(startDisplayIndex: Int,
|
||||
direction: Timeline.Direction,
|
||||
count: Long): Int {
|
||||
if (count < 1) {
|
||||
return 0
|
||||
}
|
||||
val offsetResults = getOffsetResults(startDisplayIndex, direction, count)
|
||||
if (offsetResults.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
val offsetIndex = offsetResults.last()!!.displayIndex
|
||||
if (direction == Timeline.Direction.BACKWARDS) {
|
||||
prevDisplayIndex = offsetIndex - 1
|
||||
} else {
|
||||
nextDisplayIndex = offsetIndex + 1
|
||||
}
|
||||
offsetResults.forEach { eventEntity ->
|
||||
val timelineEvent = timelineEventFactory.create(eventEntity)
|
||||
val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
|
||||
builtEvents.add(position, timelineEvent)
|
||||
}
|
||||
Timber.v("Built ${offsetResults.size} items from db")
|
||||
return offsetResults.size
|
||||
}
|
||||
|
||||
/**
|
||||
* This has to be called on TimelineThread as it access realm live results
|
||||
*/
|
||||
private fun getOffsetResults(startDisplayIndex: Int,
|
||||
direction: Timeline.Direction,
|
||||
count: Long): RealmResults<EventEntity> {
|
||||
val offsetQuery = liveEvents.where()
|
||||
if (direction == Timeline.Direction.BACKWARDS) {
|
||||
offsetQuery
|
||||
.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
.lessThanOrEqualTo(EventEntityFields.DISPLAY_INDEX, startDisplayIndex)
|
||||
} else {
|
||||
offsetQuery
|
||||
.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
||||
.greaterThanOrEqualTo(EventEntityFields.DISPLAY_INDEX, startDisplayIndex)
|
||||
}
|
||||
return offsetQuery
|
||||
.filterAllowedTypes()
|
||||
.limit(count)
|
||||
.findAll()
|
||||
}
|
||||
|
||||
|
||||
private fun buildEventQuery(realm: Realm): RealmQuery<EventEntity> {
|
||||
return if (initialEventId == null) {
|
||||
EventEntity
|
||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
|
||||
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST_FORWARD}", true)
|
||||
} else {
|
||||
EventEntity
|
||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
|
||||
.`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(initialEventId))
|
||||
}
|
||||
}
|
||||
|
||||
private fun findCurrentChunk(realm: Realm): ChunkEntity? {
|
||||
return if (initialEventId == null) {
|
||||
ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||
} else {
|
||||
ChunkEntity.findIncludingEvent(realm, initialEventId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearUnlinkedEvents(realm: Realm) {
|
||||
realm.executeTransaction {
|
||||
val unlinkedChunks = ChunkEntity
|
||||
.where(it, roomId = roomId)
|
||||
.equalTo(ChunkEntityFields.EVENTS.IS_UNLINKED, true)
|
||||
.findAll()
|
||||
unlinkedChunks.deleteAllFromRealm()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchEvent(eventId: String) {
|
||||
val params = GetContextOfEventTask.Params(roomId, eventId)
|
||||
contextOfEventTask.configureWith(params).executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
private fun postSnapshot() {
|
||||
val snapshot = snapshot()
|
||||
mainHandler.post { listener?.onUpdated(snapshot) }
|
||||
}
|
||||
|
||||
// Extension methods ***************************************************************************
|
||||
|
||||
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
||||
return if (this == Timeline.Direction.BACKWARDS) PaginationDirection.BACKWARDS else PaginationDirection.FORWARDS
|
||||
}
|
||||
|
||||
private fun RealmQuery<EventEntity>.findFirst(direction: Timeline.Direction): EventEntity? {
|
||||
return if (direction == Timeline.Direction.FORWARDS) {
|
||||
sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
} else {
|
||||
sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
|
||||
}
|
||||
.filterAllowedTypes()
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
private fun RealmQuery<EventEntity>.filterAllowedTypes(): RealmQuery<EventEntity> {
|
||||
if (allowedTypes != null) {
|
||||
`in`(EventEntityFields.TYPE, allowedTypes.toTypedArray())
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
private data class PaginationState(
|
||||
val isPaginating: Boolean = false,
|
||||
val requestedCount: Int = 0
|
||||
)
|
@ -16,129 +16,21 @@
|
||||
|
||||
package im.vector.matrix.android.internal.session.room.timeline
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.LivePagedListBuilder
|
||||
import androidx.paging.PagedList
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEventInterceptor
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineData
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
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
|
||||
private val timelineEventFactory: TimelineEventFactory,
|
||||
private val paginationTask: PaginationTask
|
||||
) : TimelineService {
|
||||
|
||||
private val eventInterceptors = ArrayList<TimelineEventInterceptor>()
|
||||
|
||||
override fun timeline(eventId: String?): LiveData<TimelineData> {
|
||||
clearUnlinkedEvents()
|
||||
val initialLoadKey = getInitialLoadKey(eventId)
|
||||
val realmDataSourceFactory = monarchy.createDataSourceFactory {
|
||||
buildDataSourceFactoryQuery(it, eventId)
|
||||
}
|
||||
val domainSourceFactory = realmDataSourceFactory
|
||||
.map { eventEntity ->
|
||||
val roomMember = roomMemberExtractor.extractFrom(eventEntity)
|
||||
TimelineEvent(eventEntity.asDomain(), eventEntity.localId, roomMember)
|
||||
}
|
||||
|
||||
val pagedListConfig = buildPagedListConfig()
|
||||
|
||||
val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig)
|
||||
.setBoundaryCallback(boundaryCallback)
|
||||
.setInitialLoadKey(initialLoadKey)
|
||||
|
||||
val eventsLiveData = monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
|
||||
|
||||
return LiveDataUtils.combine(eventsLiveData, boundaryCallback.status) { events, status ->
|
||||
val isLoadingForward = status.before == PagingRequestHelper.Status.RUNNING
|
||||
val isLoadingBackward = status.after == PagingRequestHelper.Status.RUNNING
|
||||
TimelineData(events, isLoadingForward, isLoadingBackward)
|
||||
}
|
||||
override fun createTimeline(eventId: String?, allowedTypes: List<String>?): Timeline {
|
||||
return DefaultTimeline(roomId, eventId, monarchy.realmConfiguration, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask, allowedTypes)
|
||||
}
|
||||
|
||||
// PRIVATE FUNCTIONS ***************************************************************************
|
||||
|
||||
private fun getInitialLoadKey(eventId: String?): Int {
|
||||
var initialLoadKey = 0
|
||||
if (eventId != null) {
|
||||
val indexOfEvent = indexOfEvent(eventId)
|
||||
if (indexOfEvent == EVENT_NOT_FOUND_INDEX) {
|
||||
fetchEvent(eventId)
|
||||
} else {
|
||||
initialLoadKey = indexOfEvent
|
||||
}
|
||||
}
|
||||
return initialLoadKey
|
||||
}
|
||||
|
||||
|
||||
private fun fetchEvent(eventId: String) {
|
||||
val params = GetContextOfEventTask.Params(roomId, eventId)
|
||||
contextOfEventTask.configureWith(params).executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
private fun buildPagedListConfig(): PagedList.Config {
|
||||
return PagedList.Config.Builder()
|
||||
.setEnablePlaceholders(false)
|
||||
.setPageSize(PAGE_SIZE)
|
||||
.setInitialLoadSizeHint(2 * PAGE_SIZE)
|
||||
.setPrefetchDistance(PREFETCH_DISTANCE)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun clearUnlinkedEvents() {
|
||||
monarchy.tryTransactionAsync { realm ->
|
||||
val unlinkedEvents = EventEntity
|
||||
.where(realm, roomId = roomId)
|
||||
.equalTo(EventEntityFields.IS_UNLINKED, true)
|
||||
.findAll()
|
||||
unlinkedEvents.deleteAllFromRealm()
|
||||
}
|
||||
}
|
||||
|
||||
private fun indexOfEvent(eventId: String): Int {
|
||||
var displayIndex = EVENT_NOT_FOUND_INDEX
|
||||
monarchy.doWithRealm {
|
||||
displayIndex = EventEntity.where(it, eventId = eventId).findFirst()?.displayIndex ?: EVENT_NOT_FOUND_INDEX
|
||||
}
|
||||
return displayIndex
|
||||
}
|
||||
|
||||
private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery<EventEntity> {
|
||||
val query = if (eventId == null) {
|
||||
EventEntity
|
||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
|
||||
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true)
|
||||
} else {
|
||||
EventEntity
|
||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
|
||||
.`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId))
|
||||
}
|
||||
return query.sort(EventEntityFields.DISPLAY_INDEX)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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.matrix.android.internal.session.room.timeline
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.timeline.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.util.PagingRequestHelper
|
||||
|
||||
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<TimelineEvent>() {
|
||||
|
||||
var limit = 30
|
||||
|
||||
val status = object : LiveData<PagingRequestHelper.StatusReport>() {
|
||||
|
||||
init {
|
||||
value = PagingRequestHelper.StatusReport.createDefault()
|
||||
}
|
||||
|
||||
val listener = PagingRequestHelper.Listener { postValue(it) }
|
||||
|
||||
override fun onActive() {
|
||||
helper.addListener(listener)
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
helper.removeListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onZeroItemsLoaded() {
|
||||
// actually, it's not possible
|
||||
}
|
||||
|
||||
override fun onItemAtEndLoaded(itemAtEnd: TimelineEvent) {
|
||||
val token = itemAtEnd.root.eventId?.let { getToken(it, PaginationDirection.BACKWARDS) }
|
||||
?: return
|
||||
|
||||
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
|
||||
executePaginationTask(it, token, PaginationDirection.BACKWARDS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemAtFrontLoaded(itemAtFront: TimelineEvent) {
|
||||
val token = itemAtFront.root.eventId?.let { getToken(it, PaginationDirection.FORWARDS) }
|
||||
?: return
|
||||
|
||||
helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) {
|
||||
executePaginationTask(it, token, PaginationDirection.FORWARDS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getToken(eventId: String, direction: PaginationDirection): String? {
|
||||
var token: String? = null
|
||||
monarchy.doWithRealm { realm ->
|
||||
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 = from,
|
||||
direction = direction,
|
||||
limit = limit)
|
||||
|
||||
paginationTask.configureWith(params)
|
||||
.enableRetry()
|
||||
.dispatchTo(object : MatrixCallback<Boolean> {
|
||||
override fun onSuccess(data: Boolean) {
|
||||
requestCallback.recordSuccess()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
requestCallback.recordFailure(failure)
|
||||
}
|
||||
})
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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.matrix.android.internal.session.room.timeline
|
||||
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
|
||||
|
||||
internal class TimelineEventFactory(private val roomMemberExtractor: RoomMemberExtractor) {
|
||||
|
||||
fun create(eventEntity: EventEntity): TimelineEvent {
|
||||
val roomMember = roomMemberExtractor.extractFrom(eventEntity)
|
||||
return TimelineEvent(
|
||||
eventEntity.asDomain(),
|
||||
eventEntity.localId,
|
||||
eventEntity.displayIndex,
|
||||
roomMember
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -18,7 +18,12 @@ 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.*
|
||||
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.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.query.create
|
||||
@ -26,6 +31,8 @@ import im.vector.matrix.android.internal.database.query.find
|
||||
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||
import io.realm.kotlin.createObject
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Insert Chunk in DB, and eventually merge with existing chunk event
|
||||
@ -94,17 +101,22 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
||||
* ========================================================================================================
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
enum class Result {
|
||||
SHOULD_FETCH_MORE,
|
||||
SUCCESS
|
||||
}
|
||||
|
||||
fun insertInDb(receivedChunk: TokenChunkEvent,
|
||||
roomId: String,
|
||||
direction: PaginationDirection): Try<Boolean> {
|
||||
direction: PaginationDirection): Try<Result> {
|
||||
|
||||
if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty()) {
|
||||
return Try.just(false)
|
||||
}
|
||||
return monarchy
|
||||
.tryTransactionSync { realm ->
|
||||
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
|
||||
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: throw IllegalStateException("You shouldn't use this method without a room")
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
val nextToken: String?
|
||||
val prevToken: String?
|
||||
@ -118,7 +130,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
||||
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
|
||||
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
|
||||
|
||||
// The current chunk is the one we will keep all along the merge process.
|
||||
// The current chunk is the one we will keep all along the merge processChanges.
|
||||
// We try to look for a chunk next to the token,
|
||||
// otherwise we create a whole new one
|
||||
|
||||
@ -127,28 +139,39 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
||||
} else {
|
||||
nextChunk?.apply { this.prevToken = prevToken }
|
||||
}
|
||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||
|
||||
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
||||
|
||||
// Then we merge chunks if needed
|
||||
if (currentChunk != prevChunk && prevChunk != null) {
|
||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
||||
} else if (currentChunk != nextChunk && nextChunk != null) {
|
||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, nextChunk)
|
||||
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
||||
Timber.v("Reach end of $roomId")
|
||||
currentChunk.isLastBackward = true
|
||||
} else {
|
||||
val newEventIds = receivedChunk.events.mapNotNull { it.eventId }
|
||||
ChunkEntity
|
||||
.findAllIncludingEvents(realm, newEventIds)
|
||||
.filter { it != currentChunk }
|
||||
.forEach { overlapped ->
|
||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, overlapped)
|
||||
}
|
||||
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
||||
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
||||
// Then we merge chunks if needed
|
||||
if (currentChunk != prevChunk && prevChunk != null) {
|
||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
||||
} else if (currentChunk != nextChunk && nextChunk != null) {
|
||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, nextChunk)
|
||||
} else {
|
||||
val newEventIds = receivedChunk.events.mapNotNull { it.eventId }
|
||||
ChunkEntity
|
||||
.findAllIncludingEvents(realm, newEventIds)
|
||||
.filter { it != currentChunk }
|
||||
.forEach { overlapped ->
|
||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, overlapped)
|
||||
}
|
||||
}
|
||||
roomEntity.addOrUpdate(currentChunk)
|
||||
roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = currentChunk.isUnlinked())
|
||||
}
|
||||
}
|
||||
.map {
|
||||
if (receivedChunk.events.isEmpty() && receivedChunk.stateEvents.isEmpty() && receivedChunk.start != receivedChunk.end) {
|
||||
Result.SHOULD_FETCH_MORE
|
||||
} else {
|
||||
Result.SUCCESS
|
||||
}
|
||||
roomEntity.addOrUpdate(currentChunk)
|
||||
roomEntity.addStateEvents(receivedChunk.stateEvents, isUnlinked = currentChunk.isUnlinked())
|
||||
}
|
||||
.map { true }
|
||||
}
|
||||
|
||||
private fun handleMerge(roomEntity: RoomEntity,
|
||||
@ -157,6 +180,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
||||
otherChunk: ChunkEntity): ChunkEntity {
|
||||
|
||||
// We always merge the bottom chunk into top chunk, so we are always merging backwards
|
||||
Timber.v("Merge ${currentChunk.prevToken} | ${currentChunk.nextToken} with ${otherChunk.prevToken} | ${otherChunk.nextToken}")
|
||||
return if (direction == PaginationDirection.BACKWARDS) {
|
||||
currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
|
||||
roomEntity.deleteOnCascade(otherChunk)
|
||||
|
@ -28,22 +28,17 @@ import im.vector.matrix.android.internal.database.helper.addStateEvents
|
||||
import im.vector.matrix.android.internal.database.helper.lastStateIndex
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.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.RoomSummaryUpdater
|
||||
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.RoomSyncAccountData
|
||||
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.RoomSyncUnreadNotifications
|
||||
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
|
||||
|
||||
internal class RoomSyncHandler(private val monarchy: Monarchy,
|
||||
private val readReceiptHandler: ReadReceiptHandler,
|
||||
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||
private val roomTagHandler: RoomTagHandler) {
|
||||
|
||||
sealed class HandlingStrategy {
|
||||
@ -76,7 +71,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()
|
||||
@ -107,14 +102,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
||||
)
|
||||
roomEntity.addOrUpdate(chunkEntity)
|
||||
}
|
||||
|
||||
if (roomSync.summary != null) {
|
||||
handleRoomSummary(realm, roomId, roomSync.summary)
|
||||
}
|
||||
|
||||
if (roomSync.unreadNotifications != null) {
|
||||
handleUnreadNotifications(realm, roomId, roomSync.unreadNotifications)
|
||||
}
|
||||
roomSummaryUpdater.update(realm, roomId, roomSync.summary, roomSync.unreadNotifications)
|
||||
|
||||
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
||||
handleEphemeral(realm, roomId, roomSync.ephemeral)
|
||||
@ -164,32 +152,12 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
||||
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
|
||||
}
|
||||
|
||||
lastChunk?.isLast = false
|
||||
chunkEntity.isLast = true
|
||||
lastChunk?.isLastForward = false
|
||||
chunkEntity.isLastForward = true
|
||||
chunkEntity.addAll(roomId, eventList, PaginationDirection.FORWARDS, stateIndexOffset)
|
||||
return chunkEntity
|
||||
}
|
||||
|
||||
private fun handleRoomSummary(realm: Realm,
|
||||
roomId: String,
|
||||
roomSummary: RoomSyncSummary) {
|
||||
|
||||
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
?: RoomSummaryEntity(roomId)
|
||||
|
||||
if (roomSummary.heroes.isNotEmpty()) {
|
||||
roomSummaryEntity.heroes.clear()
|
||||
roomSummaryEntity.heroes.addAll(roomSummary.heroes)
|
||||
}
|
||||
if (roomSummary.invitedMembersCount != null) {
|
||||
roomSummaryEntity.invitedMembersCount = roomSummary.invitedMembersCount
|
||||
}
|
||||
if (roomSummary.joinedMembersCount != null) {
|
||||
roomSummaryEntity.joinedMembersCount = roomSummary.joinedMembersCount
|
||||
}
|
||||
realm.insertOrUpdate(roomSummaryEntity)
|
||||
}
|
||||
|
||||
private fun handleEphemeral(realm: Realm,
|
||||
roomId: String,
|
||||
ephemeral: RoomSyncEphemeral) {
|
||||
@ -199,20 +167,6 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
||||
.flatMap { readReceiptHandler.handle(realm, roomId, it) }
|
||||
}
|
||||
|
||||
private fun handleUnreadNotifications(realm: Realm, roomId: String, unreadNotifications: RoomSyncUnreadNotifications) {
|
||||
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
?: RoomSummaryEntity(roomId)
|
||||
|
||||
if (unreadNotifications.highlightCount != null) {
|
||||
roomSummaryEntity.highlightCount = unreadNotifications.highlightCount
|
||||
}
|
||||
if (unreadNotifications.notificationCount != null) {
|
||||
roomSummaryEntity.notificationCount = unreadNotifications.notificationCount
|
||||
}
|
||||
realm.insertOrUpdate(roomSummaryEntity)
|
||||
|
||||
}
|
||||
|
||||
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
|
||||
accountData.events
|
||||
.filter { it.type == EventType.TAG }
|
||||
|
@ -40,7 +40,7 @@ internal class SyncModule {
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
RoomSyncHandler(get(), get(), get())
|
||||
RoomSyncHandler(get(), get(), get(), get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.sync
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
||||
import timber.log.Timber
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
internal class SyncResponseHandler(private val roomSyncHandler: RoomSyncHandler,
|
||||
private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
|
||||
@ -26,16 +27,19 @@ internal class SyncResponseHandler(private val roomSyncHandler: RoomSyncHandler,
|
||||
|
||||
fun handleResponse(syncResponse: SyncResponse, fromToken: String?, isCatchingUp: Boolean): Try<SyncResponse> {
|
||||
return Try {
|
||||
Timber.v("Handle sync response")
|
||||
if (syncResponse.rooms != null) {
|
||||
roomSyncHandler.handle(syncResponse.rooms)
|
||||
}
|
||||
if (syncResponse.groups != null) {
|
||||
groupSyncHandler.handle(syncResponse.groups)
|
||||
}
|
||||
if (syncResponse.accountData != null) {
|
||||
userAccountDataSyncHandler.handle(syncResponse.accountData)
|
||||
Timber.v("Start handling sync")
|
||||
val measure = measureTimeMillis {
|
||||
if (syncResponse.rooms != null) {
|
||||
roomSyncHandler.handle(syncResponse.rooms)
|
||||
}
|
||||
if (syncResponse.groups != null) {
|
||||
groupSyncHandler.handle(syncResponse.groups)
|
||||
}
|
||||
if (syncResponse.accountData != null) {
|
||||
userAccountDataSyncHandler.handle(syncResponse.accountData)
|
||||
}
|
||||
}
|
||||
Timber.v("Finish handling sync in $measure ms")
|
||||
syncResponse
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ internal class UserEntityUpdater(monarchy: Monarchy,
|
||||
|
||||
}
|
||||
|
||||
override fun process(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
|
||||
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
|
||||
val roomMembersEvents = inserted.map { it.eventId }
|
||||
val taskParams = UpdateUserTask.Params(roomMembersEvents)
|
||||
updateUserTask
|
||||
|
@ -1,530 +0,0 @@
|
||||
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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.matrix.android.internal.util;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* A helper class for {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}s and
|
||||
* {@link androidx.paging.DataSource}s to help with tracking network requests.
|
||||
* <p>
|
||||
* It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL},
|
||||
* {@link RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request
|
||||
* for each of them via {@link #runIfNotRunning(RequestType, Request)}.
|
||||
* <p>
|
||||
* It tracks a {@link Status} and an {@code error} for each {@link RequestType}.
|
||||
* <p>
|
||||
* A sample usage of this class to limit requests looks like this:
|
||||
* <pre>
|
||||
* class PagingBoundaryCallback extends PagedList.BoundaryCallback<MyItem> {
|
||||
* // TODO replace with an executor from your application
|
||||
* Executor executor = Executors.newSingleThreadExecutor();
|
||||
* PagingRequestHelper helper = new PagingRequestHelper(executor);
|
||||
* // imaginary API service, using Retrofit
|
||||
* MyApi api;
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void onItemAtFrontLoaded({@literal @}NonNull MyItem itemAtFront) {
|
||||
* helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE,
|
||||
* helperCallback -> api.getTopBefore(itemAtFront.getName(), 10).enqueue(
|
||||
* new Callback<ApiResponse>() {
|
||||
* {@literal @}Override
|
||||
* public void onResponse(Call<ApiResponse> call,
|
||||
* Response<ApiResponse> response) {
|
||||
* // TODO insert new records into database
|
||||
* helperCallback.recordSuccess();
|
||||
* }
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void onFailure(Call<ApiResponse> call, Throwable t) {
|
||||
* helperCallback.recordFailure(t);
|
||||
* }
|
||||
* }));
|
||||
* }
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void onItemAtEndLoaded({@literal @}NonNull MyItem itemAtEnd) {
|
||||
* helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER,
|
||||
* helperCallback -> api.getTopBefore(itemAtEnd.getName(), 10).enqueue(
|
||||
* new Callback<ApiResponse>() {
|
||||
* {@literal @}Override
|
||||
* public void onResponse(Call<ApiResponse> call,
|
||||
* Response<ApiResponse> response) {
|
||||
* // TODO insert new records into database
|
||||
* helperCallback.recordSuccess();
|
||||
* }
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void onFailure(Call<ApiResponse> call, Throwable t) {
|
||||
* helperCallback.recordFailure(t);
|
||||
* }
|
||||
* }));
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* <p>
|
||||
* The helper provides an API to observe combined request status, which can be reported back to the
|
||||
* application based on your business rules.
|
||||
* <pre>
|
||||
* MutableLiveData<PagingRequestHelper.Status> combined = new MutableLiveData<>();
|
||||
* helper.addListener(status -> {
|
||||
* // merge multiple states per request type into one, or dispatch separately depending on
|
||||
* // your application logic.
|
||||
* if (status.hasRunning()) {
|
||||
* combined.postValue(PagingRequestHelper.Status.RUNNING);
|
||||
* } else if (status.hasError()) {
|
||||
* // can also obtain the error via {@link StatusReport#getErrorFor(RequestType)}
|
||||
* combined.postValue(PagingRequestHelper.Status.FAILED);
|
||||
* } else {
|
||||
* combined.postValue(PagingRequestHelper.Status.SUCCESS);
|
||||
* }
|
||||
* });
|
||||
* </pre>
|
||||
*/
|
||||
// THIS class is likely to be moved into the library in a future release. Feel free to copy it
|
||||
// from this sample.
|
||||
public class PagingRequestHelper {
|
||||
private final Object mLock = new Object();
|
||||
private final Executor mRetryService;
|
||||
@GuardedBy("mLock")
|
||||
private final RequestQueue[] mRequestQueues = new RequestQueue[]
|
||||
{new RequestQueue(RequestType.INITIAL),
|
||||
new RequestQueue(RequestType.BEFORE),
|
||||
new RequestQueue(RequestType.AFTER)};
|
||||
@NonNull
|
||||
final CopyOnWriteArrayList<Listener> mListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
/**
|
||||
* Creates a new PagingRequestHelper with the given {@link Executor} which is used to run
|
||||
* retry actions.
|
||||
*
|
||||
* @param retryService The {@link Executor} that can run the retry actions.
|
||||
*/
|
||||
public PagingRequestHelper(@NonNull Executor retryService) {
|
||||
mRetryService = retryService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new listener that will be notified when any request changes {@link Status state}.
|
||||
*
|
||||
* @param listener The listener that will be notified each time a request's status changes.
|
||||
* @return True if it is added, false otherwise (e.g. it already exists in the list).
|
||||
*/
|
||||
@AnyThread
|
||||
public boolean addListener(@NonNull Listener listener) {
|
||||
return mListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given listener from the listeners list.
|
||||
*
|
||||
* @param listener The listener that will be removed.
|
||||
* @return True if the listener is removed, false otherwise (e.g. it never existed)
|
||||
*/
|
||||
public boolean removeListener(@NonNull Listener listener) {
|
||||
return mListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given {@link Request} if no other requests in the given request type is already
|
||||
* running.
|
||||
* <p>
|
||||
* If run, the request will be run in the current thread.
|
||||
*
|
||||
* @param type The type of the request.
|
||||
* @param request The request to run.
|
||||
* @return True if the request is run, false otherwise.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@AnyThread
|
||||
public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) {
|
||||
boolean hasListeners = !mListeners.isEmpty();
|
||||
StatusReport report = null;
|
||||
synchronized (mLock) {
|
||||
RequestQueue queue = mRequestQueues[type.ordinal()];
|
||||
if (queue.mRunning != null) {
|
||||
return false;
|
||||
}
|
||||
queue.mRunning = request;
|
||||
queue.mStatus = Status.RUNNING;
|
||||
queue.mFailed = null;
|
||||
queue.mLastError = null;
|
||||
if (hasListeners) {
|
||||
report = prepareStatusReportLocked();
|
||||
}
|
||||
}
|
||||
if (report != null) {
|
||||
dispatchReport(report);
|
||||
}
|
||||
final RequestWrapper wrapper = new RequestWrapper(request, this, type);
|
||||
wrapper.run();
|
||||
return true;
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private StatusReport prepareStatusReportLocked() {
|
||||
Throwable[] errors = new Throwable[]{
|
||||
mRequestQueues[0].mLastError,
|
||||
mRequestQueues[1].mLastError,
|
||||
mRequestQueues[2].mLastError
|
||||
};
|
||||
return new StatusReport(
|
||||
getStatusForLocked(RequestType.INITIAL),
|
||||
getStatusForLocked(RequestType.BEFORE),
|
||||
getStatusForLocked(RequestType.AFTER),
|
||||
errors
|
||||
);
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private Status getStatusForLocked(RequestType type) {
|
||||
return mRequestQueues[type.ordinal()].mStatus;
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@VisibleForTesting
|
||||
void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) {
|
||||
StatusReport report = null;
|
||||
final boolean success = throwable == null;
|
||||
boolean hasListeners = !mListeners.isEmpty();
|
||||
synchronized (mLock) {
|
||||
RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()];
|
||||
queue.mRunning = null;
|
||||
queue.mLastError = throwable;
|
||||
if (success) {
|
||||
queue.mFailed = null;
|
||||
queue.mStatus = Status.SUCCESS;
|
||||
} else {
|
||||
queue.mFailed = wrapper;
|
||||
queue.mStatus = Status.FAILED;
|
||||
}
|
||||
if (hasListeners) {
|
||||
report = prepareStatusReportLocked();
|
||||
}
|
||||
}
|
||||
if (report != null) {
|
||||
dispatchReport(report);
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchReport(StatusReport report) {
|
||||
for (Listener listener : mListeners) {
|
||||
listener.onStatusChange(report);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries all failed requests.
|
||||
*
|
||||
* @return True if any request is retried, false otherwise.
|
||||
*/
|
||||
public boolean retryAllFailed() {
|
||||
final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length];
|
||||
boolean retried = false;
|
||||
synchronized (mLock) {
|
||||
for (int i = 0; i < RequestType.values().length; i++) {
|
||||
toBeRetried[i] = mRequestQueues[i].mFailed;
|
||||
mRequestQueues[i].mFailed = null;
|
||||
}
|
||||
}
|
||||
for (RequestWrapper failed : toBeRetried) {
|
||||
if (failed != null) {
|
||||
failed.retry(mRetryService);
|
||||
retried = true;
|
||||
}
|
||||
}
|
||||
return retried;
|
||||
}
|
||||
|
||||
static class RequestWrapper implements Runnable {
|
||||
@NonNull
|
||||
final Request mRequest;
|
||||
@NonNull
|
||||
final PagingRequestHelper mHelper;
|
||||
@NonNull
|
||||
final RequestType mType;
|
||||
|
||||
RequestWrapper(@NonNull Request request, @NonNull PagingRequestHelper helper,
|
||||
@NonNull RequestType type) {
|
||||
mRequest = request;
|
||||
mHelper = helper;
|
||||
mType = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mRequest.run(new Request.Callback(this, mHelper));
|
||||
}
|
||||
|
||||
void retry(Executor service) {
|
||||
service.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mHelper.runIfNotRunning(mType, mRequest);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runner class that runs a request tracked by the {@link PagingRequestHelper}.
|
||||
* <p>
|
||||
* When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)}
|
||||
* or {@link Callback#recordSuccess()} once and only once. This call
|
||||
* can be made any time. Until that method call is made, {@link PagingRequestHelper} will
|
||||
* consider the request is running.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface Request {
|
||||
/**
|
||||
* Should run the request and call the given {@link Callback} with the result of the
|
||||
* request.
|
||||
*
|
||||
* @param callback The callback that should be invoked with the result.
|
||||
*/
|
||||
void run(Callback callback);
|
||||
|
||||
/**
|
||||
* Callback class provided to the {@link #run(Callback)} method to report the result.
|
||||
*/
|
||||
class Callback {
|
||||
private final AtomicBoolean mCalled = new AtomicBoolean();
|
||||
private final RequestWrapper mWrapper;
|
||||
private final PagingRequestHelper mHelper;
|
||||
|
||||
Callback(RequestWrapper wrapper, PagingRequestHelper helper) {
|
||||
mWrapper = wrapper;
|
||||
mHelper = helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method when the request succeeds and new data is fetched.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final void recordSuccess() {
|
||||
if (mCalled.compareAndSet(false, true)) {
|
||||
mHelper.recordResult(mWrapper, null);
|
||||
} else {
|
||||
throw new IllegalStateException(
|
||||
"already called recordSuccess or recordFailure");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method with the failure message and the request can be retried via
|
||||
* {@link #retryAllFailed()}.
|
||||
*
|
||||
* @param throwable The error that occured while carrying out the request.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final void recordFailure(@NonNull Throwable throwable) {
|
||||
//noinspection ConstantConditions
|
||||
if (throwable == null) {
|
||||
throw new IllegalArgumentException("You must provide a throwable describing"
|
||||
+ " the error to record the failure");
|
||||
}
|
||||
if (mCalled.compareAndSet(false, true)) {
|
||||
mHelper.recordResult(mWrapper, throwable);
|
||||
} else {
|
||||
throw new IllegalStateException(
|
||||
"already called recordSuccess or recordFailure");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class that holds the information about the current status of the ongoing requests
|
||||
* using this helper.
|
||||
*/
|
||||
public static final class StatusReport {
|
||||
/**
|
||||
* Status of the latest request that were submitted with {@link RequestType#INITIAL}.
|
||||
*/
|
||||
@NonNull
|
||||
public final Status initial;
|
||||
/**
|
||||
* Status of the latest request that were submitted with {@link RequestType#BEFORE}.
|
||||
*/
|
||||
@NonNull
|
||||
public final Status before;
|
||||
/**
|
||||
* Status of the latest request that were submitted with {@link RequestType#AFTER}.
|
||||
*/
|
||||
@NonNull
|
||||
public final Status after;
|
||||
@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;
|
||||
this.before = before;
|
||||
this.after = after;
|
||||
this.mErrors = errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to check if there are any running requests.
|
||||
*
|
||||
* @return True if there are any running requests, false otherwise.
|
||||
*/
|
||||
public boolean hasRunning() {
|
||||
return initial == Status.RUNNING
|
||||
|| before == Status.RUNNING
|
||||
|| after == Status.RUNNING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to check if there are any requests that resulted in an error.
|
||||
*
|
||||
* @return True if there are any requests that finished with error, false otherwise.
|
||||
*/
|
||||
public boolean hasError() {
|
||||
return initial == Status.FAILED
|
||||
|| before == Status.FAILED
|
||||
|| after == Status.FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error for the given request type.
|
||||
*
|
||||
* @param type The request type for which the error should be returned.
|
||||
* @return The {@link Throwable} returned by the failing request with the given type or
|
||||
* {@code null} if the request for the given type did not fail.
|
||||
*/
|
||||
@Nullable
|
||||
public Throwable getErrorFor(@NonNull RequestType type) {
|
||||
return mErrors[type.ordinal()];
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StatusReport{"
|
||||
+ "initial=" + initial
|
||||
+ ", before=" + before
|
||||
+ ", after=" + after
|
||||
+ ", mErrors=" + Arrays.toString(mErrors)
|
||||
+ '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
StatusReport that = (StatusReport) o;
|
||||
if (initial != that.initial) return false;
|
||||
if (before != that.before) return false;
|
||||
if (after != that.after) return false;
|
||||
// Probably incorrect - comparing Object[] arrays with Arrays.equals
|
||||
return Arrays.equals(mErrors, that.mErrors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = initial.hashCode();
|
||||
result = 31 * result + before.hashCode();
|
||||
result = 31 * result + after.hashCode();
|
||||
result = 31 * result + Arrays.hashCode(mErrors);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener interface to get notified by request status changes.
|
||||
*/
|
||||
public interface Listener {
|
||||
/**
|
||||
* Called when the status for any of the requests has changed.
|
||||
*
|
||||
* @param report The current status report that has all the information about the requests.
|
||||
*/
|
||||
void onStatusChange(@NonNull StatusReport report);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the status of a Request for each {@link RequestType}.
|
||||
*/
|
||||
public enum Status {
|
||||
/**
|
||||
* There is current a running request.
|
||||
*/
|
||||
RUNNING,
|
||||
/**
|
||||
* The last request has succeeded or no such requests have ever been run.
|
||||
*/
|
||||
SUCCESS,
|
||||
/**
|
||||
* The last request has failed.
|
||||
*/
|
||||
FAILED
|
||||
}
|
||||
|
||||
/**
|
||||
* Available request types.
|
||||
*/
|
||||
public enum RequestType {
|
||||
/**
|
||||
* Corresponds to an initial request made to a {@link androidx.paging.DataSource} or the empty state for
|
||||
* a {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
|
||||
*/
|
||||
INITIAL,
|
||||
/**
|
||||
* Corresponds to the {@code loadBefore} calls in {@link androidx.paging.DataSource} or
|
||||
* {@code onItemAtFrontLoaded} in
|
||||
* {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
|
||||
*/
|
||||
BEFORE,
|
||||
/**
|
||||
* Corresponds to the {@code loadAfter} calls in {@link androidx.paging.DataSource} or
|
||||
* {@code onItemAtEndLoaded} in
|
||||
* {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
|
||||
*/
|
||||
AFTER
|
||||
}
|
||||
|
||||
class RequestQueue {
|
||||
@NonNull
|
||||
final RequestType mRequestType;
|
||||
@Nullable
|
||||
RequestWrapper mFailed;
|
||||
@Nullable
|
||||
Request mRunning;
|
||||
@Nullable
|
||||
Throwable mLastError;
|
||||
@NonNull
|
||||
Status mStatus = Status.SUCCESS;
|
||||
|
||||
RequestQueue(@NonNull RequestType requestType) {
|
||||
mRequestType = requestType;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user