@@ -45,20 +45,101 @@ export function authorDefault(): User {
4545export default class MessageGrouper {
4646 private readonly groupsSignal : Signal < MessageGroup [ ] >
4747 private currentGroup ?: Message [ ]
48-
4948 private fetchBefore ?: bigint
5049 private fetchLock : boolean = false
50+ private loading : Accessor < boolean >
51+ private setLoading : Setter < boolean >
5152 nonced : Map < string , [ number , number ] >
5253 noMoreMessages : Accessor < boolean >
5354 private setNoMoreMessages : Setter < boolean >
55+ private oldestMessageId ?: Accessor < bigint | undefined >
56+ private setOldestMessageId : Setter < bigint | undefined >
57+ private newestMessageId ?: Accessor < bigint | undefined >
58+ private setNewestMessageId : Setter < bigint | undefined >
59+ private hasGap : Accessor < boolean >
60+ private setHasGap : Setter < boolean >
5461
5562 constructor (
5663 private readonly api : Api ,
5764 private readonly channelId : bigint ,
5865 ) {
5966 this . groupsSignal = createSignal ( [ ] as MessageGroup [ ] )
6067 this . nonced = new Map ( ) ;
61- [ this . noMoreMessages , this . setNoMoreMessages ] = createSignal ( false )
68+ [ this . noMoreMessages , this . setNoMoreMessages ] = createSignal ( false ) ;
69+ [ this . loading , this . setLoading ] = createSignal ( false ) ;
70+ [ this . oldestMessageId , this . setOldestMessageId ] = createSignal < bigint | undefined > ( undefined ) ;
71+ [ this . newestMessageId , this . setNewestMessageId ] = createSignal < bigint | undefined > ( undefined ) ;
72+ [ this . hasGap , this . setHasGap ] = createSignal ( false ) ;
73+ }
74+
75+ get isLoading ( ) {
76+ return this . loading ( )
77+ }
78+
79+ get hasMessageGap ( ) {
80+ return this . hasGap ( )
81+ }
82+
83+ get oldestLoaded ( ) {
84+ return this . oldestMessageId ?.( )
85+ }
86+
87+ get newestLoaded ( ) {
88+ return this . newestMessageId ?.( )
89+ }
90+
91+ /**
92+ * Update the oldest and newest message IDs after message insertion
93+ */
94+ private updateMessageBoundaries ( ) {
95+ let oldestId : bigint | undefined ;
96+ let newestId : bigint | undefined ;
97+
98+ for ( const group of this . groups ) {
99+ if ( group . isDivider ) continue ;
100+
101+ for ( const message of group ) {
102+ if ( ! oldestId || message . id < oldestId ) {
103+ oldestId = message . id ;
104+ }
105+ if ( ! newestId || message . id > newestId ) {
106+ newestId = message . id ;
107+ }
108+ }
109+ }
110+
111+ this . setOldestMessageId ( oldestId ) ;
112+ this . setNewestMessageId ( newestId ) ;
113+ }
114+
115+ /**
116+ * Determines if there is a gap between the current message context and the newest loaded messages
117+ */
118+ checkForMessageGap ( ) {
119+ if ( ! this . oldestLoaded || ! this . newestLoaded ) {
120+ this . setHasGap ( false ) ;
121+ return false ;
122+ }
123+
124+ // If the difference between newest and oldest is significantly larger than the loaded message count would suggest,
125+ // then we likely have a gap
126+ const totalMessageCount = this . getTotalMessageCount ( ) ;
127+ const expectedDiff = BigInt ( totalMessageCount * 2 ) ; // rough estimate assuming uniform message ID distribution
128+ const actualDiff = this . newestLoaded - this . oldestLoaded ;
129+
130+ const hasGap = actualDiff > expectedDiff && actualDiff > BigInt ( MESSAGE_HISTORY_LIMIT * 3 ) ;
131+ this . setHasGap ( hasGap ) ;
132+ return hasGap ;
133+ }
134+
135+ /**
136+ * Gets the total number of actual messages (excluding dividers)
137+ */
138+ getTotalMessageCount ( ) : number {
139+ return this . groups . reduce ( ( count , group ) => {
140+ if ( group . isDivider ) return count ;
141+ return count + group . length ;
142+ } , 0 ) ;
62143 }
63144
64145 /**
@@ -139,27 +220,57 @@ export default class MessageGrouper {
139220 insertMessages ( messages : Message [ ] ) {
140221 if ( messages . length === 0 ) return
141222
223+ // Create a set of existing message IDs to avoid duplicates
224+ const existingMessageIds = new Set < string > ( ) ;
225+ for ( const group of this . groups ) {
226+ if ( group . isDivider ) continue ;
227+
228+ for ( const message of group ) {
229+ existingMessageIds . add ( message . id . toString ( ) ) ;
230+ }
231+ }
232+
233+ // Filter out duplicate messages
234+ const uniqueMessages = messages . filter ( message => ! existingMessageIds . has ( message . id . toString ( ) ) ) ;
235+
236+ if ( uniqueMessages . length === 0 ) return ;
237+
142238 let groups = this . groups
143239 if ( this . currentGroup == null ) groups . push ( [ ] )
144240
145- let [ groupIndex , messageIndex ] = this . findCloseMessageIndex ( messages [ 0 ] . id )
146- let lastMessage : Message
241+ let [ groupIndex , messageIndex ] = this . findCloseMessageIndex ( uniqueMessages [ 0 ] . id )
242+ let lastMessage : Message | undefined
147243
148244 if ( groupIndex <= 0 ) {
149245 let firstMessageGroup = groups [ 0 ]
150- if ( firstMessageGroup . isDivider || groupIndex < 0 )
246+ if ( firstMessageGroup ? .isDivider || groupIndex < 0 )
151247 groups = [ firstMessageGroup = [ ] , ...groups ]
152248
153- lastMessage = ( < Message [ ] > firstMessageGroup ) [ 0 ]
249+ // Only set lastMessage if the group actually has messages
250+ if ( firstMessageGroup . length > 0 ) {
251+ lastMessage = firstMessageGroup [ 0 ]
252+ }
154253 groupIndex = 0
155254 } else {
156255 lastMessage = ( < Message [ ] > groups [ groupIndex ] ) [ messageIndex ]
157256 }
158257
159- for ( const message of messages ) {
160- const behavior = this . nextMessageBehavior ( { lastMessage, message } )
258+ for ( const message of uniqueMessages ) {
259+ // Skip divider logic for the first message if we don't have a previous message for reference
260+ const behavior = lastMessage
261+ ? this . nextMessageBehavior ( { lastMessage, message } )
262+ : false
263+
161264 if ( behavior ) {
162- if ( behavior !== true ) groups . splice ( ++ groupIndex , 0 , behavior )
265+ if ( behavior !== true ) {
266+ // Only add a divider if the dates are actually different
267+ const messageDate = new Date ( snowflakes . timestamp ( message . id ) ) . toDateString ( )
268+ const lastMessageDate = new Date ( snowflakes . timestamp ( lastMessage ! . id ) ) . toDateString ( )
269+
270+ if ( messageDate !== lastMessageDate ) {
271+ groups . splice ( ++ groupIndex , 0 , behavior )
272+ }
273+ }
163274 groups . splice ( ++ groupIndex , 0 , [ ] )
164275 messageIndex = - 1
165276 }
@@ -171,28 +282,148 @@ export default class MessageGrouper {
171282
172283 this . setGroups ( [ ...groups ] )
173284 this . currentGroup = last ( groups ) as any
285+
286+ // Update oldest and newest message IDs after inserting messages
287+ this . updateMessageBoundaries ( ) ;
288+
289+ // Check if we have a gap in message history
290+ this . checkForMessageGap ( ) ;
291+ }
292+
293+ /**
294+ * Finds a message by ID and scrolls to it, loading surrounding messages if needed
295+ */
296+ async findMessage ( id : bigint ) : Promise < [ number , number ] | null > {
297+ // First check if we already have the message
298+ const [ groupIndex , messageIndex ] = this . findCloseMessageIndex ( id )
299+ if ( groupIndex >= 0 ) {
300+ const group = this . groups [ groupIndex ]
301+ if ( ! group . isDivider ) {
302+ const message = group [ messageIndex ]
303+ if ( message . id === id ) {
304+ return [ groupIndex , messageIndex ]
305+ }
306+ }
307+ }
308+
309+ // If we don't have the message, load messages around it
310+ await this . fetchMessages ( id )
311+
312+ // Check again after loading
313+ const [ newGroupIndex , newMessageIndex ] = this . findCloseMessageIndex ( id )
314+ if ( newGroupIndex >= 0 ) {
315+ const group = this . groups [ newGroupIndex ]
316+ if ( ! group . isDivider ) {
317+ const message = group [ newMessageIndex ]
318+ if ( message . id === id ) {
319+ return [ newGroupIndex , newMessageIndex ]
320+ }
321+ }
322+ }
323+
324+ return null
174325 }
175326
176327 /**
177328 * Fetches messages from the API and inserts into the grouper.
329+ * @param targetId If provided, fetch messages around this ID
330+ * @param after If true, fetch messages after the provided targetId or last loaded message
331+ * @param before If true, fetch messages before the provided targetId
178332 */
179- async fetchMessages ( ) {
180- if ( this . noMoreMessages ( ) || this . fetchLock )
333+ async fetchMessages ( targetId ?: bigint , after ?: boolean , before ?: boolean ) {
334+ if ( this . fetchLock )
181335 return
182336
183337 this . fetchLock = true
338+ this . setLoading ( true )
184339 const params : Record < string , any > = { limit : MESSAGE_HISTORY_LIMIT }
185- if ( this . fetchBefore != null )
340+
341+ if ( targetId ) {
342+ if ( after ) {
343+ // Fetch messages after the target ID
344+ params . after = targetId
345+ } else if ( before ) {
346+ // Fetch messages before the target ID
347+ params . before = targetId
348+ } else {
349+ // Fetch messages around the target ID
350+ params . around = targetId
351+ }
352+ } else if ( after ) {
353+ // Fetch messages after the oldest loaded message
354+ const lastGroup = this . groups [ this . groups . length - 1 ]
355+ if ( lastGroup && ! lastGroup . isDivider && lastGroup . length > 0 ) {
356+ const lastMessage = lastGroup [ lastGroup . length - 1 ]
357+ params . after = lastMessage . id
358+ } else {
359+ // If we have no messages, just return
360+ this . fetchLock = false
361+ this . setLoading ( false )
362+ return
363+ }
364+ } else if ( this . fetchBefore != null ) {
365+ // Fetch messages before the oldest loaded message
186366 params . before = this . fetchBefore
367+ }
187368
188- const response = await this . api . request < Message [ ] > ( 'GET' , `/channels/${ this . channelId } /messages` , { params } )
189- const messages = response . ensureOk ( ) . jsonOrThrow ( )
369+ try {
370+ const response = await this . api . request < Message [ ] > ( 'GET' , `/channels/${ this . channelId } /messages` , { params } )
371+ const messages = response . ensureOk ( ) . jsonOrThrow ( )
190372
191- this . fetchBefore = last ( messages ) ?. id
192- if ( messages . length < MESSAGE_HISTORY_LIMIT )
193- this . setNoMoreMessages ( true )
194- this . insertMessages ( messages . reverse ( ) )
195- this . fetchLock = false
373+ if ( messages . length === 0 ) {
374+ this . fetchLock = false
375+ this . setLoading ( false )
376+ return
377+ }
378+
379+ if ( ! targetId && ! after && ! before ) {
380+ this . fetchBefore = last ( messages ) ?. id
381+ if ( messages . length < MESSAGE_HISTORY_LIMIT )
382+ this . setNoMoreMessages ( true )
383+ }
384+
385+ this . insertMessages ( messages . reverse ( ) )
386+
387+ // After inserting messages, check if we still have a gap
388+ this . checkForMessageGap ( ) ;
389+ } finally {
390+ this . fetchLock = false
391+ this . setLoading ( false )
392+ }
393+ }
394+
395+ /**
396+ * Fetches the next chunk of messages to fill the gap between contexts
397+ */
398+ async fetchNextChunk ( ) {
399+ if ( ! this . oldestLoaded || ! this . newestLoaded ) return ;
400+
401+ // Determine whether we should fetch messages after the oldest or before the newest
402+ // If the gap is closer to the oldest loaded message, fetch after the oldest
403+ // If the gap is closer to the newest loaded message, fetch before the newest
404+
405+ const totalMessageCount = this . getTotalMessageCount ( ) ;
406+
407+ // If we have fewer than 50 messages loaded, we need to fetch more messages
408+ if ( totalMessageCount < 50 ) {
409+ // If we're closer to the oldest message context (likely jumped to an old message)
410+ // then fetch messages after the oldest message to move forward in time
411+ await this . fetchMessages ( this . oldestLoaded , true ) ;
412+ }
413+ // Otherwise, if we have a lot of old messages loaded (scrolled up a lot)
414+ // then fetch newer messages to move toward present time
415+ else {
416+ await this . fetchMessages ( this . newestLoaded , false , false ) ;
417+ }
418+ }
419+
420+ /**
421+ * Fetches the latest messages to jump to bottom
422+ */
423+ async fetchLatestMessages ( ) {
424+ this . setGroups ( [ ] ) ;
425+ this . fetchBefore = undefined ;
426+ await this . fetchMessages ( ) ;
196427 }
197428
198429 private nextMessageBehavior (
@@ -204,10 +435,18 @@ export default class MessageGrouper {
204435 lastMessage ??= this . lastMessage
205436 if ( ! lastMessage ) return false
206437
207- let timestamp
208- if ( ! isSameDay ( timestamp = snowflakes . timestamp ( message . id ) , snowflakes . timestamp ( lastMessage . id ) ) )
438+ let timestamp = snowflakes . timestamp ( message . id )
439+ let lastTimestamp = snowflakes . timestamp ( lastMessage . id )
440+
441+ const dateStr = new Date ( timestamp ) . toDateString ( )
442+ const lastDateStr = new Date ( lastTimestamp ) . toDateString ( )
443+
444+ // only add a day divider if the messages are from different days
445+ if ( dateStr !== lastDateStr ) {
209446 return { isDivider : true , content : humanizeDate ( timestamp ) }
447+ }
210448
449+ // group messages if they are from the same author and within 15 minutes
211450 return message . author_id !== lastMessage . author_id
212451 || message . id - lastMessage . id > SNOWFLAKE_BOUNDARY
213452 }
0 commit comments