Handle chunks merging with thread summary

Add animation to fragment transition with offset for recyclerview initialization
Support threads on deleted events
This commit is contained in:
ariskotsomitopoulos 2021-11-25 17:59:28 +02:00
parent afc69c77bd
commit c4967a2871
17 changed files with 80 additions and 110 deletions

View file

@ -34,6 +34,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.find import org.matrix.android.sdk.internal.database.query.find
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.extensions.assertIsManaged import org.matrix.android.sdk.internal.extensions.assertIsManaged
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
import timber.log.Timber import timber.log.Timber
@ -157,9 +158,21 @@ private fun ChunkEntity.addTimelineEventFromMerge(realm: Realm, timelineEventEnt
this.senderName = timelineEventEntity.senderName this.senderName = timelineEventEntity.senderName
this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName this.isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName
} }
handleThreadSummary(realm, eventId, copied)
timelineEvents.add(copied) timelineEvents.add(copied)
} }
/**
* Upon copy of the timeline events we should update the latestMessage TimelineEventEntity with the new one
*/
private fun handleThreadSummary(realm: Realm, oldEventId: String, newTimelineEventEntity: TimelineEventEntity) {
EventEntity
.whereRoomId(realm, newTimelineEventEntity.roomId)
.equalTo(EventEntityFields.IS_ROOT_THREAD, true)
.equalTo(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.EVENT_ID, oldEventId)
.findFirst()?.threadSummaryLatestMessage = newTimelineEventEntity
}
private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity { private fun handleReadReceipts(realm: Realm, roomId: String, eventEntity: EventEntity, senderId: String): ReadReceiptsSummaryEntity {
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst() val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventEntity.eventId).findFirst()
?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply { ?: realm.createObject<ReadReceiptsSummaryEntity>(eventEntity.eventId).apply {

View file

@ -49,6 +49,13 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu
.equalTo(EventEntityFields.EVENT_ID, eventId) .equalTo(EventEntityFields.EVENT_ID, eventId)
} }
internal fun EventEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery<EventEntity> {
return realm.where<EventEntity>()
.equalTo(EventEntityFields.ROOM_ID, roomId)
}
internal fun EventEntity.Companion.where(realm: Realm, eventIds: List<String>): RealmQuery<EventEntity> { internal fun EventEntity.Companion.where(realm: Realm, eventIds: List<String>): RealmQuery<EventEntity> {
return realm.where<EventEntity>() return realm.where<EventEntity>()
.`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray()) .`in`(EventEntityFields.EVENT_ID, eventIds.toTypedArray())

View file

@ -270,53 +270,4 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
optimizedThreadSummaryMap.updateThreadSummaryIfNeeded() optimizedThreadSummaryMap.updateThreadSummaryIfNeeded()
} }
// /**
// * Mark or update the thread root event accordingly. If the Threading is disabled
// * no action is done
// */
// private fun updateRootThreadEventIfNeeded(realm: Realm, eventEntity: EventEntity) {
//
// if (!BuildConfig.THREADING_ENABLED) return
//
// val rootThreadEventId = eventEntity.rootThreadEventId
//
// if (eventEntity.isThread && rootThreadEventId != null) {
// markEventAsRootEvent(realm, rootThreadEventId)
// } else {
// markAsRootEventIfNeeded(realm, eventEntity.eventId)
// }
// }
// /**
// * Finds the event with rootThreadEventId and marks it as a root thread
// */
// private fun markEventAsRootEvent(realm: Realm, rootThreadEventId: String) {
// val rootThreadEvent = EventEntity
// .where(realm, rootThreadEventId)
// .equalTo(EventEntityFields.IS_THREAD, false).findFirst() ?: return
// rootThreadEvent.isThread = true
// }
//
// /**
// * Also check if there is at least one thread message for that rootThreadEventId,
// * that means it is a root thread so it should be updated accordingly
// */
// private fun markAsRootEventIfNeeded(realm: Realm, candidateIdRootThread: String) {
// EventEntity
// .whereRootThreadEventId(realm, candidateIdRootThread)
// .findFirst() ?: return
//
// markEventAsRootEvent(realm, candidateIdRootThread)
// }
// /**
// * Returns the chunk for the current room if exists, otherwise it creates a new ChunkEntity
// */
// private fun getOrCreateThreadChunk(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity {
// return ChunkEntity.findThreadChunkOfRoom(realm, roomId, rootThreadEventId)
// ?: realm.createObject<ChunkEntity>().apply {
// this.rootThreadEventId = rootThreadEventId
// }
// }
} }

View file

@ -125,19 +125,19 @@ class MessageItemFactory @Inject constructor(
pillsPostProcessorFactory.create(roomId) pillsPostProcessorFactory.create(roomId)
} }
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event val event = params.event
val highlight = params.isHighlighted val highlight = params.isHighlighted
val callback = params.callback val callback = params.callback
event.root.eventId ?: return null event.root.eventId ?: return null
roomId = event.roomId roomId = event.roomId
val informationData = messageInformationDataFactory.create(params) val informationData = messageInformationDataFactory.create(params)
val threadDetails = if (params.isFromThreadTimeline()) null else event.root.threadDetails
if (event.root.isRedacted()) { if (event.root.isRedacted()) {
// message is redacted // message is redacted
val attributes = messageItemAttributesFactory.create(null, informationData, callback) val attributes = messageItemAttributesFactory.create(null, informationData, callback, threadDetails)
return buildRedactedItem(attributes, highlight) return buildRedactedItem(attributes, highlight)
} }
@ -154,7 +154,6 @@ class MessageItemFactory @Inject constructor(
} }
// always hide summary when we are on thread timeline // always hide summary when we are on thread timeline
val threadDetails = if(params.isFromThreadTimeline()) null else event.root.threadDetails
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, threadDetails) val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback, threadDetails)
// val all = event.root.toContent() // val all = event.root.toContent()
@ -180,9 +179,10 @@ class MessageItemFactory @Inject constructor(
} }
} }
private fun isFromThreadTimeline(params: TimelineItemFactoryParams){ private fun isFromThreadTimeline(params: TimelineItemFactoryParams) {
params.rootThreadEventId params.rootThreadEventId
} }
private fun buildOptionsMessageItem(messageContent: MessageOptionsContent, private fun buildOptionsMessageItem(messageContent: MessageOptionsContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,

View file

@ -72,6 +72,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
attributes.threadCallback?.onThreadSummaryClicked(attributes.informationData.eventId, attributes.threadDetails?.isRootThread ?: false) attributes.threadCallback?.onThreadSummaryClicked(attributes.informationData.eventId, attributes.threadDetails?.isRootThread ?: false)
} }
} }
override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder) super.bind(holder)
if (attributes.informationData.showInformation) { if (attributes.informationData.showInformation) {
@ -111,26 +112,28 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
// Threads // Threads
if(BuildConfig.THREADING_ENABLED) { if (BuildConfig.THREADING_ENABLED) {
holder.threadSummaryConstraintLayout.onClick(_threadClickListener) holder.threadSummaryConstraintLayout.onClick(_threadClickListener)
attributes.threadDetails?.let { threadDetails -> attributes.threadDetails?.let { threadDetails ->
holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread holder.threadSummaryConstraintLayout.isVisible = threadDetails.isRootThread
holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString() holder.threadSummaryCounterTextView.text = threadDetails.numberOfThreads.toString()
holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage holder.threadSummaryInfoTextView.text = threadDetails.threadSummaryLatestTextMessage
threadDetails.threadSummarySenderInfo?.let { senderInfo ->
attributes.avatarRenderer.render(MatrixItem.UserItem(senderInfo.userId, senderInfo.displayName, senderInfo.avatarUrl), holder.threadSummaryAvatarImageView) val userId = threadDetails.threadSummarySenderInfo?.userId ?: return@let
} val displayName = threadDetails.threadSummarySenderInfo?.displayName
} ?: run{holder.threadSummaryConstraintLayout.isVisible = false} val avatarUrl = threadDetails.threadSummarySenderInfo?.avatarUrl
attributes.avatarRenderer.render(MatrixItem.UserItem(userId, displayName, avatarUrl), holder.threadSummaryAvatarImageView)
} ?: run { holder.threadSummaryConstraintLayout.isVisible = false }
} }
} }
override fun unbind(holder: H) { override fun unbind(holder: H) {
attributes.avatarRenderer.clear(holder.avatarImageView) attributes.avatarRenderer.clear(holder.avatarImageView)
holder.avatarImageView.setOnClickListener(null) holder.avatarImageView.setOnClickListener(null)
holder.avatarImageView.setOnLongClickListener(null) holder.avatarImageView.setOnLongClickListener(null)
holder.memberNameView.setOnClickListener(null) holder.memberNameView.setOnClickListener(null)
holder.memberNameView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null)
attributes.avatarRenderer.clear(holder.threadSummaryAvatarImageView)
holder.threadSummaryConstraintLayout.setOnClickListener(null) holder.threadSummaryConstraintLayout.setOnClickListener(null)
super.unbind(holder) super.unbind(holder)
} }

View file

@ -16,7 +16,6 @@
package im.vector.app.features.home.room.threads.list.model package im.vector.app.features.home.room.threads.list.model
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
@ -31,8 +30,8 @@ import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
@EpoxyModelClass(layout = R.layout.item_thread_summary) @EpoxyModelClass(layout = R.layout.item_thread_list)
abstract class ThreadSummaryModel : VectorEpoxyModel<ThreadSummaryModel.Holder>() { abstract class ThreadListModel : VectorEpoxyModel<ThreadListModel.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute lateinit var matrixItem: MatrixItem

View file

@ -20,21 +20,21 @@ import com.airbnb.epoxy.EpoxyController
import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.threads.list.model.threadSummary import im.vector.app.features.home.room.threads.list.model.threadList
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
class ThreadSummaryController @Inject constructor( class ThreadListController @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val dateFormatter: VectorDateFormatter private val dateFormatter: VectorDateFormatter
) : EpoxyController() { ) : EpoxyController() {
var listener: Listener? = null var listener: Listener? = null
private var viewState: ThreadSummaryViewState? = null private var viewState: ThreadListViewState? = null
fun update(viewState: ThreadSummaryViewState) { fun update(viewState: ThreadListViewState) {
this.viewState = viewState this.viewState = viewState
requestModelBuild() requestModelBuild()
} }
@ -46,7 +46,7 @@ class ThreadSummaryController @Inject constructor(
safeViewState.rootThreadEventList.invoke() safeViewState.rootThreadEventList.invoke()
?.forEach { timelineEvent -> ?.forEach { timelineEvent ->
val date = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.ROOM_LIST) val date = dateFormatter.format(timelineEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
threadSummary { threadList {
id(timelineEvent.eventId) id(timelineEvent.eventId)
avatarRenderer(host.avatarRenderer) avatarRenderer(host.avatarRenderer)
matrixItem(timelineEvent.senderInfo.toMatrixItem()) matrixItem(timelineEvent.senderInfo.toMatrixItem())

View file

@ -31,23 +31,23 @@ import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.flow
class ThreadSummaryViewModel @AssistedInject constructor(@Assisted val initialState: ThreadSummaryViewState, class ThreadListViewModel @AssistedInject constructor(@Assisted val initialState: ThreadListViewState,
private val session: Session) : private val session: Session) :
VectorViewModel<ThreadSummaryViewState, EmptyAction, EmptyViewEvents>(initialState) { VectorViewModel<ThreadListViewState, EmptyAction, EmptyViewEvents>(initialState) {
private val room = session.getRoom(initialState.roomId) private val room = session.getRoom(initialState.roomId)
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(initialState: ThreadSummaryViewState): ThreadSummaryViewModel fun create(initialState: ThreadListViewState): ThreadListViewModel
} }
companion object : MavericksViewModelFactory<ThreadSummaryViewModel, ThreadSummaryViewState> { companion object : MavericksViewModelFactory<ThreadListViewModel, ThreadListViewState> {
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: ThreadSummaryViewState): ThreadSummaryViewModel? { override fun create(viewModelContext: ViewModelContext, state: ThreadListViewState): ThreadListViewModel? {
val fragment: ThreadListFragment = (viewModelContext as FragmentViewModelContext).fragment() val fragment: ThreadListFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.threadSummaryViewModelFactory.create(state) return fragment.threadListViewModelFactory.create(state)
} }
} }

View file

@ -22,7 +22,7 @@ import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
data class ThreadSummaryViewState( data class ThreadListViewState(
val rootThreadEventList: Async<List<TimelineEvent>> = Uninitialized, val rootThreadEventList: Async<List<TimelineEvent>> = Uninitialized,
val shouldFilterThreads: Boolean = false, val shouldFilterThreads: Boolean = false,
val roomId: String val roomId: String

View file

@ -20,14 +20,13 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources.getDrawable
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.parentFragmentViewModel
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.databinding.BottomSheetThreadListBinding import im.vector.app.databinding.BottomSheetThreadListBinding
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryViewModel import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryViewState import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewState
class ThreadListBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetThreadListBinding>() { class ThreadListBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetThreadListBinding>() {
@ -35,13 +34,12 @@ class ThreadListBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetThr
return BottomSheetThreadListBinding.inflate(inflater, container, false) return BottomSheetThreadListBinding.inflate(inflater, container, false)
} }
private val threadListViewModel: ThreadListViewModel by parentFragmentViewModel()
private val threadListViewModel: ThreadSummaryViewModel by parentFragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
threadListViewModel.subscribe(this){ threadListViewModel.subscribe(this) {
renderState(it) renderState(it)
} }
views.threadListModalAllThreads.views.bottomSheetActionClickableZone.debouncedClicks { views.threadListModalAllThreads.views.bottomSheetActionClickableZone.debouncedClicks {
@ -52,18 +50,11 @@ class ThreadListBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetThr
threadListViewModel.applyFiltering(true) threadListViewModel.applyFiltering(true)
dismiss() dismiss()
} }
} }
private fun renderState(state: ThreadSummaryViewState) { private fun renderState(state: ThreadListViewState) {
val tickDrawable = ContextCompat.getDrawable(requireContext(), R.drawable.ic_tick)
if(state.shouldFilterThreads){ views.threadListModalAllThreads.rightIcon = if (state.shouldFilterThreads) null else tickDrawable
views.threadListModalAllThreads.rightIcon = null views.threadListModalMyThreads.rightIcon = if (state.shouldFilterThreads) tickDrawable else null
views.threadListModalMyThreads.rightIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_tick)
}else{
views.threadListModalAllThreads.rightIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_tick)
views.threadListModalMyThreads.rightIcon = null
}
} }
} }

View file

@ -34,20 +34,20 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.animation.TimelineItemAnimator import im.vector.app.features.home.room.detail.timeline.animation.TimelineItemAnimator
import im.vector.app.features.home.room.threads.ThreadsActivity import im.vector.app.features.home.room.threads.ThreadsActivity
import im.vector.app.features.home.room.threads.arguments.ThreadListArgs import im.vector.app.features.home.room.threads.arguments.ThreadListArgs
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryController import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListController
import im.vector.app.features.home.room.threads.list.viewmodel.ThreadSummaryViewModel import im.vector.app.features.home.room.threads.list.viewmodel.ThreadListViewModel
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import javax.inject.Inject import javax.inject.Inject
class ThreadListFragment @Inject constructor( class ThreadListFragment @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val threadSummaryController: ThreadSummaryController, private val threadListController: ThreadListController,
val threadSummaryViewModelFactory: ThreadSummaryViewModel.Factory val threadListViewModelFactory: ThreadListViewModel.Factory
) : VectorBaseFragment<FragmentThreadListBinding>(), ) : VectorBaseFragment<FragmentThreadListBinding>(),
ThreadSummaryController.Listener { ThreadListController.Listener {
private val threadSummaryViewModel: ThreadSummaryViewModel by fragmentViewModel() private val threadListViewModel: ThreadListViewModel by fragmentViewModel()
private val threadListArgs: ThreadListArgs by args() private val threadListArgs: ThreadListArgs by args()
@ -74,13 +74,13 @@ class ThreadListFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initToolbar() initToolbar()
views.threadListRecyclerView.configureWith(threadSummaryController, TimelineItemAnimator(), hasFixedSize = false) views.threadListRecyclerView.configureWith(threadListController, TimelineItemAnimator(), hasFixedSize = false)
threadSummaryController.listener = this threadListController.listener = this
} }
override fun onDestroyView() { override fun onDestroyView() {
views.threadListRecyclerView.cleanup() views.threadListRecyclerView.cleanup()
threadSummaryController.listener = null threadListController.listener = null
super.onDestroyView() super.onDestroyView()
} }
@ -89,8 +89,8 @@ class ThreadListFragment @Inject constructor(
renderToolbar() renderToolbar()
} }
override fun invalidate() = withState(threadSummaryViewModel) { state -> override fun invalidate() = withState(threadListViewModel) { state ->
threadSummaryController.update(state) threadListController.update(state)
} }
private fun renderToolbar() { private fun renderToolbar() {

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"> <set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="-100%p" android:toXDelta="0" <translate
android:fromXDelta="-100%p" android:toXDelta="0"
android:duration="@android:integer/config_mediumAnimTime"/> android:duration="@android:integer/config_mediumAnimTime"/>
</set> </set>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"> <set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="100%p" android:toXDelta="0" <translate
android:startOffset="250"
android:fromXDelta="100%p" android:toXDelta="0"
android:duration="@android:integer/config_mediumAnimTime"/> android:duration="@android:integer/config_mediumAnimTime"/>
</set> </set>

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"> <set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="-100%p" <translate
android:startOffset="250"
android:fromXDelta="0" android:toXDelta="-100%p"
android:duration="@android:integer/config_mediumAnimTime"/> android:duration="@android:integer/config_mediumAnimTime"/>
</set> </set>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"> <set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromXDelta="0" android:toXDelta="100%p" <translate
android:fromXDelta="0" android:toXDelta="100%p"
android:duration="@android:integer/config_mediumAnimTime"/> android:duration="@android:integer/config_mediumAnimTime"/>
</set> </set>

View file

@ -34,7 +34,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
android:background="?android:colorBackground" android:background="?android:colorBackground"
tools:listitem="@layout/item_thread_summary" /> tools:listitem="@layout/item_thread_list" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>