@messenger-box/tailwind-ui-inbox 10.0.3-alpha.67 → 10.0.3-alpha.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/components/InboxMessage/ConversationItem.d.ts +10 -7
  3. package/lib/components/InboxMessage/ConversationItem.d.ts.map +1 -1
  4. package/lib/components/InboxMessage/ConversationItem.js +58 -77
  5. package/lib/components/InboxMessage/ConversationItem.js.map +1 -1
  6. package/lib/components/InboxMessage/LeftSidebar.d.ts +2 -1
  7. package/lib/components/InboxMessage/LeftSidebar.d.ts.map +1 -1
  8. package/lib/components/InboxMessage/LeftSidebar.js +15 -8
  9. package/lib/components/InboxMessage/LeftSidebar.js.map +1 -1
  10. package/lib/components/InboxMessage/MessageInput.d.ts.map +1 -1
  11. package/lib/components/InboxMessage/MessageInput.js +15 -1
  12. package/lib/components/InboxMessage/MessageInput.js.map +1 -1
  13. package/lib/components/InboxMessage/Messages.d.ts.map +1 -1
  14. package/lib/components/InboxMessage/Messages.js +48 -13
  15. package/lib/components/InboxMessage/Messages.js.map +1 -1
  16. package/lib/components/InboxMessage/SubscriptionHandler.d.ts +19 -0
  17. package/lib/components/InboxMessage/SubscriptionHandler.d.ts.map +1 -0
  18. package/lib/components/InboxMessage/SubscriptionHandler.js +41 -0
  19. package/lib/components/InboxMessage/SubscriptionHandler.js.map +1 -0
  20. package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.d.ts +12 -0
  21. package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.d.ts.map +1 -0
  22. package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.js +134 -0
  23. package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.js.map +1 -0
  24. package/lib/components/InboxMessage/message-widgets/index.d.ts +1 -0
  25. package/lib/components/InboxMessage/message-widgets/index.d.ts.map +1 -1
  26. package/lib/components/slot-fill/chat-message-filler.js +1 -1
  27. package/lib/components/slot-fill/chat-message-filler.js.map +1 -1
  28. package/lib/container/Inbox.d.ts.map +1 -1
  29. package/lib/container/Inbox.js +188 -60
  30. package/lib/container/Inbox.js.map +1 -1
  31. package/lib/container/InboxWithLoader.d.ts +10 -3
  32. package/lib/container/InboxWithLoader.d.ts.map +1 -1
  33. package/lib/container/InboxWithLoader.js +81 -30
  34. package/lib/container/InboxWithLoader.js.map +1 -1
  35. package/lib/container/ServiceInbox.js +1 -1
  36. package/lib/container/ServiceInbox.js.map +1 -1
  37. package/lib/container/ThreadMessages.js +1 -1
  38. package/lib/container/ThreadMessages.js.map +1 -1
  39. package/lib/container/ThreadMessagesInbox.js +1 -1
  40. package/lib/container/ThreadMessagesInbox.js.map +1 -1
  41. package/lib/container/Threads.js +1 -1
  42. package/lib/container/Threads.js.map +1 -1
  43. package/lib/index.js +1 -1
  44. package/package.json +4 -4
  45. package/src/components/InboxMessage/ConversationItem.tsx +188 -186
  46. package/src/components/InboxMessage/LeftSidebar.tsx +20 -11
  47. package/src/components/InboxMessage/MessageInput.tsx +16 -1
  48. package/src/components/InboxMessage/Messages.tsx +48 -11
  49. package/src/components/InboxMessage/SubscriptionHandler.tsx +55 -0
  50. package/src/components/InboxMessage/message-widgets/SlackLikeMessageGroup.tsx +208 -0
  51. package/src/components/InboxMessage/message-widgets/index.ts +1 -0
  52. package/src/container/Inbox.tsx +194 -66
  53. package/src/container/InboxWithLoader.tsx +104 -38
