Timeline: try to fix some issues with permalink [WIP]

This commit is contained in:
ganfra 2019-09-14 14:11:41 +02:00
parent f4ab770be9
commit 5d6d0202a9
9 changed files with 178 additions and 93 deletions

View File

@ -42,10 +42,11 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.math.max
import kotlin.math.min
private const val MIN_FETCHING_COUNT = 30
private const val DISPLAY_INDEX_UNKNOWN = Int.MIN_VALUE
internal class DefaultTimeline(
private val roomId: String,
@ -85,8 +86,8 @@ internal class DefaultTimeline(
private var roomEntity: RoomEntity? = null
private var prevDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
private var nextDisplayIndex: Int = DISPLAY_INDEX_UNKNOWN
private var prevDisplayIndex: Int? = null
private var nextDisplayIndex: Int? = null
private val builtEvents = Collections.synchronizedList<TimelineEvent>(ArrayList())
private val builtEventsIdMap = Collections.synchronizedMap(HashMap<String, Int>())
private val backwardsPaginationState = AtomicReference(PaginationState())
@ -222,6 +223,7 @@ internal class DefaultTimeline(
if (isStarted.compareAndSet(true, false)) {
eventDecryptor.destroy()
Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId")
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
BACKGROUND_HANDLER.post {
cancelableBag.cancel()
roomEntity?.sendingTimelineEvents?.removeAllChangeListeners()
@ -303,11 +305,8 @@ internal class DefaultTimeline(
private fun hasMoreInCache(direction: Timeline.Direction): Boolean {
return Realm.getInstance(realmConfiguration).use { localRealm ->
val timelineEventEntity = buildEventQuery(localRealm).findFirst(direction)
?: return false
?: return false
if (direction == Timeline.Direction.FORWARDS) {
if (findCurrentChunk(localRealm)?.isLastForward == true) {
return false
}
val firstEvent = builtEvents.firstOrNull() ?: return true
firstEvent.displayIndex < timelineEventEntity.root!!.displayIndex
} else {
@ -334,16 +333,17 @@ internal class DefaultTimeline(
* This has to be called on TimelineThread as it access realm live results
* @return true if createSnapshot should be posted
*/
private fun paginateInternal(startDisplayIndex: Int,
private fun paginateInternal(startDisplayIndex: Int?,
direction: Timeline.Direction,
count: Int): Boolean {
count: Int,
strict: Boolean = false): Boolean {
updatePaginationState(direction) { it.copy(requestedCount = count, isPaginating = true) }
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong())
val builtCount = buildTimelineEvents(startDisplayIndex, direction, count.toLong(), strict)
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)
val fetchingCount = max(MIN_FETCHING_COUNT, newRequestedCount)
executePaginationTask(direction, fetchingCount)
} else {
updatePaginationState(direction) { it.copy(isPaginating = false, requestedCount = 0) }
@ -404,20 +404,19 @@ internal class DefaultTimeline(
.findFirst()
shouldFetchInitialEvent = initialEvent == null
initialEvent?.root?.displayIndex
} ?: DISPLAY_INDEX_UNKNOWN
}
prevDisplayIndex = initialDisplayIndex
nextDisplayIndex = initialDisplayIndex
val currentInitialEventId = initialEventId
if (currentInitialEventId != null && shouldFetchInitialEvent) {
fetchEvent(currentInitialEventId)
} else {
val count = Math.min(settings.initialSize, liveEvents.size)
val count = min(settings.initialSize, liveEvents.size)
if (isLive) {
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count)
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count, strict = false)
} else {
paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2)
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2)
paginateInternal(initialDisplayIndex, Timeline.Direction.FORWARDS, count / 2, strict = false)
paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count / 2, strict = true)
}
}
postSnapshot()
@ -429,9 +428,9 @@ internal class DefaultTimeline(
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)
from = token,
direction = direction.toPaginationDirection(),
limit = limit)
Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask
@ -479,14 +478,15 @@ internal class DefaultTimeline(
* 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,
private fun buildTimelineEvents(startDisplayIndex: Int?,
direction: Timeline.Direction,
count: Long): Int {
if (count < 1) {
count: Long,
strict: Boolean = false): Int {
if (count < 1 || startDisplayIndex == null) {
return 0
}
val start = System.currentTimeMillis()
val offsetResults = getOffsetResults(startDisplayIndex, direction, count)
val offsetResults = getOffsetResults(startDisplayIndex, direction, count, strict)
if (offsetResults.isEmpty()) {
return 0
}
@ -501,7 +501,7 @@ internal class DefaultTimeline(
val timelineEvent = buildTimelineEvent(eventEntity)
if (timelineEvent.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) {
&& timelineEvent.root.mxDecryptionResult == null) {
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
}
@ -527,16 +527,23 @@ internal class DefaultTimeline(
*/
private fun getOffsetResults(startDisplayIndex: Int,
direction: Timeline.Direction,
count: Long): RealmResults<TimelineEventEntity> {
count: Long,
strict: Boolean): RealmResults<TimelineEventEntity> {
val offsetQuery = liveEvents.where()
if (direction == Timeline.Direction.BACKWARDS) {
offsetQuery
.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
if (strict) {
offsetQuery.lessThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
} else {
offsetQuery.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
}
} else {
offsetQuery
.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING)
.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
offsetQuery.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING)
if (strict) {
offsetQuery.greaterThan(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
} else {
offsetQuery.greaterThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, startDisplayIndex)
}
}
return offsetQuery
.limit(count)
@ -589,8 +596,8 @@ internal class DefaultTimeline(
}
private fun clearAllValues() {
prevDisplayIndex = DISPLAY_INDEX_UNKNOWN
nextDisplayIndex = DISPLAY_INDEX_UNKNOWN
prevDisplayIndex = null
nextDisplayIndex = null
builtEvents.clear()
builtEventsIdMap.clear()
backwardsPaginationState.set(PaginationState())

View File

@ -20,10 +20,10 @@ import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
fun createBackgroundHandler(name: String): Handler = Handler(
internal fun createBackgroundHandler(name: String): Handler = Handler(
HandlerThread(name).apply { start() }.looper
)
fun createUIHandler(): Handler = Handler(
internal fun createUIHandler(): Handler = Handler(
Looper.getMainLooper()
)

View File

@ -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.riotx.core.utils
import android.os.Handler
internal class Debouncer(private val handler: Handler) {
private val runnables = HashMap<String, Runnable>()
fun debounce(identifier: String, millis: Long, r: Runnable): Boolean {
if (runnables.containsKey(identifier)) {
// debounce
val old = runnables[identifier]
handler.removeCallbacks(old)
}
insertRunnable(identifier, r, millis)
return true
}
private fun insertRunnable(identifier: String, r: Runnable, millis: Long) {
val chained = Runnable {
handler.post(r)
runnables.remove(identifier)
}
runnables[identifier] = chained
handler.postDelayed(chained, millis)
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.riotx.core.utils
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
internal fun createBackgroundHandler(name: String): Handler = Handler(
HandlerThread(name).apply { start() }.looper
)
internal fun createUIHandler(): Handler = Handler(
Looper.getMainLooper()
)

View File

@ -19,22 +19,17 @@ package im.vector.riotx.features.home.createdirect
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.paging.PagedListEpoxyController
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.util.createUIHandler
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.EmptyItem_
import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.epoxy.noResultItem
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.createUIHandler
import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject

View File

@ -202,6 +202,7 @@ class RoomDetailFragment :
}
}
private val roomDetailArgs: RoomDetailArgs by args()
private val glideRequests by lazy {
GlideApp.with(this)
@ -221,11 +222,13 @@ class RoomDetailFragment :
@Inject lateinit var roomDetailViewModelFactory: RoomDetailViewModel.Factory
@Inject lateinit var textComposerViewModelFactory: TextComposerViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
@Inject lateinit var eventHtmlRenderer: EventHtmlRenderer
@Inject lateinit var vectorPreferences: VectorPreferences
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
private lateinit var scrollOnHighlightedEventCallback: ScrollOnHighlightedEventCallback
private lateinit var endlessScrollListener: EndlessRecyclerViewScrollListener
override fun getLayoutResId() = R.layout.fragment_room_detail
@ -374,17 +377,17 @@ class RoomDetailFragment :
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody
?: messageContent.body)
?: messageContent.body)
formattedBody = eventHtmlRenderer.render(document)
}
composerLayout.composerRelatedMessageContent.text = formattedBody
?: nonFormattedBody
?: nonFormattedBody
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
avatarRenderer.render(event.senderAvatar, event.root.senderId
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
composerLayout.expand {
@ -413,9 +416,9 @@ class RoomDetailFragment :
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
REACTION_SELECT_REQUEST_CODE -> {
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
?: return
?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
?: return
?: return
//TODO check if already reacted with that?
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
}
@ -430,7 +433,10 @@ class RoomDetailFragment :
epoxyVisibilityTracker.attach(recyclerView)
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
endlessScrollListener = EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction ->
roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction))
}
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
scrollOnHighlightedEventCallback = ScrollOnHighlightedEventCallback(layoutManager, timelineEventController)
recyclerView.layoutManager = layoutManager
recyclerView.itemAnimator = null
@ -441,35 +447,32 @@ class RoomDetailFragment :
it.dispatchTo(scrollOnHighlightedEventCallback)
}
recyclerView.addOnScrollListener(
EndlessRecyclerViewScrollListener(layoutManager, RoomDetailViewModel.PAGINATION_COUNT) { direction ->
roomDetailViewModel.process(RoomDetailActions.LoadMoreTimelineEvents(direction))
})
recyclerView.addOnScrollListener(endlessScrollListener)
recyclerView.setController(timelineEventController)
timelineEventController.callback = this
if (vectorPreferences.swipeToReplyIsEnabled()) {
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
R.drawable.ic_reply,
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
(model as? AbsMessageItem)?.attributes?.informationData?.let {
val eventId = it.eventId
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
}
}
R.drawable.ic_reply,
object : RoomMessageTouchHelperCallback.QuickReplayHandler {
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
(model as? AbsMessageItem)?.attributes?.informationData?.let {
val eventId = it.eventId
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
}
}
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
return when (model) {
is MessageFileItem,
is MessageImageVideoItem,
is MessageTextItem -> {
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
}
else -> false
}
}
})
override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
return when (model) {
is MessageFileItem,
is MessageImageVideoItem,
is MessageTextItem -> {
return (model as AbsMessageItem).attributes.informationData.sendState == SendState.SYNCED
}
else -> false
}
}
})
val touchHelper = ItemTouchHelper(swipeCallback)
touchHelper.attachToRecyclerView(recyclerView)
}

