diff --git a/app/src/main/java/one/mixin/android/repository/ConversationRepository.kt b/app/src/main/java/one/mixin/android/repository/ConversationRepository.kt index 8e50e28c27..ffdde75bf6 100644 --- a/app/src/main/java/one/mixin/android/repository/ConversationRepository.kt +++ b/app/src/main/java/one/mixin/android/repository/ConversationRepository.kt @@ -61,10 +61,12 @@ import one.mixin.android.vo.ConversationMinimal import one.mixin.android.vo.ConversationStatus import one.mixin.android.vo.ConversationStorageUsage import one.mixin.android.vo.GroupInfo -import one.mixin.android.vo.isAppCard import one.mixin.android.vo.Job import one.mixin.android.vo.Message import one.mixin.android.vo.MessageItem +import one.mixin.android.vo.isAppCard +import one.mixin.android.vo.isImage +import one.mixin.android.vo.isVideo import one.mixin.android.vo.MessageMention import one.mixin.android.vo.MessageMinimal import one.mixin.android.vo.Participant @@ -196,37 +198,25 @@ class ConversationRepository messageId: String, excludeLive: Boolean, ): Int { - val list = if (excludeLive) { - messageDao.getMediaMessagesExcludeLiveList(conversationId) + val item = messageDao.getMediaMessage(conversationId, messageId) ?: return -1 + if (excludeLive && !item.isImage() && !item.isVideo()) return -1 + if (item.isAppCard() && !item.isAppCardWithCover()) return -1 + return if (excludeLive) { + messageDao.indexMediaMessagesExcludeLive(conversationId, messageId) } else { - messageDao.getMediaMessagesList(conversationId) + messageDao.indexMediaMessages(conversationId, messageId) } - val filteredList = list.filter { !it.isAppCard() || it.isAppCardWithCover() } - return filteredList.indexOfFirst { it.messageId == messageId }.coerceAtLeast(0) } suspend fun countIndexMediaMessages( conversationId: String, excludeLive: Boolean, - ): Int { - val list = if (excludeLive) { - messageDao.getMediaMessagesExcludeLiveList(conversationId) - } else { - messageDao.getMediaMessagesList(conversationId) - } - return list.count { !it.isAppCard() || it.isAppCardWithCover() } - } - - fun getMediaMessagesDataSource( - conversationId: String, - excludeLive: Boolean, - ): DataSource.Factory { - return if (excludeLive) { - messageDao.getMediaMessagesExcludeLive(conversationId) + ): Int = + if (excludeLive) { + messageDao.countIndexMediaMessagesExcludeLive(conversationId) } else { - messageDao.getMediaMessages(conversationId) + messageDao.countIndexMediaMessages(conversationId) } - } fun getMediaMessages( conversationId: String, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsAddBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsAddBottomSheetDialogFragment.kt index 3145719aec..3c4b30bb4e 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsAddBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsAddBottomSheetDialogFragment.kt @@ -335,6 +335,7 @@ private fun PerpsAddContent( val defaultLiquidationPrice = position.liquidationPrice ?.takeIf { showLiquidationPrice && it.isNotBlank() } val displayLiquidationPrice = remoteLiquidationPrice ?: defaultLiquidationPrice + val visibleLiquidationPrice = displayLiquidationPrice?.takeIf { it != "-" } val entryPriceText = position.entryPrice .takeIf { it.isNotBlank() } ?.let { formatPerpsPrice(it, priceScale) } @@ -582,12 +583,8 @@ private fun PerpsAddContent( Spacer(modifier = Modifier.height(16.dp)) PerpsAddInfoRow( title = stringResource(R.string.Liquidation_Price), - value = when (displayLiquidationPrice) { - "-" -> "-" - null -> "-" - else -> formatPerpsPrice(displayLiquidationPrice, priceScale) - }, - isLoading = isLiquidationLoading && displayLiquidationPrice != "-", + value = visibleLiquidationPrice?.let { formatPerpsPrice(it, priceScale) } ?: "-", + isLoading = isLiquidationLoading && visibleLiquidationPrice != null, onTipClick = { showPerpsGuide(PerpetualGuideBottomSheetDialogFragment.TAB_LIQUIDATION) }, diff --git a/app/src/main/java/one/mixin/android/ui/media/SharedMediaViewModel.kt b/app/src/main/java/one/mixin/android/ui/media/SharedMediaViewModel.kt index f977d37dc7..a4c2b0bf7d 100644 --- a/app/src/main/java/one/mixin/android/ui/media/SharedMediaViewModel.kt +++ b/app/src/main/java/one/mixin/android/ui/media/SharedMediaViewModel.kt @@ -1,22 +1,14 @@ package one.mixin.android.ui.media import android.net.Uri -import android.annotation.SuppressLint -import androidx.annotation.OptIn import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.media3.common.util.UnstableApi -import androidx.paging.DataSource import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList -import androidx.paging.PositionalDataSource -import androidx.arch.core.executor.ArchTaskExecutor import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import one.mixin.android.Constants.PAGE_SIZE import one.mixin.android.job.AttachmentDownloadJob import one.mixin.android.job.ConvertVideoJob @@ -32,10 +24,8 @@ import one.mixin.android.vo.MessageCategory import one.mixin.android.vo.MessageItem import one.mixin.android.vo.isEncrypted import one.mixin.android.vo.isImage -import one.mixin.android.vo.isAppCard import one.mixin.android.vo.isSignal import one.mixin.android.vo.isVideo -import org.threeten.bp.ZonedDateTime import javax.inject.Inject @HiltViewModel @@ -46,61 +36,27 @@ class SharedMediaViewModel private val jobManager: MixinJobManager, ) : ViewModel() { - fun getMediaMessagesExcludeLive(conversationId: String): LiveData> { - val liveData = MutableLiveData>() - viewModelScope.launch(Dispatchers.IO) { - val list = conversationRepository.getMediaMessagesExcludeLiveList(conversationId) - val filteredList = list.filter { !it.isAppCard() || it.isAppCardWithCover() } - val pagedList = createPagedList(filteredList, 0) - liveData.postValue(pagedList) - } - return liveData - } - - @SuppressLint("RestrictedApi") - private fun createPagedList(list: List, initialKey: Int): PagedList { - val config = PagedList.Config.Builder() - .setPageSize(MediaFragment.PAGE_SIZE) - .setEnablePlaceholders(false) - .build() - return PagedList.Builder(ListDataSource(list), config) - .setNotifyExecutor(ArchTaskExecutor.getMainThreadExecutor()) - .setFetchExecutor(ArchTaskExecutor.getIOThreadExecutor()) - .setInitialKey(initialKey) + fun getMediaMessagesExcludeLive(conversationId: String): LiveData> = + LivePagedListBuilder( + conversationRepository.getMediaMessagesExcludeLive(conversationId), + PagedList.Config.Builder() + .setPrefetchDistance(MediaFragment.PAGE_SIZE * 2) + .setPageSize(MediaFragment.PAGE_SIZE) + .setEnablePlaceholders(true) + .build(), + ) .build() - } - - fun getAudioMessages(conversationId: String): LiveData> { - val liveData = MutableLiveData>() - viewModelScope.launch(Dispatchers.IO) { - val list = conversationRepository.getAudioMessagesList(conversationId) - val sortedList = list.sortedWith( - Comparator { o1, o2 -> - if (o1 == null || o2 == null) return@Comparator 0 - val time1 = ZonedDateTime.parse(o1.createdAt) - val time2 = ZonedDateTime.parse(o2.createdAt) - val year1 = time1.year - val year2 = time2.year - val day1 = time1.dayOfYear - val day2 = time2.dayOfYear - if (year1 == year2) { - if (day1 == day2) { - return@Comparator time1.toOffsetDateTime() - .compareTo(time2.toOffsetDateTime()) - } else { - return@Comparator day2 - day1 - } - } else { - return@Comparator year2 - year1 - } - }, - ) - val pagedList = createPagedList(sortedList, 0) - liveData.postValue(pagedList) - } - return liveData - } + fun getAudioMessages(conversationId: String): LiveData> = + LivePagedListBuilder( + conversationRepository.getAudioMessages(conversationId), + PagedList.Config.Builder() + .setPrefetchDistance(PAGE_SIZE * 2) + .setPageSize(PAGE_SIZE) + .setEnablePlaceholders(true) + .build(), + ) + .build() fun getPostMessages(conversationId: String): LiveData> { return LivePagedListBuilder( @@ -226,25 +182,11 @@ class SharedMediaViewModel excludeLive: Boolean, ) = conversationRepository.countIndexMediaMessages(conversationId, excludeLive) - @OptIn(UnstableApi::class) fun getMediaMessages( conversationId: String, index: Int, excludeLive: Boolean, - ): LiveData> { - val liveData = MutableLiveData>() - viewModelScope.launch(Dispatchers.IO) { - val list = if (excludeLive) { - conversationRepository.getMediaMessagesExcludeLiveList(conversationId) - } else { - conversationRepository.getMediaMessagesList(conversationId) - } - val filteredList = list.filter { !it.isAppCard() || it.isAppCardWithCover() } - val pagedList = createPagedList(filteredList, index) - liveData.postValue(pagedList) - } - return liveData - } + ): LiveData> = conversationRepository.getMediaMessages(conversationId, index, excludeLive) suspend fun getMediaMessage( conversationId: String, @@ -259,27 +201,3 @@ class SharedMediaViewModel } } } - -class ListDataSource(private val items: List) : PositionalDataSource() { - override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback) { - val totalCount = items.size - if (totalCount == 0) { - callback.onResult(emptyList(), 0, 0) - return - } - val position = computeInitialLoadPosition(params, totalCount) - val loadSize = computeInitialLoadSize(params, position, totalCount) - callback.onResult(items.subList(position, minOf(position + loadSize, totalCount)), position, totalCount) - } - - override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback) { - val start = params.startPosition - val end = minOf(start + params.loadSize, items.size) - if (start < items.size && start < end) { - callback.onResult(items.subList(start, end)) - } else { - callback.onResult(emptyList()) - } - } -} - diff --git a/app/src/main/java/one/mixin/android/ui/media/pager/MediaPagerActivity.kt b/app/src/main/java/one/mixin/android/ui/media/pager/MediaPagerActivity.kt index f84da8faf4..1fbbc99486 100644 --- a/app/src/main/java/one/mixin/android/ui/media/pager/MediaPagerActivity.kt +++ b/app/src/main/java/one/mixin/android/ui/media/pager/MediaPagerActivity.kt @@ -315,7 +315,12 @@ class MediaPagerActivity : BaseActivity(), DismissFrameLayout.OnDismissListener, private fun observeAllDataSource() = lifecycleScope.launch { val excludeLive = mediaSource == MediaSource.SharedMedia - initialIndex = viewModel.indexMediaMessages(conversationId, messageId, excludeLive) + val index = viewModel.indexMediaMessages(conversationId, messageId, excludeLive) + if (index < 0) { + reportEvent("Media message not found,conversationId: $conversationId,messageId: $messageId") + return@launch + } + initialIndex = index viewModel.getMediaMessages(conversationId, initialIndex, excludeLive) .observe( this@MediaPagerActivity, @@ -326,9 +331,7 @@ class MediaPagerActivity : BaseActivity(), DismissFrameLayout.OnDismissListener, runCatching { adapter.initialPos = initialIndex it.loadAround(initialIndex) - if (excludeLive) { - binding.viewPager.setCurrentItem(initialIndex, false) - } else if (it.getOrNull(initialIndex)?.messageId == messageId) { // Only change when data is same + if (it.getOrNull(initialIndex)?.messageId == messageId) { binding.viewPager.setCurrentItem(initialIndex, false) } else { lifecycleScope.launch { diff --git a/app/src/main/java/one/mixin/android/vo/AppCardData.kt b/app/src/main/java/one/mixin/android/vo/AppCardData.kt index 03814b1483..b72577b0d1 100644 --- a/app/src/main/java/one/mixin/android/vo/AppCardData.kt +++ b/app/src/main/java/one/mixin/android/vo/AppCardData.kt @@ -56,11 +56,11 @@ data class AppCardData( } } - val hashCover: Boolean + @IgnoredOnParcel + val hasCover: Boolean get() { if (oldVersion) return false - if (coverUrl.isNullOrBlank()) return false - return true + return !coverUrl.isNullOrBlank() || !cover?.url.isNullOrBlank() } val hasValidCoverSize: Boolean diff --git a/app/src/main/java/one/mixin/android/vo/MessageItem.kt b/app/src/main/java/one/mixin/android/vo/MessageItem.kt index 6a517a420b..684d5e90fa 100644 --- a/app/src/main/java/one/mixin/android/vo/MessageItem.kt +++ b/app/src/main/java/one/mixin/android/vo/MessageItem.kt @@ -220,7 +220,7 @@ data class MessageItem( fun isAppCardWithCover(): Boolean { if (!isAppCard()) return false - return appCardData?.hashCover == true + return appCardData?.hasCover == true } private fun unfinishedAttachment(): Boolean = !mediaDownloaded(this.mediaStatus) && (isData() || isImage() || isVideo() || isAudio()) diff --git a/app/src/test/java/one/mixin/android/vo/AppCardDataTest.kt b/app/src/test/java/one/mixin/android/vo/AppCardDataTest.kt new file mode 100644 index 0000000000..ef1501dcca --- /dev/null +++ b/app/src/test/java/one/mixin/android/vo/AppCardDataTest.kt @@ -0,0 +1,36 @@ +package one.mixin.android.vo + +import one.mixin.android.util.GsonHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class AppCardDataTest { + @Test + fun nestedCoverUrlCountsAsCover() { + val coverUrl = "https://example.com/cover.png" + val appCardData = AppCardData( + appId = "app-id", + iconUrl = null, + coverUrl = null, + cover = Cover( + height = 100, + width = 100, + mimeType = "image/png", + url = coverUrl, + thumbnail = null, + ), + title = "title", + description = "description", + action = null, + updatedAt = null, + shareable = null, + ) + val message = create(MessageCategory.APP_CARD.name).copy( + content = GsonHelper.customGson.toJson(appCardData), + ) + + assertTrue(appCardData.hasCover) + assertEquals(coverUrl, message.appCardCoverUrl()) + } +}