@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.
- package/CHANGELOG.md +8 -0
- package/lib/components/InboxMessage/ConversationItem.d.ts +10 -7
- package/lib/components/InboxMessage/ConversationItem.d.ts.map +1 -1
- package/lib/components/InboxMessage/ConversationItem.js +58 -77
- package/lib/components/InboxMessage/ConversationItem.js.map +1 -1
- package/lib/components/InboxMessage/LeftSidebar.d.ts +2 -1
- package/lib/components/InboxMessage/LeftSidebar.d.ts.map +1 -1
- package/lib/components/InboxMessage/LeftSidebar.js +15 -8
- package/lib/components/InboxMessage/LeftSidebar.js.map +1 -1
- package/lib/components/InboxMessage/MessageInput.d.ts.map +1 -1
- package/lib/components/InboxMessage/MessageInput.js +15 -1
- package/lib/components/InboxMessage/MessageInput.js.map +1 -1
- package/lib/components/InboxMessage/Messages.d.ts.map +1 -1
- package/lib/components/InboxMessage/Messages.js +48 -13
- package/lib/components/InboxMessage/Messages.js.map +1 -1
- package/lib/components/InboxMessage/SubscriptionHandler.d.ts +19 -0
- package/lib/components/InboxMessage/SubscriptionHandler.d.ts.map +1 -0
- package/lib/components/InboxMessage/SubscriptionHandler.js +41 -0
- package/lib/components/InboxMessage/SubscriptionHandler.js.map +1 -0
- package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.d.ts +12 -0
- package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.d.ts.map +1 -0
- package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.js +134 -0
- package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.js.map +1 -0
- package/lib/components/InboxMessage/message-widgets/index.d.ts +1 -0
- package/lib/components/InboxMessage/message-widgets/index.d.ts.map +1 -1
- package/lib/components/slot-fill/chat-message-filler.js +1 -1
- package/lib/components/slot-fill/chat-message-filler.js.map +1 -1
- package/lib/container/Inbox.d.ts.map +1 -1
- package/lib/container/Inbox.js +188 -60
- package/lib/container/Inbox.js.map +1 -1
- package/lib/container/InboxWithLoader.d.ts +10 -3
- package/lib/container/InboxWithLoader.d.ts.map +1 -1
- package/lib/container/InboxWithLoader.js +81 -30
- package/lib/container/InboxWithLoader.js.map +1 -1
- package/lib/container/ServiceInbox.js +1 -1
- package/lib/container/ServiceInbox.js.map +1 -1
- package/lib/container/ThreadMessages.js +1 -1
- package/lib/container/ThreadMessages.js.map +1 -1
- package/lib/container/ThreadMessagesInbox.js +1 -1
- package/lib/container/ThreadMessagesInbox.js.map +1 -1
- package/lib/container/Threads.js +1 -1
- package/lib/container/Threads.js.map +1 -1
- package/lib/index.js +1 -1
- package/package.json +4 -4
- package/src/components/InboxMessage/ConversationItem.tsx +188 -186
- package/src/components/InboxMessage/LeftSidebar.tsx +20 -11
- package/src/components/InboxMessage/MessageInput.tsx +16 -1
- package/src/components/InboxMessage/Messages.tsx +48 -11
- package/src/components/InboxMessage/SubscriptionHandler.tsx +55 -0
- package/src/components/InboxMessage/message-widgets/SlackLikeMessageGroup.tsx +208 -0
- package/src/components/InboxMessage/message-widgets/index.ts +1 -0
- package/src/container/Inbox.tsx +194 -66
- 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
|
+
};
|
package/src/container/Inbox.tsx
CHANGED
|
@@ -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] :
|
|
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
|
-
|
|
173
|
-
|
|
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={
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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 (
|
|
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
|
-
|
|
536
|
-
|
|
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
|
-
|
|
566
|
-
|
|
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
|
-
|
|
579
|
-
|
|
659
|
+
// Throttle scroll events for better performance, especially in Firefox
|
|
660
|
+
if (scrollTimeoutRef.current) {
|
|
661
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
662
|
+
}
|
|
580
663
|
|
|
581
|
-
|
|
582
|
-
|
|
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
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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">
|