@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.
Files changed (27) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/lib/components/InboxMessage/LeftSidebar.d.ts +2 -0
  3. package/lib/components/InboxMessage/LeftSidebar.d.ts.map +1 -1
  4. package/lib/components/InboxMessage/LeftSidebar.js +16 -5
  5. package/lib/components/InboxMessage/LeftSidebar.js.map +1 -1
  6. package/lib/components/InboxMessage/Messages.d.ts +3 -1
  7. package/lib/components/InboxMessage/Messages.d.ts.map +1 -1
  8. package/lib/components/InboxMessage/Messages.js +56 -15
  9. package/lib/components/InboxMessage/Messages.js.map +1 -1
  10. package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.d.ts +14 -0
  11. package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.d.ts.map +1 -0
  12. package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.js +138 -0
  13. package/lib/components/InboxMessage/message-widgets/SlackLikeMessageGroup.js.map +1 -0
  14. package/lib/components/InboxMessage/message-widgets/index.d.ts +1 -0
  15. package/lib/components/InboxMessage/message-widgets/index.d.ts.map +1 -1
  16. package/lib/components/slot-fill/chat-message-filler.js +1 -1
  17. package/lib/components/slot-fill/chat-message-filler.js.map +1 -1
  18. package/lib/container/Inbox.d.ts.map +1 -1
  19. package/lib/container/Inbox.js +293 -69
  20. package/lib/container/Inbox.js.map +1 -1
  21. package/lib/index.js +1 -1
  22. package/package.json +2 -2
  23. package/src/components/InboxMessage/LeftSidebar.tsx +14 -4
  24. package/src/components/InboxMessage/Messages.tsx +62 -15
  25. package/src/components/InboxMessage/message-widgets/SlackLikeMessageGroup.tsx +240 -0
  26. package/src/components/InboxMessage/message-widgets/index.ts +1 -0
  27. 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 h-full flex flex-col bg-white">
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 className="h-full overflow-y-auto p-2 sm:p-4 space-y-1">
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 messageList = useMemo(() => {
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 className="w-full" ref={innerRef}>
85
- {messageList?.map((message, index) => {
86
- return (
87
- <MessageSliceRenderer
88
- key={index}
89
- message={message}
90
- index={index}
91
- onOpen={onOpen}
92
- currentUser={currentUser}
93
- onMessageClick={onMessageClick}
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
+ };
@@ -1 +1,2 @@
1
1
  export { MessageSliceRenderer } from './MessageSliceRenderer';
2
+ export { SlackLikeMessageGroup } from './SlackLikeMessageGroup';