@@ -0,0 +1,55 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ /**
4
+ * Shared SubscriptionHandler for Apollo subscribeToMore
5
+ *
6
+ * @param subscribeToMore - Apollo subscribeToMore function
7
+ * @param document - GraphQL subscription document
8
+ * @param variables - Variables for the subscription
9
+ * @param updateQuery - Apollo updateQuery function
10
+ * @param onError - Optional error handler
11
+ * @param enabled - If false, disables the subscription
12
+ */
13
+ export function SubscriptionHandler({
14
+ subscribeToMore,
15
+ document,
16
+ variables,
17
+ updateQuery,
18
+ onError,
19
+ enabled = true,
20
+ }: {
21
+ subscribeToMore: Function;
22
+ document: any;
23
+ variables: Record<string, any>;
24
+ updateQuery: (prev: any, { subscriptionData }: any) => any;
25
+ onError?: (error: any) => void;
26
+ enabled?: boolean;
27
+ }) {
28
+ useEffect(() => {
29
+ if (!enabled) return;
30
+
31
+ console.log('SubscriptionHandler: Setting up subscription with variables:', variables);
32
+
33
+ const unsubscribe = subscribeToMore({
34
+ document,
35
+ variables,
36
+ updateQuery,
37
+ onError,
38
+ });
39
+
40
+ console.log('SubscriptionHandler: Subscription setup successful, unsubscribe function:', unsubscribe);
41
+
42
+ return () => {
43
+ console.log('SubscriptionHandler: Cleaning up subscription');
44
+ if (unsubscribe && typeof unsubscribe === 'function') {
45
+ try {
46
+ unsubscribe();
47
+ } catch (error) {
48
+ console.error('Error unsubscribing:', error);
49
+ }
50
+ }
51
+ };
52
+ }, [subscribeToMore, document, variables, updateQuery, onError, enabled]);
53
+
54
+ return null;
55
+ }
@@ -0,0 +1,208 @@
1
+ import React from 'react';
2
+ import { format, formatDistanceToNow, differenceInMinutes } from 'date-fns';
3
+ import { IAuthUser, IPost } from 'common';
4
+ import { FilesList } from '../../inbox';
5
+
6
+ interface SlackLikeMessageGroupProps {
7
+ messages: IPost[];
8
+ currentUser: IAuthUser;
9
+ onOpen: (element?: any) => void;
10
+ onMessageClick: (msg: IPost) => void;
11
+ }
12
+
13
+ interface MessageGroupProps {
14
+ author: any;
15
+ messages: IPost[];
16
+ currentUser: IAuthUser;
17
+ onOpen: (element?: any) => void;
18
+ onMessageClick: (msg: IPost) => void;
19
+ }
20
+
21
+ // Utility function to group messages by user and time
22
+ export const groupMessagesByUserAndTime = (messages: IPost[], timeThresholdMinutes = 5): IPost[][] => {
23
+ if (!messages || messages.length === 0) return [];
24
+
25
+ const groups: IPost[][] = [];
26
+ let currentGroup: IPost[] = [];
27
+ let lastMessage: IPost | null = null;
28
+
29
+ for (const message of messages) {
30
+ if (typeof message === 'string') continue; // Skip date separators
31
+
32
+ const shouldStartNewGroup =
33
+ !lastMessage ||
34
+ lastMessage.author?.id !== message.author?.id ||
35
+ differenceInMinutes(new Date(message.createdAt), new Date(lastMessage.createdAt)) > timeThresholdMinutes;
36
+
37
+ if (shouldStartNewGroup) {
38
+ if (currentGroup.length > 0) {
39
+ groups.push(currentGroup);
40
+ }
41
+ currentGroup = [message];
42
+ } else {
43
+ currentGroup.push(message);
44
+ }
45
+
46
+ lastMessage = message;
47
+ }
48
+
49
+ if (currentGroup.length > 0) {
50
+ groups.push(currentGroup);
51
+ }
52
+
53
+ return groups;
54
+ };
55
+
56
+ const MessageGroup: React.FC<MessageGroupProps> = ({ author, messages, currentUser, onOpen, onMessageClick }) => {
57
+ const isOwnMessage = author?.id === currentUser?.id;
58
+ const authorName =
59
+ author?.givenName && author?.familyName
60
+ ? `${author.givenName} ${author.familyName}`
61
+ : author?.username || 'Unknown User';
62
+
63
+ const firstMessage = messages[0];
64
+ const formatTime = (timestamp: string) => {
65
+ const date = new Date(timestamp);
66
+ return format(date, 'h:mm a');
67
+ };
68
+
69
+ return (
70
+ <div className="mb-1 group hover:bg-white hover:bg-opacity-60 -mx-4 px-4 py-1 rounded transition-colors">
71
+ <div className="flex items-start space-x-2">
72
+ {/* Avatar - show for all messages */}
73
+ <div className="flex-shrink-0 mt-0.5">
74
+ <img
75
+ className="w-9 h-9 rounded-lg cursor-pointer hover:opacity-80 transition-opacity"
76
+ src={author?.picture || '/default-avatar.svg'}
77
+ alt={authorName}
78
+ onClick={() => onOpen(firstMessage)}
79
+ onError={(e) => {
80
+ e.currentTarget.src = '/default-avatar.svg';
81
+ }}
82
+ />
83
+ </div>
84
+
85
+ <div className="flex-1 min-w-0">
86
+ {/* Author name and timestamp - show for all messages */}
87
+ <div className="flex items-center space-x-2 mb-1">
88
+ <span className="text-sm font-bold text-gray-900">{authorName}</span>
89
+ <span className="text-xs text-gray-500">{formatTime(firstMessage.createdAt)}</span>
90
+ {isOwnMessage && <span className="text-xs text-gray-400 italic">(you)</span>}
91
+ </div>
92
+
93
+ {/* Messages in the group - single line for each message */}
94
+ <div className="space-y-1">
95
+ {messages.map((message, index) => (
96
+ <MessageBubble
97
+ key={message.id}
98
+ message={message}
99
+ isOwnMessage={isOwnMessage}
100
+ isFirstInGroup={index === 0}
101
+ isLastInGroup={index === messages.length - 1}
102
+ showTimestamp={isOwnMessage && index === 0}
103
+ onMessageClick={onMessageClick}
104
+ totalInGroup={messages.length}
105
+ authorName={authorName}
106
+ formatTime={formatTime}
107
+ />
108
+ ))}
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ );
114
+ };
115
+
116
+ interface MessageBubbleProps {
117
+ message: IPost;
118
+ isOwnMessage: boolean;
119
+ isFirstInGroup: boolean;
120
+ isLastInGroup: boolean;
121
+ showTimestamp: boolean;
122
+ onMessageClick: (msg: IPost) => void;
123
+ totalInGroup: number;
124
+ authorName: string;
125
+ formatTime: (timestamp: string) => string;
126
+ }
127
+
128
+ const MessageBubble: React.FC<MessageBubbleProps> = ({
129
+ message,
130
+ isOwnMessage,
131
+ isFirstInGroup,
132
+ isLastInGroup,
133
+ showTimestamp,
134
+ onMessageClick,
135
+ totalInGroup,
136
+ authorName,
137
+ formatTime,
138
+ }) => {
139
+ const handleClick = () => {
140
+ onMessageClick?.(message);
141
+ };
142
+
143
+ // All messages use the same format (Slack style - left aligned)
144
+ return (
145
+ <div className="py-0.5 hover:bg-gray-50 hover:bg-opacity-50 rounded px-1 -mx-1 group">
146
+ <div className="text-sm text-gray-900 cursor-pointer hover:bg-gray-100 px-1 rounded" onClick={handleClick}>
147
+ {/* Show timestamp on hover */}
148
+ <span className="text-xs text-gray-500 opacity-0 group-hover:opacity-100 transition-opacity float-right ml-2">
149
+ {formatTime(message.createdAt)}
150
+ </span>
151
+
152
+ {message.message && (
153
+ <span className="whitespace-pre-wrap break-words leading-relaxed">{message.message}</span>
154
+ )}
155
+
156
+ {message.files?.totalCount > 0 && (
157
+ <div className="mt-1 clear-both">
158
+ <FilesList uploaded files={message.files.data} />
159
+ </div>
160
+ )}
161
+ <div className={`${navigator.userAgent.includes('Firefox') ? 'mt-1' : ''} clear-both`}></div>
162
+
163
+ {/* Show delivery status for own messages */}
164
+ {/* {isOwnMessage && message.isDelivered !== undefined && (
165
+ <div className="text-xs text-gray-400 mt-1 clear-both">
166
+ {message.isDelivered
167
+ ? message.isRead
168
+ ? '✓✓ Read'
169
+ : '✓✓ Delivered'
170
+ : '✓ Sent'
171
+ }
172
+ </div>
173
+ )} */}
174
+ </div>
175
+ </div>
176
+ );
177
+ };
178
+
179
+ export const SlackLikeMessageGroup: React.FC<SlackLikeMessageGroupProps> = ({
180
+ messages,
181
+ currentUser,
182
+ onOpen,
183
+ onMessageClick,
184
+ }) => {
185
+ // Filter out non-message items (like date strings)
186
+ const actualMessages = messages.filter((msg) => typeof msg !== 'string') as IPost[];
187
+
188
+ // Group messages by user and time
189
+ const messageGroups = groupMessagesByUserAndTime(actualMessages);
190
+
191
+ return (
192
+ <div className="space-y-2">
193
+ {messageGroups.map((group, groupIndex) => {
194
+ const author = group[0]?.author;
195
+ return (
196
+ <MessageGroup
197
+ key={`group-${groupIndex}-${group[0]?.id}`}
198
+ author={author}
199
+ messages={group}
200
+ currentUser={currentUser}
201
+ onOpen={onOpen}
202
+ onMessageClick={onMessageClick}
203
+ />
204
+ );
205
+ })}
206
+ </div>
207
+ );
208
+ };
@@ -1 +1,2 @@
1
1
  export { MessageSliceRenderer } from './MessageSliceRenderer';
