diff --git a/app/build.gradle b/app/build.gradle index 8dfa0371..9c561017 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/java/im/vector/riotredesign/Riot.kt b/app/src/main/java/im/vector/riotredesign/Riot.kt index 14b19602..fa5a769f 100644 --- a/app/src/main/java/im/vector/riotredesign/Riot.kt +++ b/app/src/main/java/im/vector/riotredesign/Riot.kt @@ -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()) diff --git a/app/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java b/app/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java index f88d4f80..08fed579 100644 --- a/app/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java +++ b/app/src/main/java/im/vector/riotredesign/core/glide/MyAppGlideModule.java @@ -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 {} \ No newline at end of file +public final class MyAppGlideModule extends AppGlideModule { + + @Override + public void applyOptions(Context context, GlideBuilder builder) { + builder.setLogLevel(Log.ERROR); + } + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt index 55bf748f..b64cadb1 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt @@ -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 */ + 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) { 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 { - 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 { + val resolvedUrl = Matrix.getInstance().currentSession!!.contentUrlResolver() + .resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) + + return glideRequest + .load(resolvedUrl) + .apply(RequestOptions.circleCropTransform()) } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index cb299d45..a013a0dc 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -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() } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt index 7e82c031..ad3cd4cf 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt @@ -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) { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index f3ca3b3d..f7a59f2d 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -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() } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 296918c7..30a262c4 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -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) { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index 4ca21b94..4ebf4984 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -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() + private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES) companion object : MvRxViewModelFactory { + const val PAGINATION_COUNT = 50 + @JvmStatic override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? { val currentSession = viewModelContext.activity.get() @@ -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 {}) } @@ -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() } - - } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt index ca2f39b7..4df1551c 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewState.kt @@ -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 = Uninitialized, val asyncTimelineData: Async = Uninitialized ) : MvRxState { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt index 719a570a..0888f672 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -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) } } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index d8558e6a..0c6b28e1 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -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( - 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>>() + private var currentSnapshot: List = 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): List> { - 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) { + submitSnapshot(snapshot) + } + + private fun submitSnapshot(newSnapshot: List) { + 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> { + (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): List> { val epoxyModels = ArrayList>() - 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>) { - 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) } } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/animation/TimelineItemAnimator.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/animation/TimelineItemAnimator.kt index 8a9615db..94fe199c 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/animation/TimelineItemAnimator.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/animation/TimelineItemAnimator.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index a490d8fb..8d03e42b 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -42,8 +42,6 @@ class MessageItemFactory(private val colorProvider: ColorProvider, private val timelineDateFormatter: TimelineDateFormatter, private val htmlRenderer: EventHtmlRenderer) { - private val messagesDisplayedWithInformation = HashSet() - 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 diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt new file mode 100644 index 00000000..c88bd7e9 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt @@ -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) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineAsyncHelper.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineAsyncHelper.kt new file mode 100644 index 00000000..a54d5f83 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineAsyncHelper.kt @@ -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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt new file mode 100644 index 00000000..565f4254 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -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.filterDisplayableEvents(): List { + return this.filter { + it.isDisplayable() + } +} + +fun List.nextDisplayableEvent(index: Int): TimelineEvent? { + return if (index == size - 1) { + null + } else { + subList(index + 1, this.size).firstOrNull { it.isDisplayable() } + } +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventDiffUtilCallback.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventDiffUtilCallback.kt new file mode 100644 index 00000000..cd62b511 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventDiffUtilCallback.kt @@ -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, + private val newList: List) : 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListEpoxyController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListEpoxyController.kt deleted file mode 100644 index 30f11a8b..00000000 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListEpoxyController.kt +++ /dev/null @@ -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( - /** - * The handler to use for building models. By default this uses the main thread, but you can use - * [EpoxyAsyncUtil.getAsyncBackgroundHandler] to do model building in the background. - * - * The notify thread of your PagedList (from setNotifyExecutor in the PagedList Builder) must be - * the same as this thread. Otherwise Epoxy will crash. - */ - modelBuildingHandler: Handler = EpoxyController.defaultModelBuildingHandler, - /** - * The handler to use when calculating the diff between built model lists. - * By default this uses the main thread, but you can use - * [EpoxyAsyncUtil.getAsyncBackgroundHandler] to do diffing in the background. - */ - diffingHandler: Handler = EpoxyController.defaultDiffingHandler, - /** - * [PagedListEpoxyController] uses an [DiffUtil.ItemCallback] to detect changes between - * [PagedList]s. By default, it relies on simple object equality but you can provide a custom - * one if you don't use all fields in the object in your models. - */ - itemDiffCallback: DiffUtil.ItemCallback = DEFAULT_ITEM_DIFF_CALLBACK as DiffUtil.ItemCallback -) : EpoxyController(modelBuildingHandler, diffingHandler) { - // this is where we keep the already built models - protected val modelCache = PagedListModelCache( - modelBuilder = { pos, item -> - buildItemModels(pos, item) - }, - rebuildCallback = { - requestModelBuild() - }, - itemDiffCallback = itemDiffCallback, - modelBuildingHandler = modelBuildingHandler - ) - - var currentList: PagedList? = null - private set - - final override fun buildModels() { - addModels(modelCache.getModels()) - } - - override fun onModelBound( - holder: EpoxyViewHolder, - boundModel: EpoxyModel<*>, - position: Int, - previouslyBoundModel: EpoxyModel<*>? - ) { - modelCache.loadAround(boundModel) - } - - /** - * This function adds all built models to the adapter. You can override this method to add extra - * items into the model list or remove some. - */ - open fun addModels(models: List>) { - super.add(models) - } - - /** - * Builds the model for a given item. This must return a single model for each item. If you want - * to inject headers etc, you can override [addModels] function. - * - * If the `item` is `null`, you should provide the placeholder. If your [PagedList] is configured - * without placeholders, you don't need to handle the `null` case. - */ - abstract fun buildItemModels(currentPosition: Int, items: List): List> - - /** - * Submit a new paged list. - * - * A diff will be calculated between this list and the previous list so you may still get calls - * to [buildItemModel] with items from the previous list. - */ - fun submitList(newList: PagedList?) { - currentList = newList - modelCache.submitList(newList) - } - - companion object { - /** - * [PagedListEpoxyController] calculates a diff on top of the PagedList to check which - * models are invalidated. - * This is the default [DiffUtil.ItemCallback] which uses object equality. - */ - val DEFAULT_ITEM_DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem - - override fun areContentsTheSame(oldItem: Any, newItem: Any) = oldItem == newItem - } - } -} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListModelCache.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListModelCache.kt deleted file mode 100644 index cbaf81a7..00000000 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/paging/PagedListModelCache.kt +++ /dev/null @@ -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( - private val modelBuilder: (itemIndex: Int, items: List) -> List>, - private val rebuildCallback: () -> Unit, - private val itemDiffCallback: DiffUtil.ItemCallback, - private val diffExecutor: Executor? = null, - private val modelBuildingHandler: Handler -) { - - - // Int is the index of the pagedList item - // We have to be able to find the pagedlist position coming from an epoxy model to trigger - // LoadAround with accuracy - private val modelCache = linkedMapOf, Int>() - private var isCacheStale = AtomicBoolean(true) - - /** - * Tracks the last accessed position so that we can report it back to the paged list when models are built. - */ - private var lastPosition: Int? = null - - /** - * Observer for the PagedList changes that invalidates the model cache when data is updated. - */ - private val updateCallback = object : ListUpdateCallback { - override fun onChanged(position: Int, count: Int, payload: Any?) { - invalidate() - rebuildCallback() - } - - override fun onMoved(fromPosition: Int, toPosition: Int) { - invalidate() - rebuildCallback() - } - - override fun onInserted(position: Int, count: Int) { - invalidate() - rebuildCallback() - } - - override fun onRemoved(position: Int, count: Int) { - invalidate() - rebuildCallback() - } - } - - @SuppressLint("RestrictedApi") - private val asyncDiffer = AsyncPagedListDiffer( - updateCallback, - AsyncDifferConfig.Builder( - itemDiffCallback - ).also { builder -> - if (diffExecutor != null) { - builder.setBackgroundThreadExecutor(diffExecutor) - } - // we have to reply on this private API, otherwise, paged list might be changed when models are being built, - // potentially creating concurrent modification problems. - builder.setMainThreadExecutor { runnable: Runnable -> - modelBuildingHandler.post(runnable) - } - }.build() - ) - - fun submitList(pagedList: PagedList?) { - asyncDiffer.submitList(pagedList) - } - - fun getModels(): List> { - if (isCacheStale.compareAndSet(true, false)) { - asyncDiffer.currentList?.forEachIndexed { position, _ -> - buildModel(position) - } - } - lastPosition?.let { - triggerLoadAround(it) - } - return modelCache.keys.toList() - } - - fun loadAround(model: EpoxyModel<*>) { - modelCache[model]?.let { itemPosition -> - triggerLoadAround(itemPosition) - lastPosition = itemPosition - } - } - - // PRIVATE METHODS ***************************************************************************** - - private fun invalidate() { - modelCache.clear() - isCacheStale.set(true) - } - - private fun cacheModelsAtPosition(itemPosition: Int, epoxyModels: Set>) { - epoxyModels.forEach { - modelCache[it] = itemPosition - } - } - - private fun buildModel(pos: Int) { - if (pos >= asyncDiffer.currentList?.size ?: 0) { - return - } - modelBuilder(pos, asyncDiffer.currentList as List).also { - cacheModelsAtPosition(pos, it.toSet()) - } - } - - private fun triggerLoadAround(position: Int) { - asyncDiffer.currentList?.let { - if (it.size > 0) { - it.loadAround(Math.min(position, it.size - 1)) - } - } - } -} diff --git a/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt b/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt index 3f48043d..a5189494 100644 --- a/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt +++ b/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt @@ -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) } } diff --git a/app/src/main/res/layout/item_empty.xml b/app/src/main/res/layout/item_empty.xml index f7afb775..c8dee60c 100644 --- a/app/src/main/res/layout/item_empty.xml +++ b/app/src/main/res/layout/item_empty.xml @@ -1,4 +1,4 @@ \ No newline at end of file + android:layout_height="0dp" /> \ No newline at end of file diff --git a/app/src/main/res/values/theme_dark.xml b/app/src/main/res/values/theme_dark.xml index 9359d054..5c1a506e 100644 --- a/app/src/main/res/values/theme_dark.xml +++ b/app/src/main/res/values/theme_dark.xml @@ -3,7 +3,7 @@ -