View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail
import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.riotx.core.platform.DefaultListUpdateCallback
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import timber.log.Timber
import java.util.concurrent.atomic.AtomicReference
class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutManager,
@ -28,17 +29,16 @@ class ScrollOnHighlightedEventCallback(private val layoutManager: LinearLayoutMa
override fun onChanged(position: Int, count: Int, tag: Any?) {
val eventId = scheduledEventId.get() ?: return
val positionToScroll = timelineEventController.searchPositionOfEvent(eventId)
if (positionToScroll != null) {
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition()
// Do not scroll it item is already visible
if (positionToScroll !in firstVisibleItem..lastVisibleItem) {
Timber.v("Scroll to $positionToScroll")
// Note: Offset will be from the bottom, since the layoutManager is reversed
layoutManager.scrollToPosition(position)
layoutManager.scrollToPosition(positionToScroll)
}
scheduledEventId.set(null)
}

View File

@ -18,11 +18,15 @@ package im.vector.riotx.features.home.room.detail
import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.riotx.core.platform.DefaultListUpdateCallback
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import timber.log.Timber
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager) : DefaultListUpdateCallback {
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
private val timelineEventController: TimelineEventController) : DefaultListUpdateCallback {
override fun onInserted(position: Int, count: Int) {
if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0) {
Timber.v("On inserted $count count at position: $position")
if (position == 0 && layoutManager.findFirstVisibleItemPosition() == 0 && !timelineEventController.isLoadingForward()) {
layoutManager.scrollToPosition(0)
}
}

View File

@ -35,12 +35,7 @@ import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory
import im.vector.riotx.features.home.room.detail.timeline.helper.*
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import org.threeten.bp.LocalDateTime
@ -91,8 +86,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun onUrlLongClicked(url: String): Boolean
}
private var showingForwardLoader = false
private val modelCache = arrayListOf<CacheItemData?>()
private var currentSnapshot: List<TimelineEvent> = emptyList()
private var inSubmitList: Boolean = false
private var timeline: Timeline? = null
@ -163,7 +158,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
synchronized(modelCache) {
for (i in 0 until modelCache.size) {
if (modelCache[i]?.eventId == eventIdToHighlight
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
|| modelCache[i]?.eventId == this.eventIdToHighlight) {
modelCache[i] = null
}
}
@ -182,17 +177,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
override fun buildModels() {
val loaderAdded = LoadingItem_()
.id("forward_loading_item")
val timestamp = System.currentTimeMillis()
showingForwardLoader = LoadingItem_()
.id("forward_loading_item_$timestamp")
.addWhen(Timeline.Direction.FORWARDS)
val timelineModels = getModels()
add(timelineModels)
// Avoid displaying two loaders if there is no elements between them
if (!loaderAdded || timelineModels.isNotEmpty()) {
if (!showingForwardLoader || timelineModels.isNotEmpty()) {
LoadingItem_()
.id("backward_loading_item")
.id("backward_loading_item_$timestamp")
.addWhen(Timeline.Direction.BACKWARDS)
}
}
@ -224,8 +220,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
// We then are sure we always have items up to date.
if (modelCache[position] == null
|| modelCache[position]?.mergedHeaderModel != null
|| modelCache[position]?.formattedDayModel != null) {
|| modelCache[position]?.mergedHeaderModel != null
|| modelCache[position]?.formattedDayModel != null) {
modelCache[position] = buildItemModels(position, currentSnapshot)
}
}
@ -255,7 +251,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
}
val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent, items, addDaySeparator, currentPosition, callback){
val mergedHeaderModel = mergedHeaderItemFactory.create(event, nextEvent, items, addDaySeparator, currentPosition, callback) {
requestModelBuild()
}
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
@ -284,6 +280,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun searchPositionOfEvent(eventId: String): Int? = synchronized(modelCache) {
// Search in the cache
var realPosition = 0
if (showingForwardLoader) {
realPosition++
}
for (i in 0 until modelCache.size) {
val itemCache = modelCache[i]
if (itemCache?.eventId == eventId) {
@ -319,6 +318,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return modelCache.getOrNull(position - offsetValue)?.eventId
}
fun isLoadingForward() = showingForwardLoader
private data class CacheItemData(
val localId: Long,
val eventId: String?,