feat: harden chat list sync and polish media viewer
- prevent stale refreshChat snapshots from overwriting newer realtime chat list state - add reaction overlay and double-tap zoom in the media viewer
This commit is contained in:
@@ -89,6 +89,9 @@ interface ChatDao {
|
||||
@Query("SELECT muted FROM chats WHERE id = :chatId LIMIT 1")
|
||||
suspend fun isChatMuted(chatId: Long): Boolean?
|
||||
|
||||
@Query("SELECT * FROM chats WHERE id = :chatId LIMIT 1")
|
||||
suspend fun getChatEntity(chatId: Long): ChatEntity?
|
||||
|
||||
@Query("UPDATE chats SET muted = :muted WHERE id = :chatId")
|
||||
suspend fun updateChatMuted(chatId: Long, muted: Boolean)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import ru.daemonlord.messenger.data.chat.dto.ChatNotificationSettingsUpdateDto
|
||||
import ru.daemonlord.messenger.data.chat.dto.ChatProfileUpdateRequestDto
|
||||
import ru.daemonlord.messenger.data.chat.dto.ChatTitleUpdateRequestDto
|
||||
import ru.daemonlord.messenger.data.chat.dto.DiscoverChatDto
|
||||
import ru.daemonlord.messenger.data.chat.local.entity.ChatEntity
|
||||
import ru.daemonlord.messenger.data.chat.local.dao.ChatDao
|
||||
import ru.daemonlord.messenger.data.chat.mapper.toChatEntity
|
||||
import ru.daemonlord.messenger.data.chat.mapper.toDomain
|
||||
@@ -82,7 +83,21 @@ class NetworkChatRepository @Inject constructor(
|
||||
try {
|
||||
val chat = chatApiService.getChatById(chatId = chatId)
|
||||
chatDao.upsertUsers(chat.toUserShortEntityOrNull()?.let(::listOf).orEmpty())
|
||||
chatDao.upsertChats(listOf(chat.toChatEntity()))
|
||||
val incomingEntity = chat.toChatEntity()
|
||||
val localEntity = chatDao.getChatEntity(chatId = chatId)
|
||||
val mergedEntity = if (localEntity != null && localEntity.isLocallyNewerThan(incomingEntity)) {
|
||||
incomingEntity.copy(
|
||||
unreadCount = maxOf(localEntity.unreadCount, incomingEntity.unreadCount),
|
||||
unreadMentionsCount = maxOf(localEntity.unreadMentionsCount, incomingEntity.unreadMentionsCount),
|
||||
lastMessageText = localEntity.lastMessageText,
|
||||
lastMessageType = localEntity.lastMessageType,
|
||||
lastMessageCreatedAt = localEntity.lastMessageCreatedAt,
|
||||
updatedSortAt = localEntity.updatedSortAt,
|
||||
)
|
||||
} else {
|
||||
incomingEntity
|
||||
}
|
||||
chatDao.upsertChats(listOf(mergedEntity))
|
||||
AppResult.Success(Unit)
|
||||
} catch (error: Throwable) {
|
||||
AppResult.Error(error.toAppError())
|
||||
@@ -422,3 +437,11 @@ class NetworkChatRepository @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ChatEntity.isLocallyNewerThan(other: ChatEntity): Boolean {
|
||||
val localSort = updatedSortAt ?: lastMessageCreatedAt
|
||||
val remoteSort = other.updatedSortAt ?: other.lastMessageCreatedAt
|
||||
if (localSort.isNullOrBlank()) return false
|
||||
if (remoteSort.isNullOrBlank()) return true
|
||||
return localSort > remoteSort
|
||||
}
|
||||
|
||||
@@ -656,7 +656,12 @@ private fun ChatScreen(
|
||||
val view = LocalView.current
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val allViewerMediaItems = remember(state.messages) { buildChatViewerMediaItems(state.messages) }
|
||||
val allViewerMediaItems = remember(state.messages, state.reactionByMessageId) {
|
||||
buildChatViewerMediaItems(
|
||||
messages = state.messages,
|
||||
reactionByMessageId = state.reactionByMessageId,
|
||||
)
|
||||
}
|
||||
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
||||
val isChannelChat = state.chatType.equals("channel", ignoreCase = true)
|
||||
val isPrivateChat = state.chatType.equals("private", ignoreCase = true)
|
||||
@@ -2451,6 +2456,7 @@ private fun PrivateChatInfoCard(
|
||||
private data class ChatViewerMediaItem(
|
||||
val url: String,
|
||||
val type: ChatViewerMediaType,
|
||||
val reactions: List<ru.daemonlord.messenger.domain.message.model.MessageReaction> = emptyList(),
|
||||
)
|
||||
|
||||
@Composable
|
||||
@@ -2518,6 +2524,37 @@ private fun ChatMediaViewerOverlay(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = showTopBar && items.getOrNull(pagerState.currentPage)?.reactions?.isNotEmpty() == true,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items.getOrNull(pagerState.currentPage)
|
||||
?.reactions
|
||||
?.forEach { reaction ->
|
||||
Surface(
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
color = Color.White.copy(alpha = 0.14f),
|
||||
) {
|
||||
Text(
|
||||
text = "${reaction.emoji} ${reaction.count}",
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = if (reaction.reacted) FontWeight.SemiBold else FontWeight.Normal,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2588,6 +2625,15 @@ private fun ZoomableImageViewerPage(
|
||||
.pointerInput(imageUrl) {
|
||||
detectTapGestures(
|
||||
onTap = { onToggleChrome() },
|
||||
onDoubleTap = {
|
||||
if (scale > 1.3f) {
|
||||
scale = 1f
|
||||
offset = Offset.Zero
|
||||
} else {
|
||||
scale = 2.2f
|
||||
offset = Offset.Zero
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
@@ -6512,10 +6558,14 @@ private fun buildChatInfoEntries(
|
||||
return entries
|
||||
}
|
||||
|
||||
private fun buildChatViewerMediaItems(messages: List<MessageItem>): List<ChatViewerMediaItem> {
|
||||
private fun buildChatViewerMediaItems(
|
||||
messages: List<MessageItem>,
|
||||
reactionByMessageId: Map<Long, List<ru.daemonlord.messenger.domain.message.model.MessageReaction>> = emptyMap(),
|
||||
): List<ChatViewerMediaItem> {
|
||||
return messages
|
||||
.flatMap { message ->
|
||||
val messageType = message.type.lowercase(Locale.getDefault())
|
||||
val reactions = reactionByMessageId[message.id].orEmpty()
|
||||
val items = message.attachments.mapNotNull { attachment ->
|
||||
val fileType = attachment.fileType.lowercase(Locale.getDefault())
|
||||
when {
|
||||
@@ -6524,6 +6574,7 @@ private fun buildChatViewerMediaItems(messages: List<MessageItem>): List<ChatVie
|
||||
!fileType.contains("webp") -> ChatViewerMediaItem(
|
||||
url = attachment.fileUrl,
|
||||
type = ChatViewerMediaType.Image,
|
||||
reactions = reactions,
|
||||
)
|
||||
|
||||
fileType.startsWith("video/") &&
|
||||
@@ -6531,6 +6582,7 @@ private fun buildChatViewerMediaItems(messages: List<MessageItem>): List<ChatVie
|
||||
!messageType.contains("video_note") -> ChatViewerMediaItem(
|
||||
url = attachment.fileUrl,
|
||||
type = ChatViewerMediaType.Video,
|
||||
reactions = reactions,
|
||||
)
|
||||
|
||||
else -> null
|
||||
@@ -6549,11 +6601,13 @@ private fun buildChatViewerMediaItems(messages: List<MessageItem>): List<ChatVie
|
||||
!isStickerLikeUrl(legacyUrl) -> items += ChatViewerMediaItem(
|
||||
url = legacyUrl,
|
||||
type = ChatViewerMediaType.Image,
|
||||
reactions = reactions,
|
||||
)
|
||||
|
||||
messageType == "video" -> items += ChatViewerMediaItem(
|
||||
url = legacyUrl,
|
||||
type = ChatViewerMediaType.Video,
|
||||
reactions = reactions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user