@messenger-box/tailwind-ui-inbox 10.0.3-alpha.69 → 10.0.3-alpha.71
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/LeftSidebar.d.ts +2 -0
- package/lib/components/InboxMessage/LeftSidebar.d.ts.map +1 -1
- package/lib/components/InboxMessage/LeftSidebar.js +16 -5
- package/lib/components/InboxMessage/LeftSidebar.js.map +1 -1
- package/lib/components/InboxMessage/Messages.d.ts +3 -1
- package/lib/components/InboxMessage/Messages.d.ts.map +1 -1
- package/lib/components/InboxMessage/Messages.js +56 -15
- package/lib/components/InboxMessage/Messages.js.map +1 -1
- package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.d.ts +14 -0
- package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.d.ts.map +1 -0
- package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.js +138 -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 +293 -69
- package/lib/container/Inbox.js.map +1 -1
- package/lib/index.js +1 -1
- package/package.json +2 -2
- package/src/components/InboxMessage/LeftSidebar.tsx +14 -4
- package/src/components/InboxMessage/Messages.tsx +62 -15
- package/src/components/InboxMessage/message-widgets/SlackLikeMessageGroup.tsx +240 -0
- package/src/components/InboxMessage/message-widgets/index.ts +1 -0
- package/src/container/Inbox.tsx +391 -134
|
@@ -20,6 +20,8 @@ type LeftSidebarProps = {
|
|
|
20
20
|
supportServices?: any;
|
|
21
21
|
role?: any;
|
|
22
22
|
messagesQuery?: any;
|
|
23
|
+
windowHeight?: number;
|
|
24
|
+
windowWidth?: number;
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
export const LeftSidebar = React.memo((props: LeftSidebarProps) => {
|
|
@@ -33,6 +35,8 @@ export const LeftSidebar = React.memo((props: LeftSidebarProps) => {
|
|
|
33
35
|
supportServices,
|
|
34
36
|
role,
|
|
35
37
|
messagesQuery,
|
|
38
|
+
windowHeight = 768,
|
|
39
|
+
windowWidth = 1024,
|
|
36
40
|
} = props;
|
|
37
41
|
const [keyword, setKeyword] = useState('');
|
|
38
42
|
const { t } = useTranslation('translations');
|
|
@@ -59,12 +63,18 @@ export const LeftSidebar = React.memo((props: LeftSidebarProps) => {
|
|
|
59
63
|
);
|
|
60
64
|
}
|
|
61
65
|
return (
|
|
62
|
-
<div className="w-full
|
|
63
|
-
<div className="p-3 sm:p-4 border-b border-gray-200">
|
|
66
|
+
<div className="w-full flex flex-col bg-white" style={{ height: `${windowHeight}px`, maxHeight: '100vh' }}>
|
|
67
|
+
<div className="flex-shrink-0 p-3 sm:p-4 border-b border-gray-200">
|
|
64
68
|
<SearchInput keyword={keyword} setKeyword={setKeyword} />
|
|
65
69
|
</div>
|
|
66
|
-
<div className="flex-1 overflow-hidden">
|
|
67
|
-
<div
|
|
70
|
+
<div className="flex-1 min-h-0 overflow-hidden">
|
|
71
|
+
<div
|
|
72
|
+
className="overflow-y-auto p-2 sm:p-4 space-y-1"
|
|
73
|
+
style={{
|
|
74
|
+
height: `${windowHeight - 80}px`, // Subtract header height
|
|
75
|
+
minHeight: 0,
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
68
78
|
<>
|
|
69
79
|
{supportServices ? supportServices : <></>}
|
|
70
80
|
|
|
@@ -3,6 +3,7 @@ import React, { useMemo, useRef, useEffect, useState } from 'react';
|
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { UserModalContent } from './UserModalContent';
|
|
5
5
|
import { MessageSliceRenderer } from './message-widgets';
|
|
6
|
+
import { SlackLikeMessageGroup } from './message-widgets/SlackLikeMessageGroup';
|
|
6
7
|
|
|
7
8
|
interface MessagesProps {
|
|
8
9
|
channelId: number;
|
|
@@ -14,6 +15,8 @@ interface MessagesProps {
|
|
|
14
15
|
subscribeToNewMessages?: () => any;
|
|
15
16
|
subscribeToNewServiceMessages?: () => any;
|
|
16
17
|
onMessageClick: (msg) => void;
|
|
18
|
+
isDesktopView?: boolean;
|
|
19
|
+
isSmallScreen?: boolean;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export const Messages = ({
|
|
@@ -25,6 +28,8 @@ export const Messages = ({
|
|
|
25
28
|
subscribeToNewMessages,
|
|
26
29
|
subscribeToNewServiceMessages,
|
|
27
30
|
onMessageClick,
|
|
31
|
+
isDesktopView = false,
|
|
32
|
+
isSmallScreen = false,
|
|
28
33
|
}: MessagesProps) => {
|
|
29
34
|
const [isOpen, setIsOpen] = useState(false);
|
|
30
35
|
const [selectedElement, setSelectedElement] = useState(null);
|
|
@@ -60,7 +65,7 @@ export const Messages = ({
|
|
|
60
65
|
}
|
|
61
66
|
}, [channelMessages]);
|
|
62
67
|
|
|
63
|
-
const
|
|
68
|
+
const messageListWithDates = useMemo(() => {
|
|
64
69
|
let currentDate = '';
|
|
65
70
|
let res = [];
|
|
66
71
|
channelMessages?.map((msg) => {
|
|
@@ -71,7 +76,7 @@ export const Messages = ({
|
|
|
71
76
|
else msgDate = format(new Date(msg.createdAt), 'eee, do MMMM');
|
|
72
77
|
|
|
73
78
|
if (msgDate !== currentDate) {
|
|
74
|
-
res.push(msgDate);
|
|
79
|
+
res.push({ type: 'date', content: msgDate });
|
|
75
80
|
currentDate = msgDate;
|
|
76
81
|
}
|
|
77
82
|
res.push(msg);
|
|
@@ -79,21 +84,63 @@ export const Messages = ({
|
|
|
79
84
|
return res;
|
|
80
85
|
}, [channelMessages]);
|
|
81
86
|
|
|
87
|
+
// Group messages by date sections for Slack-like rendering
|
|
88
|
+
const messagesByDate = useMemo(() => {
|
|
89
|
+
const sections = [];
|
|
90
|
+
let currentSection = { date: null, messages: [] };
|
|
91
|
+
|
|
92
|
+
messageListWithDates.forEach((item) => {
|
|
93
|
+
if (item?.type === 'date') {
|
|
94
|
+
if (currentSection.messages.length > 0) {
|
|
95
|
+
sections.push(currentSection);
|
|
96
|
+
}
|
|
97
|
+
currentSection = { date: item.content, messages: [] };
|
|
98
|
+
} else {
|
|
99
|
+
currentSection.messages.push(item);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (currentSection.messages.length > 0) {
|
|
104
|
+
sections.push(currentSection);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return sections;
|
|
108
|
+
}, [messageListWithDates]);
|
|
109
|
+
|
|
82
110
|
return (
|
|
83
111
|
<>
|
|
84
|
-
<div
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
112
|
+
<div
|
|
113
|
+
className={`w-full pb-8 pt-4 ${
|
|
114
|
+
isDesktopView ? 'space-y-8 max-w-full mx-auto' : isSmallScreen ? 'space-y-4' : 'space-y-6'
|
|
115
|
+
}`}
|
|
116
|
+
ref={innerRef}
|
|
117
|
+
>
|
|
118
|
+
{messagesByDate?.map((section, sectionIndex) => (
|
|
119
|
+
<div key={`section-${sectionIndex}`} className="w-full">
|
|
120
|
+
{/* Date separator */}
|
|
121
|
+
{section.date && (
|
|
122
|
+
<div className="flex items-center justify-center my-6">
|
|
123
|
+
<div className="flex-grow border-t border-gray-200"></div>
|
|
124
|
+
<div className="mx-4 px-3 py-1 bg-white border border-gray-200 rounded-full text-xs font-medium text-gray-600">
|
|
125
|
+
{section.date}
|
|
126
|
+
</div>
|
|
127
|
+
<div className="flex-grow border-t border-gray-200"></div>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Messages grouped by user and time */}
|
|
132
|
+
<div className={`${isDesktopView ? 'mb-6' : 'mb-4'}`}>
|
|
133
|
+
<SlackLikeMessageGroup
|
|
134
|
+
messages={section.messages}
|
|
135
|
+
currentUser={currentUser}
|
|
136
|
+
onOpen={onOpen}
|
|
137
|
+
onMessageClick={onMessageClick}
|
|
138
|
+
isDesktopView={isDesktopView}
|
|
139
|
+
isSmallScreen={isSmallScreen}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
))}
|
|
97
144
|
</div>
|
|
98
145
|
<ChatModal element={selectedElement} isOpen={isOpen} onClose={onClose} />
|
|
99
146
|
</>
|
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
isDesktopView?: boolean;
|
|
12
|
+
isSmallScreen?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface MessageGroupProps {
|
|
16
|
+
author: any;
|
|
17
|
+
messages: IPost[];
|
|
18
|
+
currentUser: IAuthUser;
|
|
19
|
+
onOpen: (element?: any) => void;
|
|
20
|
+
onMessageClick: (msg: IPost) => void;
|
|
21
|
+
isDesktopView?: boolean;
|
|
22
|
+
isSmallScreen?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Utility function to group messages by user and time
|
|
26
|
+
export const groupMessagesByUserAndTime = (messages: IPost[], timeThresholdMinutes = 5): IPost[][] => {
|
|
27
|
+
if (!messages || messages.length === 0) return [];
|
|
28
|
+
|
|
29
|
+
const groups: IPost[][] = [];
|
|
30
|
+
let currentGroup: IPost[] = [];
|
|
31
|
+
let lastMessage: IPost | null = null;
|
|
32
|
+
|
|
33
|
+
for (const message of messages) {
|
|
34
|
+
if (typeof message === 'string') continue; // Skip date separators
|
|
35
|
+
|
|
36
|
+
const shouldStartNewGroup =
|
|
37
|
+
!lastMessage ||
|
|
38
|
+
lastMessage.author?.id !== message.author?.id ||
|
|
39
|
+
differenceInMinutes(new Date(message.createdAt), new Date(lastMessage.createdAt)) > timeThresholdMinutes;
|
|
40
|
+
|
|
41
|
+
if (shouldStartNewGroup) {
|
|
42
|
+
if (currentGroup.length > 0) {
|
|
43
|
+
groups.push(currentGroup);
|
|
44
|
+
}
|
|
45
|
+
currentGroup = [message];
|
|
46
|
+
} else {
|
|
47
|
+
currentGroup.push(message);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
lastMessage = message;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (currentGroup.length > 0) {
|
|
54
|
+
groups.push(currentGroup);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return groups;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const MessageGroup: React.FC<MessageGroupProps> = ({
|
|
61
|
+
author,
|
|
62
|
+
messages,
|
|
63
|
+
currentUser,
|
|
64
|
+
onOpen,
|
|
65
|
+
onMessageClick,
|
|
66
|
+
isDesktopView = false,
|
|
67
|
+
isSmallScreen = false,
|
|
68
|
+
}) => {
|
|
69
|
+
const isOwnMessage = author?.id === currentUser?.id;
|
|
70
|
+
const authorName =
|
|
71
|
+
author?.givenName && author?.familyName
|
|
72
|
+
? `${author.givenName} ${author.familyName}`
|
|
73
|
+
: author?.username || 'Unknown User';
|
|
74
|
+
|
|
75
|
+
const firstMessage = messages[0];
|
|
76
|
+
const formatTime = (timestamp: string) => {
|
|
77
|
+
const date = new Date(timestamp);
|
|
78
|
+
return format(date, 'h:mm a');
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
className={`group hover:bg-white hover:bg-opacity-60 rounded transition-colors ${
|
|
84
|
+
isDesktopView ? 'mb-8 -mx-6 px-6 py-4' : isSmallScreen ? 'mb-4 -mx-2 px-2 py-2' : 'mb-6 -mx-4 px-4 py-3'
|
|
85
|
+
}`}
|
|
86
|
+
>
|
|
87
|
+
<div
|
|
88
|
+
className={`flex items-start ${
|
|
89
|
+
isDesktopView ? 'space-x-4' : isSmallScreen ? 'space-x-2' : 'space-x-3'
|
|
90
|
+
}`}
|
|
91
|
+
>
|
|
92
|
+
{/* Avatar - show for all messages */}
|
|
93
|
+
<div className="flex-shrink-0 mt-0.5">
|
|
94
|
+
<img
|
|
95
|
+
className={`rounded-lg cursor-pointer hover:opacity-80 transition-opacity ${
|
|
96
|
+
isDesktopView ? 'w-12 h-12' : isSmallScreen ? 'w-8 h-8' : 'w-10 h-10'
|
|
97
|
+
}`}
|
|
98
|
+
src={author?.picture || '/default-avatar.svg'}
|
|
99
|
+
alt={authorName}
|
|
100
|
+
onClick={() => onOpen(firstMessage)}
|
|
101
|
+
onError={(e) => {
|
|
102
|
+
e.currentTarget.src = '/default-avatar.svg';
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="flex-1 min-w-0 overflow-hidden">
|
|
108
|
+
{/* Author name and timestamp - show for all messages */}
|
|
109
|
+
<div className="flex items-center space-x-2 mb-1">
|
|
110
|
+
<span className="text-sm font-semibold text-gray-900 truncate">{authorName}</span>
|
|
111
|
+
<span className="text-xs text-gray-500 flex-shrink-0">
|
|
112
|
+
{formatTime(firstMessage.createdAt)}
|
|
113
|
+
</span>
|
|
114
|
+
{isOwnMessage && <span className="text-xs text-gray-400 italic flex-shrink-0">(you)</span>}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Messages in the group - single line for each message */}
|
|
118
|
+
<div className="space-y-0.5">
|
|
119
|
+
{messages.map((message, index) => (
|
|
120
|
+
<MessageBubble
|
|
121
|
+
key={message.id}
|
|
122
|
+
message={message}
|
|
123
|
+
isOwnMessage={isOwnMessage}
|
|
124
|
+
isFirstInGroup={index === 0}
|
|
125
|
+
isLastInGroup={index === messages.length - 1}
|
|
126
|
+
showTimestamp={isOwnMessage && index === 0}
|
|
127
|
+
onMessageClick={onMessageClick}
|
|
128
|
+
totalInGroup={messages.length}
|
|
129
|
+
authorName={authorName}
|
|
130
|
+
formatTime={formatTime}
|
|
131
|
+
/>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
interface MessageBubbleProps {
|
|
141
|
+
message: IPost;
|
|
142
|
+
isOwnMessage: boolean;
|
|
143
|
+
isFirstInGroup: boolean;
|
|
144
|
+
isLastInGroup: boolean;
|
|
145
|
+
showTimestamp: boolean;
|
|
146
|
+
onMessageClick: (msg: IPost) => void;
|
|
147
|
+
totalInGroup: number;
|
|
148
|
+
authorName: string;
|
|
149
|
+
formatTime: (timestamp: string) => string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
153
|
+
message,
|
|
154
|
+
isOwnMessage,
|
|
155
|
+
isFirstInGroup,
|
|
156
|
+
isLastInGroup,
|
|
157
|
+
showTimestamp,
|
|
158
|
+
onMessageClick,
|
|
159
|
+
totalInGroup,
|
|
160
|
+
authorName,
|
|
161
|
+
formatTime,
|
|
162
|
+
}) => {
|
|
163
|
+
const handleClick = () => {
|
|
164
|
+
onMessageClick?.(message);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// All messages use the same format (Slack style - left aligned)
|
|
168
|
+
return (
|
|
169
|
+
<div className="py-1 hover:bg-gray-50 hover:bg-opacity-50 rounded px-1 sm:px-2 -mx-1 sm:-mx-2 group relative">
|
|
170
|
+
<div
|
|
171
|
+
className="text-sm text-gray-900 cursor-pointer hover:bg-gray-100 px-1 sm:px-2 py-1 rounded"
|
|
172
|
+
onClick={handleClick}
|
|
173
|
+
>
|
|
174
|
+
{/* Show timestamp on hover */}
|
|
175
|
+
<span className="text-xs text-gray-500 opacity-0 group-hover:opacity-100 transition-opacity absolute right-1 sm:right-2 top-1">
|
|
176
|
+
{formatTime(message.createdAt)}
|
|
177
|
+
</span>
|
|
178
|
+
|
|
179
|
+
{message.message && (
|
|
180
|
+
<div className="whitespace-pre-wrap break-words leading-relaxed pr-12 sm:pr-16">
|
|
181
|
+
{message.message}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{message.files?.totalCount > 0 && (
|
|
186
|
+
<div className="mt-2 pr-12 sm:pr-16">
|
|
187
|
+
<FilesList uploaded files={message.files.data} />
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
|
|
191
|
+
{/* Show delivery status for own messages */}
|
|
192
|
+
{/* {isOwnMessage && message.isDelivered !== undefined && (
|
|
193
|
+
<div className="text-xs text-gray-400 mt-1 clear-both">
|
|
194
|
+
{message.isDelivered
|
|
195
|
+
? message.isRead
|
|
196
|
+
? '✓✓ Read'
|
|
197
|
+
: '✓✓ Delivered'
|
|
198
|
+
: '✓ Sent'
|
|
199
|
+
}
|
|
200
|
+
</div>
|
|
201
|
+
)} */}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const SlackLikeMessageGroup: React.FC<SlackLikeMessageGroupProps> = ({
|
|
208
|
+
messages,
|
|
209
|
+
currentUser,
|
|
210
|
+
onOpen,
|
|
211
|
+
onMessageClick,
|
|
212
|
+
isDesktopView = false,
|
|
213
|
+
isSmallScreen = false,
|
|
214
|
+
}) => {
|
|
215
|
+
// Filter out non-message items (like date strings)
|
|
216
|
+
const actualMessages = messages.filter((msg) => typeof msg !== 'string') as IPost[];
|
|
217
|
+
|
|
218
|
+
// Group messages by user and time
|
|
219
|
+
const messageGroups = groupMessagesByUserAndTime(actualMessages);
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div className={`min-h-fit ${isDesktopView ? 'space-y-8' : isSmallScreen ? 'space-y-4' : 'space-y-6'}`}>
|
|
223
|
+
{messageGroups.map((group, groupIndex) => {
|
|
224
|
+
const author = group[0]?.author;
|
|
225
|
+
return (
|
|
226
|
+
<MessageGroup
|
|
227
|
+
key={`group-${groupIndex}-${group[0]?.id}`}
|
|
228
|
+
author={author}
|
|
229
|
+
messages={group}
|
|
230
|
+
currentUser={currentUser}
|
|
231
|
+
onOpen={onOpen}
|
|
232
|
+
onMessageClick={onMessageClick}
|
|
233
|
+
isDesktopView={isDesktopView}
|
|
234
|
+
isSmallScreen={isSmallScreen}
|
|
235
|
+
/>
|
|
236
|
+
);
|
|
237
|
+
})}
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
};
|