2
+ export { SlackLikeMessageGroup } from './SlackLikeMessageGroup';
@@ -18,6 +18,7 @@ import { objectId } from '@messenger-box/core';
18
18
  import { ThreadsInbox } from './ThreadsInbox';
19
19
  import { ThreadMessagesInbox } from './ThreadMessagesInbox';
20
20
  import { useApolloClient } from '@apollo/client';
21
+ import { SubscriptionHandler } from '../components/InboxMessage/SubscriptionHandler';
21
22
 
22
23
  const { MESSAGES_PER_PAGE } = config;
23
24
 
@@ -151,11 +152,16 @@ const Inbox = (props: InboxProps) => {
151
152
  return uniqBy([...(userChannels?.supportServiceChannels ?? []), ...(userChannels?.channelsByUser ?? [])], 'id');
152
153
  }, [userChannels]);
153
154
 
155
+ // Memoize stable channel array to prevent unnecessary re-renders
156
+ const stableChannels = useMemo(() => {
157
+ return channels || [];
158
+ }, [channels]);
159
+
154
160
  // Memoized values derived from Apollo cache data
155
161
  const channelFilters = useMemo(() => {
156
162
  const filters = { ...channelFilterProp };
157
163
  const channelType = filters?.type ?? RoomType.Direct;
158
- filters.type = supportServices ? [channelType, RoomType.Service] : RoomType.Direct;
164
+ filters.type = supportServices ? [channelType, RoomType.Service] : channelType;
159
165
  return filters;
160
166
  }, [channelFilterProp, supportServices]);
