Skip to content

Commit 627bd9d

Browse files
committed
improve chat viewport restoration and scrolling precision
- introduce `ChatScrollCommand` for unified handling of viewport restoration, jumping to messages, and scrolling to bottom - implement `ChatViewportCacheEntry` to store anchor message ID and precise pixel offsets - add staged scrolling to handle large distance jumps more efficiently - update `CacheProvider` and `CachePreferences` to persist and retrieve detailed viewport state - replace simple message ID scrolling with the new command-based system in `ChatContent` - enhance logic for restoring scroll position when opening chats or topic threads - optimize viewport snapshot capturing and saving on lifecycle events
1 parent 6340b9d commit 627bd9d

12 files changed

Lines changed: 804 additions & 128 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.monogram.domain.models
2+
3+
import kotlinx.serialization.Serializable
4+
5+
@Serializable
6+
data class ChatViewportCacheEntry(
7+
val anchorMessageId: Long? = null,
8+
val anchorOffsetPx: Int = 0,
9+
val atBottom: Boolean = true
10+
)

domain/src/main/java/org/monogram/domain/repository/CacheProvider.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.monogram.domain.repository
33
import kotlinx.coroutines.flow.Flow
44
import kotlinx.coroutines.flow.StateFlow
55
import org.monogram.domain.models.AttachMenuBotModel
6+
import org.monogram.domain.models.ChatViewportCacheEntry
67
import org.monogram.domain.models.FolderModel
78
import org.monogram.domain.models.GifModel
89
import org.monogram.domain.models.RecentEmojiModel
@@ -34,6 +35,9 @@ interface CacheProvider {
3435
fun saveChatScrollPosition(chatId: Long, messageId: Long)
3536
fun getChatScrollPosition(chatId: Long): Long
3637

38+
fun saveChatViewport(chatId: Long, threadId: Long?, viewport: ChatViewportCacheEntry)
39+
fun getChatViewport(chatId: Long, threadId: Long?): ChatViewportCacheEntry?
40+
3741
fun setSavedGifs(gifs: List<GifModel>)
3842

3943
fun setInstalledStickerSets(sets: List<StickerSetModel>)

presentation/src/main/java/org/monogram/presentation/core/util/CachePreferences.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
66
import kotlinx.coroutines.flow.StateFlow
77
import kotlinx.serialization.json.Json
88
import org.monogram.domain.models.AttachMenuBotModel
9+
import org.monogram.domain.models.ChatViewportCacheEntry
910
import org.monogram.domain.models.FolderModel
1011
import org.monogram.domain.models.GifModel
1112
import org.monogram.domain.models.RecentEmojiModel
@@ -102,6 +103,33 @@ class CachePreferences(private val context: Context) : CacheProvider {
102103
return prefs.getLong("chat_scroll_$chatId", 0L)
103104
}
104105

106+
override fun saveChatViewport(chatId: Long, threadId: Long?, viewport: ChatViewportCacheEntry) {
107+
prefs.edit()
108+
.putString(chatViewportKey(chatId, threadId), Json.encodeToString(viewport))
109+
.apply()
110+
}
111+
112+
override fun getChatViewport(chatId: Long, threadId: Long?): ChatViewportCacheEntry? {
113+
val directJson = prefs.getString(chatViewportKey(chatId, threadId), null)
114+
if (!directJson.isNullOrBlank()) {
115+
return try {
116+
Json.decodeFromString<ChatViewportCacheEntry>(directJson)
117+
} catch (_: Exception) {
118+
null
119+
}
120+
}
121+
122+
if (threadId != null) return null
123+
if (!prefs.contains("chat_scroll_$chatId")) return null
124+
125+
val legacy = prefs.getLong("chat_scroll_$chatId", 0L)
126+
return if (legacy == 0L) {
127+
ChatViewportCacheEntry(atBottom = true)
128+
} else {
129+
ChatViewportCacheEntry(anchorMessageId = legacy, anchorOffsetPx = 0, atBottom = false)
130+
}
131+
}
132+
105133
override fun setSavedGifs(gifs: List<GifModel>) {
106134
prefs.edit().putString(KEY_SAVED_GIFS, Json.encodeToString(gifs)).apply()
107135
_savedGifs.value = gifs
@@ -138,5 +166,9 @@ class CachePreferences(private val context: Context) : CacheProvider {
138166

139167
companion object {
140168
private const val KEY_SAVED_GIFS = "saved_gifs"
169+
170+
private fun chatViewportKey(chatId: Long, threadId: Long?): String {
171+
return "chat_viewport_${chatId}_${threadId ?: 0L}"
172+
}
141173
}
142174
}

presentation/src/main/java/org/monogram/presentation/features/chats/currentChat/ChatComponent.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,22 @@ package org.monogram.presentation.features.chats.currentChat
33
import androidx.compose.runtime.Stable
44
import androidx.compose.ui.platform.Clipboard
55
import kotlinx.coroutines.flow.StateFlow
6-
import org.monogram.domain.models.*
6+
import org.monogram.domain.models.AttachMenuBotModel
7+
import org.monogram.domain.models.BotCommandModel
8+
import org.monogram.domain.models.BotMenuButtonModel
9+
import org.monogram.domain.models.ChatPermissionsModel
10+
import org.monogram.domain.models.ChatViewportCacheEntry
11+
import org.monogram.domain.models.GifModel
12+
import org.monogram.domain.models.InlineKeyboardButtonModel
13+
import org.monogram.domain.models.KeyboardButtonModel
14+
import org.monogram.domain.models.MessageEntity
15+
import org.monogram.domain.models.MessageModel
16+
import org.monogram.domain.models.MessageSendOptions
17+
import org.monogram.domain.models.MessageViewerModel
18+
import org.monogram.domain.models.StickerSetModel
19+
import org.monogram.domain.models.TopicModel
20+
import org.monogram.domain.models.UserModel
21+
import org.monogram.domain.models.WallpaperModel
722
import org.monogram.domain.repository.InlineBotResultsModel
823
import org.monogram.domain.repository.MessageRepository
924
import org.monogram.domain.repository.StickerRepository
@@ -80,11 +95,13 @@ interface ChatComponent {
8095
fun onShowAllPinnedMessages()
8196
fun onDismissPinnedMessages()
8297
fun onScrollToMessageConsumed()
98+
fun onScrollCommandConsumed()
8399
fun onScrollToBottom()
84100
fun onDownloadFile(fileId: Int)
85101
fun onDownloadHighRes(messageId: Long)
86102
fun onCancelDownloadFile(fileId: Int)
87103
fun updateScrollPosition(messageId: Long)
104+
fun updateViewport(viewport: ChatViewportCacheEntry)
88105
fun onBottomReached(isAtBottom: Boolean)
89106
fun onHighlightConsumed()
90107
fun onTyping()
@@ -222,10 +239,12 @@ interface ChatComponent {
222239
val pinnedMessageCount: Int = 0,
223240
val pinnedMessageIndex: Int = 0,
224241
val scrollToMessageId: Long? = null,
242+
val pendingScrollCommand: ChatScrollCommand? = null,
225243
val highlightedMessageId: Long? = null,
226244
val isAtBottom: Boolean = true,
227245
val currentScrollMessageId: Long = 0L,
228246
val lastScrollPosition: Long = 0L,
247+
val lastSavedViewport: ChatViewportCacheEntry? = null,
229248
val isLatestLoaded: Boolean = true,
230249
val isOldestLoaded: Boolean = false,
231250
val fontSize: Float = 16f,

0 commit comments

Comments
 (0)