161
167
 
@@ -168,10 +174,11 @@ const Inbox = (props: InboxProps) => {
168
174
  );
169
175
  }, [channels]);
170
176
 
171
- const currentUser = useMemo(
172
- () => users?.find((user) => user && user.alias?.includes(auth?.authUserId)),
173
- [users, auth?.authUserId],
174
- );
177
+ // const currentUser = useMemo(
178
+ // () => users?.find((user) => user && user.alias?.includes(auth?.authUserId)),
179
+ // [users, auth?.authUserId],
180
+ // );
181
+ const currentUser = auth;
175
182
 
176
183
  const channelName = useMemo(() => {
177
184
  if (!channels || !pathChannelId) return '';
@@ -226,8 +233,8 @@ const Inbox = (props: InboxProps) => {
226
233
  const basePath = pathPrefix ? `${pathPrefix}${mainPath}` : mainPath;
227
234
 
228
235
  const searchParams = new URLSearchParams();
229
- if (channelRole) searchParams.set('channelRole', channelRole);
230
- if (orgName) searchParams.set('orgName', orgName);
236
+ // if (channelRole) searchParams.set('channelRole', channelRole);
237
+ // if (orgName) searchParams.set('orgName', orgName);
231
238
 
232
239
  const newPath = searchParams.toString() ? `${basePath}?${searchParams.toString()}` : basePath;
233
240
  navigate(newPath, { replace: true });
@@ -282,7 +289,7 @@ const Inbox = (props: InboxProps) => {
282
289
  >
283
290
  <LeftSidebar
284
291
  currentUser={currentUser}
285
- userChannels={channels || []}
292
+ userChannels={stableChannels}
286
293
  userChannelsLoading={userChannelsLoading}
287
294
  users={users}
288
295
  handleSelectChannel={handleSelectChannel}
@@ -290,6 +297,7 @@ const Inbox = (props: InboxProps) => {
290
297
  channelToTop={0}
291
298
  getChannelsRefetch={getChannelsRefetch}
292
299
  role={channelRole}
300
+ messagesQuery={data?.[1]}
293
301
  />
294
302
  </div>
295
303
 
@@ -321,13 +329,13 @@ const Inbox = (props: InboxProps) => {
321
329
 
322
330
  {/* Right Sidebar - Desktop Only */}
323
331
  {pathChannelId && data?.[1] && !isMobileView && (
324
- <div className="w-80 xl:w-96 border-l border-gray-200 bg-white flex-shrink-0">
325
- <RightSidebarWrapper
326
- MessagesLoaderQuery={data?.[1]}
327
- selectedPost={null}
328
- detailSidebarOptions={detailSidebarOptions}
329
- />
330
- </div>
332
+ // <div className="w-80 xl:w-96 border-l border-gray-200 bg-white flex-shrink-0">
333
+ <RightSidebarWrapper
334
+ MessagesLoaderQuery={data?.[1]}
335
+ selectedPost={null}
336
+ detailSidebarOptions={detailSidebarOptions}
337
+ />
338
+ // </div>
331
339
  )}
332
340
  </div>
333
341
  );
@@ -492,14 +500,14 @@ const RightSidebarWrapper = React.memo(({ MessagesLoaderQuery, selectedPost, det
492
500
  if (!sortedMessages.length) return null;
493
501
 
494
502
  return (
495
- <div className="w-80 xl:w-96 border-l border-gray-200 bg-white flex-shrink-0">
496
- <RightSidebar
497
- channelMessages={sortedMessages}
498
- visibility="visible"
499
- selectedPost={selectedPost}
500
- {...detailSidebarOptions}
501
- />
502
- </div>
503
+ // <div className="w-80 xl:w-96 border-l border-gray-200 bg-white flex-shrink-0">
504
+ <RightSidebar
505
+ channelMessages={sortedMessages}
506
+ visibility="visible"
507
+ selectedPost={selectedPost}
508
+ {...detailSidebarOptions}
509
+ />
510
+ // </div>
503
511
  );
504
512
  });
505
513
 
@@ -509,6 +517,9 @@ const MessagesComponent = React.memo((props: any) => {
509
517
  const messageRootListRef = useRef(null);
510
518
  const messageListRef = useRef(null);
511
519
  const apolloClient = useApolloClient();
520
+ const [isLoadingOlder, setIsLoadingOlder] = React.useState(false);
521
+ const isLoadingOlderRef = useRef(false);
522
+ const scrollTimeoutRef = useRef(null);
512
523
 
513
524
  const auth = useSelector(userSelector);
514
525
  const { startUpload } = useUploadFiles();
@@ -525,21 +536,28 @@ const MessagesComponent = React.memo((props: any) => {
525
536
  const totalCount = data?.messages?.totalCount || 0;
526
537
 
527
538
  const scrollToBottom = useCallback(() => {
528
- if (messageListRef?.current && messageRootListRef?.current) {
539
+ if (messageRootListRef?.current) {
529
540
  messageRootListRef.current.scrollTop = messageRootListRef.current.scrollHeight;
530
541
  }
531
542
  }, []);
532
543
 
533
- // Auto-scroll on new messages
544
+ // Auto-scroll on new messages (but not when loading older messages)
534
545
  useEffect(() => {
535
- const timer = setTimeout(() => scrollToBottom(), 100);
536
- return () => clearTimeout(timer);
546
+ if (!isLoadingOlderRef.current) {
547
+ const timer = setTimeout(() => scrollToBottom(), 100);
548
+ return () => clearTimeout(timer);
549
+ }
537
550
  }, [messages.length, scrollToBottom]);
538
551
 
539
552
  const onFetchOld = useCallback(
540
553
  async (skip: number) => {
541
- if (channelId && fetchMoreMessages) {
554
+ if (channelId && fetchMoreMessages && !isLoadingOlder) {
542
555
  try {
556
+ setIsLoadingOlder(true);
557
+ isLoadingOlderRef.current = true;
558
+ // Capture current scroll height before fetching
559
+ const oldScrollHeight = messageRootListRef?.current?.scrollHeight || 0;
560
+
543
561
  await fetchMoreMessages({
544
562
  variables: {
545
563
  channelId: channelId.toString(),
@@ -562,28 +580,119 @@ const MessagesComponent = React.memo((props: any) => {
562
580
  },
563
581
  });
564
582
 
565
- if (messageRootListRef?.current) {
566
- messageRootListRef.current.scrollTop += messageRootListRef.current.clientHeight;
567
- }
583
+ // Maintain scroll position after loading older messages
584
+ setTimeout(() => {
585
+ if (messageRootListRef?.current) {
586
+ const newScrollHeight = messageRootListRef.current.scrollHeight;
587
+ const scrollDiff = newScrollHeight - oldScrollHeight;
588
+ // For normal flex layout, maintain position by adjusting scroll offset
589
+ messageRootListRef.current.scrollTop = scrollDiff;
590
+ }
591
+ // Reset the loading flag after position is maintained
592
+ setTimeout(() => {
593
+ isLoadingOlderRef.current = false;
594
+ }, 50);
595
+ }, 100);
568
596
  } catch (error) {
569
597
  console.error('Error fetching older messages:', error);
598
+ isLoadingOlderRef.current = false;
599
+ } finally {
600
+ setIsLoadingOlder(false);
570
601
  }
571
602
  }
572
603
  },
573
- [channelId, fetchMoreMessages],
604
+ [channelId, fetchMoreMessages, isLoadingOlder],
574
605
  );
575
606
 
607
+ // Scroll to bottom when channel changes
608
+ useEffect(() => {
609
+ if (channelId && messages.length > 0) {
610
+ isLoadingOlderRef.current = false; // Reset flag on channel change
611
+ const timer = setTimeout(() => scrollToBottom(), 200);
612
+ return () => clearTimeout(timer);
613
+ }
614
+ }, [channelId, scrollToBottom]);
615
+
616
+ // Alternative scroll detection for Firefox
617
+ useEffect(() => {
618
+ const element = messageRootListRef.current;
619
+ if (!element) return;
620
+
621
+ // Firefox-specific scroll detection using passive listeners
622
+ const handleScrollEnd = () => {
623
+ if (!isLoadingOlder && element) {
624
+ const { scrollTop } = element;
625
+ const isAtTop = Math.round(scrollTop) <= 30;
626
+ const hasMoreMessages = totalCount > messages.length;
627
+
628
+ if (isAtTop && hasMoreMessages) {
629
+ console.log('ScrollEnd triggered load more (Firefox):', {
630
+ scrollTop: Math.round(scrollTop),
631
+ totalCount,
632
+ messagesLength: messages.length,
633
+ });
634
+ onFetchOld(messages.length);
635
+ }
636
+ }
637
+ };
638
+
639
+ // Use scrollend event if available (modern Firefox/Chrome)
640
+ if ('onscrollend' in element) {
641
+ element.addEventListener('scrollend', handleScrollEnd, { passive: true });
642
+ return () => {
643
+ element.removeEventListener('scrollend', handleScrollEnd);
644
+ };
645
+ }
646
+ }, [totalCount, messages.length, onFetchOld, isLoadingOlder]);
647
+
648
+ // Cleanup scroll timeout on unmount
649
+ useEffect(() => {
650
+ return () => {
651
+ if (scrollTimeoutRef.current) {
652
+ clearTimeout(scrollTimeoutRef.current);
653
+ }
654
+ };
655
+ }, []);
656
+
576
657
  const onMessagesScroll = useCallback(
577
658
  async (e: any) => {
578
- if (messageRootListRef.current) {
579
- const { clientHeight, scrollHeight, scrollTop } = messageRootListRef.current;
659
+ // Throttle scroll events for better performance, especially in Firefox
660
+ if (scrollTimeoutRef.current) {
661
+ clearTimeout(scrollTimeoutRef.current);
662
+ }
580
663
 
581
- if (clientHeight - scrollTop === scrollHeight && totalCount > messages.length) {
582
- await onFetchOld(messages.length);
664
+ scrollTimeoutRef.current = setTimeout(async () => {
665
+ if (messageRootListRef.current && !isLoadingOlder) {
666
+ const element = messageRootListRef.current;
667
+ const { clientHeight, scrollHeight, scrollTop } = element;
668
+
669
+ // Firefox-compatible scroll detection
670
+ // Use Math.ceil to handle Firefox's fractional scrollTop values
671
+ const isAtTop = Math.ceil(scrollTop) <= 25;
672
+ const hasMoreMessages = totalCount > messages.length;
673
+
674
+ // Additional Firefox-specific check
675
+ const isFirefox = navigator.userAgent.includes('Firefox');
676
+ const firefoxAdjustedTop = isFirefox ? Math.round(scrollTop) <= 30 : isAtTop;
677
+
678
+ if ((isAtTop || firefoxAdjustedTop) && hasMoreMessages) {
679
+ console.log('Triggering load more:', {
680
+ scrollTop: Math.ceil(scrollTop),
681
+ originalScrollTop: scrollTop,
682
+ totalCount,
683
+ messagesLength: messages.length,
684
+ scrollHeight,
685
+ clientHeight,
686
+ browser: isFirefox ? 'Firefox' : 'Other',
687
+ isAtTop,
688
+ firefoxAdjustedTop,
689
+ });
690
+ await onFetchOld(messages.length);
691
+ }
583
692
  }
584
- }
693
+ }, 100);
585
694
  },
586
- [totalCount, messages.length, onFetchOld],
695
+ [totalCount, messages.length, onFetchOld, isLoadingOlder],
587
696
  );
588
697
 
589
698
  // Optimistic message sending with Apollo cache updates
@@ -760,37 +869,56 @@ const MessagesComponent = React.memo((props: any) => {
760
869
  <>
761
870
  <div
762
871
  ref={messageRootListRef}
763
- className="flex flex-col-reverse flex-grow flex-shrink overflow-y-auto p-4 px-[10px] md:px-[50px] lg:px-[80px]"
872
+ className="flex flex-col flex-grow flex-shrink overflow-y-auto p-4 px-4 md:px-8 lg:px-12 bg-gray-50"
764
873
  onScroll={onMessagesScroll}
765
874
  >
766
875
  {messages.length > 0 ? (
767
- <Messages
768
- innerRef={messageListRef}
769
- channelId={channelId}
770
- currentUser={auth}
771
- channelMessages={messages}
772
- totalCount={totalCount}
773
- onMessageClick={onMessageClick}
774
- subscribeToNewMessages={() =>
775
- subscribeToMore?.({
776
- document: CHAT_MESSAGE_ADDED,
777
- variables: { channelId: channelId.toString() },
778
- updateQuery: (prev: any, { subscriptionData }: any) => {
779
- if (!subscriptionData.data) return prev;
780
- const newMessage = subscriptionData.data.chatMessageAdded;
781
-
782
- return {
783
- ...prev,
784
- messages: {
785
- ...prev?.messages,
786
- data: uniqBy([...(prev?.messages?.data || []), newMessage], 'id'),
787
- totalCount: (prev?.messages?.totalCount || 0) + 1,
788
- },
789
- };
790
- },
791
- })
792
- }
793
- />
876
+ <>
877
+ {/* Loading indicator for older messages at the top */}
878
+ {isLoadingOlder && (
879
+ <div className="flex justify-center py-4">
880
+ <div className="flex items-center space-x-2 text-gray-500">
881
+ <Spinner className="w-4 h-4" />
882
+ <span className="text-sm">Loading older messages...</span>
883
+ </div>
884
+ </div>
885
+ )}
886
+ <Messages
887
+ innerRef={messageListRef}
888
+ channelId={channelId}
889
+ currentUser={auth}
890
+ channelMessages={messages}
891
+ totalCount={totalCount}
892
+ onMessageClick={onMessageClick}
893
+ />
894
+ <SubscriptionHandler
895
+ subscribeToMore={subscribeToMore}
896
+ document={CHAT_MESSAGE_ADDED}
897
+ variables={{ channelId: channelId.toString() }}
898
+ enabled={!!channelId && !!subscribeToMore}
899
+ updateQuery={(prev: any, { subscriptionData }: any) => {
900
+ console.log('Subscription updateQuery called:', { prev, subscriptionData });
901
+ if (!subscriptionData.data) {
902
+ console.log('No subscription data, returning prev');
903
+ return prev;
904
+ }
905
+ const newMessage = subscriptionData.data.chatMessageAdded;
906
+ console.log('New message received via subscription:', newMessage);
907
+
908
+ return {
909
+ ...prev,
910
+ messages: {
911
+ ...prev?.messages,
912
+ data: uniqBy([...(prev?.messages?.data || []), newMessage], 'id'),
913
+ totalCount: (prev?.messages?.totalCount || 0) + 1,
914
+ },
915
+ };
916
+ }}
917
+ onError={(error) => {
918
+ console.error('Subscription error:', error);
919
+ }}
920
+ />
921
+ </>
794
922
  ) : (
795
923
  <div className="flex-1 flex items-center justify-center text-gray-500">
796
924
  <div className="text-center">