mr-chat-bird 1.0.0

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 (49) hide show
  1. package/LICENCE +21 -0
  2. package/README.md +26 -0
  3. package/app/layout.tsx +69 -0
  4. package/app/page.tsx +9 -0
  5. package/dist/AddReaction-DCDVOMZB.svg +1 -0
  6. package/dist/TextFormat-R4ZVDKE2.svg +1 -0
  7. package/dist/index.css +388 -0
  8. package/dist/index.d.mts +5 -0
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.js +6425 -0
  11. package/dist/index.mjs +6420 -0
  12. package/eslint.config.mjs +11 -0
  13. package/index.ts +1 -0
  14. package/next-env.d.ts +5 -0
  15. package/next.config.mjs +30 -0
  16. package/package.json +69 -0
  17. package/postcss.config.cjs +14 -0
  18. package/public/favicon.svg +1 -0
  19. package/src/components/ChatUserList/ChatUserList.module.css +253 -0
  20. package/src/components/ChatUserList/ChatUserList.tsx +434 -0
  21. package/src/components/ChatUserList/ChatUserList.type.tsx +12 -0
  22. package/src/components/ChatUserList/ChatUserMessage.tsx +362 -0
  23. package/src/components/ChatUserList/users_list.json +648 -0
  24. package/src/components/ColorSchemeToggle/ColorSchemeToggle.tsx +15 -0
  25. package/src/components/EmojiPickerPopover/EmojiPickerPopover.tsx +72 -0
  26. package/src/components/MrChat/index.tsx +34 -0
  27. package/src/components/RichTextEditor/DropzoneMenuItem.tsx +33 -0
  28. package/src/components/RichTextEditor/EmojiNode.tsx +36 -0
  29. package/src/components/RichTextEditor/RichTextEditor.module.css +95 -0
  30. package/src/components/RichTextEditor/RichTextEditor.tsx +248 -0
  31. package/src/components/UserProfile/UserProfileDrawer.module.css +120 -0
  32. package/src/components/UserProfile/UserProfileDrawer.tsx +115 -0
  33. package/src/components/VirtualizedList/ChatScrollContainer.tsx +92 -0
  34. package/src/components/VirtualizedList/index.tsx +31 -0
  35. package/src/lib/axios.ts +12 -0
  36. package/src/lib/socket.ts +29 -0
  37. package/src/store/provider.tsx +8 -0
  38. package/src/store/slices/ChatSlice.ts +249 -0
  39. package/src/store/socket/index.tsx +32 -0
  40. package/src/store/store.ts +11 -0
  41. package/src/theme.ts +84 -0
  42. package/src/utils/environment.ts +5 -0
  43. package/src/utils/helper.ts +36 -0
  44. package/src/utils/icons/richText/Add.svg +1 -0
  45. package/src/utils/icons/richText/AddReaction.svg +1 -0
  46. package/src/utils/icons/richText/Docs.svg +1 -0
  47. package/src/utils/icons/richText/Image.svg +1 -0
  48. package/src/utils/icons/richText/TextFormat.svg +1 -0
  49. package/tsconfig.json +25 -0
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Avatar, Text } from "@mantine/core";
5
+ import {
6
+ IconArrowLeft,
7
+ IconTrash,
8
+ IconBan,
9
+ } from "@tabler/icons-react";
10
+ import classes from "./UserProfileDrawer.module.css";
11
+
12
+ type UserProfileDrawerProps = {
13
+ opened: boolean;
14
+ onClose: () => void;
15
+ user?: {
16
+ avatar?: string;
17
+ username?: string;
18
+ displayName?: string;
19
+ };
20
+ onDeleteMessages?: () => void;
21
+ onBlock?: () => void;
22
+ };
23
+
24
+ export default function UserProfileDrawer({
25
+ opened,
26
+ onClose,
27
+ user,
28
+ onDeleteMessages,
29
+ onBlock,
30
+ }: UserProfileDrawerProps) {
31
+ const [visible, setVisible] = useState(opened);
32
+ const [showImagePreview, setShowImagePreview] = useState(false);
33
+
34
+ useEffect(() => {
35
+ if (opened) setVisible(true);
36
+ }, [opened]);
37
+
38
+ const handleClose = () => {
39
+ setVisible(false);
40
+ setTimeout(onClose, 250); // match animation duration
41
+ };
42
+
43
+ if (!opened && !visible) return null;
44
+
45
+ return (
46
+ <>
47
+ {/* Overlay */}
48
+ <div className={classes.overlay} onClick={handleClose}>
49
+ {/* Drawer */}
50
+ <div
51
+ className={`${classes.drawer} ${
52
+ visible ? classes.open : classes.close
53
+ }`}
54
+ onClick={(e) => e.stopPropagation()}
55
+ >
56
+ {/* Header */}
57
+ <div className={classes.header}>
58
+ <div className={classes.backBtn} onClick={handleClose}>
59
+ <IconArrowLeft size={20} />
60
+ </div>
61
+ <Text fw={600}>Contact info</Text>
62
+ </div>
63
+
64
+ {/* Profile */}
65
+ <div className={classes.profileSection}>
66
+ <Avatar
67
+ src={user?.avatar}
68
+ size={150}
69
+ radius="xl"
70
+ style={{ cursor: "pointer" }}
71
+ onClick={() => user?.avatar ? setShowImagePreview(true) : {}}
72
+ />
73
+
74
+ <Text fw={700} size="lg" mt="sm">
75
+ {user?.displayName}
76
+ </Text>
77
+
78
+ <Text size="sm" c="dimmed">
79
+ @{user?.username}
80
+ </Text>
81
+ </div>
82
+
83
+ {/* Actions */}
84
+ <div className={classes.actions}>
85
+ <div
86
+ className={`${classes.item} ${classes.itemDanger}`}
87
+ onClick={onDeleteMessages}
88
+ >
89
+ <IconTrash size={18} />
90
+ <span>Delete all messages</span>
91
+ </div>
92
+
93
+ <div
94
+ className={`${classes.item} ${classes.itemDanger}`}
95
+ onClick={onBlock}
96
+ >
97
+ <IconBan size={18} />
98
+ <span>Block</span>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+ {/* Fullscreen Image Preview */}
105
+ {showImagePreview && (
106
+ <div
107
+ className={classes.imagePreview}
108
+ onClick={() => setShowImagePreview(false)}
109
+ >
110
+ <img src={user?.avatar} alt="profile" />
111
+ </div>
112
+ )}
113
+ </>
114
+ );
115
+ }
@@ -0,0 +1,92 @@
1
+ "use client";
2
+
3
+ import React, { useRef, useEffect } from "react";
4
+
5
+ type Props<T> = {
6
+ data: T[];
7
+ renderItem: (item: T, index: number) => React.ReactNode;
8
+ loadMore: any;
9
+ hasMore: boolean;
10
+ scrollToBottomTrigger?: any;
11
+ };
12
+
13
+ export function ChatScrollContainer<T>({
14
+ data,
15
+ renderItem,
16
+ loadMore,
17
+ hasMore,
18
+ scrollToBottomTrigger,
19
+ }: Props<T>) {
20
+ const containerRef = useRef<HTMLDivElement>(null);
21
+ const isFetchingRef = useRef(false);
22
+ const isInitialLoadRef = useRef(true);
23
+ const prevLengthRef = useRef(0);
24
+
25
+ useEffect(() => {
26
+ const el = containerRef.current;
27
+ if (!el) return;
28
+
29
+ setTimeout(() => {
30
+ el.scrollTop = el.scrollHeight;
31
+ }, 100);
32
+ }, [scrollToBottomTrigger]);
33
+
34
+ useEffect(() => {
35
+ const el = containerRef.current;
36
+ if (!el || !data.length) return;
37
+
38
+ if (isInitialLoadRef.current) {
39
+ el.scrollTop = el.scrollHeight;
40
+ isInitialLoadRef.current = false;
41
+ }
42
+
43
+ prevLengthRef.current = data.length;
44
+ }, [data]);
45
+
46
+ const handleScroll = () => {
47
+ const el = containerRef.current;
48
+ if (!el) return;
49
+
50
+ if (el.scrollTop < 20 && hasMore && !isFetchingRef.current) {
51
+ loadOlderMessages();
52
+ }
53
+ };
54
+
55
+ const loadOlderMessages = async () => {
56
+ const el = containerRef.current;
57
+ if (!el || isFetchingRef.current || !hasMore) return;
58
+
59
+ isFetchingRef.current = true;
60
+
61
+ const prevScrollHeight = el.scrollHeight;
62
+ const prevScrollTop = el.scrollTop;
63
+
64
+ await loadMore();
65
+
66
+ setTimeout(() => {
67
+ const newScrollHeight = el.scrollHeight;
68
+ const heightDiff = newScrollHeight - prevScrollHeight;
69
+
70
+ el.scrollTop = prevScrollTop + heightDiff;
71
+
72
+ isFetchingRef.current = false;
73
+ }, 0);
74
+ };
75
+
76
+ return (
77
+ <div
78
+ ref={containerRef}
79
+ onScroll={handleScroll}
80
+ style={{
81
+ overflowY: "auto",
82
+ height: "100%",
83
+ display: "flex",
84
+ flexDirection: "column",
85
+ }}
86
+ >
87
+ {data.map((item, index) => (
88
+ <div key={(item as any)?.id || index}>{renderItem(item, index)}</div>
89
+ ))}
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import { Virtuoso, VirtuosoProps } from "react-virtuoso";
4
+
5
+ type Props<T> = {
6
+ data: T[];
7
+ itemContent: (index: number, item: T) => React.ReactNode;
8
+ height?: string | number;
9
+ startReached?: () => void;
10
+ endReached?: () => void;
11
+ } & VirtuosoProps<T, any>;
12
+
13
+ export function VirtualizedList<T>({
14
+ data,
15
+ itemContent,
16
+ height = "100%",
17
+ startReached,
18
+ endReached,
19
+ ...props
20
+ }: Props<T>) {
21
+ return (
22
+ <Virtuoso
23
+ style={{ height }}
24
+ data={data}
25
+ startReached={startReached}
26
+ endReached={endReached}
27
+ itemContent={(index, item) => itemContent(index, item)}
28
+ {...props}
29
+ />
30
+ );
31
+ }
@@ -0,0 +1,12 @@
1
+ import { environment } from '../utils/environment';
2
+ import axios from 'axios';
3
+
4
+ const axiosInstance = axios.create({
5
+ baseURL: environment.BASE_URL,
6
+ headers: {
7
+ 'Content-Type': 'application/json',
8
+ Authorization: environment.AUTHORIZATION
9
+ },
10
+ });
11
+
12
+ export default axiosInstance;
@@ -0,0 +1,29 @@
1
+ import { io, Socket } from "socket.io-client";
2
+ import { environment } from "../utils/environment";
3
+
4
+ let socket: Socket | null = null;
5
+ let currentUserId: string | undefined;
6
+
7
+ export const getSocket = (userId?: string): Socket | null => {
8
+ if (!userId) return null;
9
+
10
+ if (!socket || currentUserId !== userId) {
11
+ if (socket) {
12
+ socket.disconnect();
13
+ socket = null;
14
+ }
15
+
16
+ socket = io(environment.SOCKET_URL, {
17
+ autoConnect: false,
18
+ transports: ["websocket"],
19
+ auth: {
20
+ apiKey: environment.AUTHORIZATION,
21
+ userId: userId,
22
+ },
23
+ });
24
+
25
+ currentUserId = userId;
26
+ }
27
+
28
+ return socket;
29
+ };
@@ -0,0 +1,8 @@
1
+ 'use client';
2
+
3
+ import { Provider } from 'react-redux';
4
+ import { store } from './store';
5
+
6
+ export function ReduxProvider({ children }: { children: React.ReactNode }) {
7
+ return <Provider store={store}>{children}</Provider>;
8
+ }
@@ -0,0 +1,249 @@
1
+ import { createSlice } from "@reduxjs/toolkit";
2
+ import axios from "../../lib/axios";
3
+ import { notifications } from "@mantine/notifications";
4
+
5
+ export interface ChatUser {
6
+ chatId: string;
7
+ userId: string;
8
+ username: string;
9
+ displayName?: string;
10
+ avatar?: string;
11
+ message?: string;
12
+ unreadCount?: number;
13
+ }
14
+
15
+ export interface MessagedUserRows {
16
+ success: boolean;
17
+ status: boolean;
18
+ message: string;
19
+ data: ChatUser[];
20
+ }
21
+
22
+ export interface MessagedUserList {
23
+ hasMore: boolean;
24
+ rows: MessagedUserRows;
25
+ }
26
+
27
+ export interface MessagesState {
28
+ messagedUserList: MessagedUserList | null;
29
+ messageList: any;
30
+ }
31
+
32
+ const initialState: MessagesState = {
33
+ messagedUserList: null,
34
+ messageList: [],
35
+ };
36
+
37
+ export const MessagesSlice = createSlice({
38
+ name: "messages",
39
+ initialState,
40
+ reducers: {
41
+ setMessagedUserList: (state, action) => {
42
+ const newData = action.payload;
43
+
44
+ if (!state.messagedUserList || newData.page === 0) {
45
+ state.messagedUserList = newData;
46
+ return;
47
+ }
48
+
49
+ const existingList = state.messagedUserList.rows.data;
50
+
51
+ const merged = [...existingList, ...newData.rows.data];
52
+
53
+ const uniqueMap = new Map();
54
+
55
+ merged.forEach((item: any) => {
56
+ uniqueMap.set(item.chatId, item);
57
+ });
58
+
59
+ const uniqueList = Array.from(uniqueMap.values());
60
+
61
+ state.messagedUserList = {
62
+ ...newData,
63
+ rows: {
64
+ ...newData.rows,
65
+ data: uniqueList,
66
+ },
67
+ };
68
+ },
69
+ setMessageList: (state, action) => {
70
+ const newData = action.payload;
71
+
72
+ if (!state.messageList?.rows?.data || newData.page === 0) {
73
+ state.messageList = newData;
74
+ return;
75
+ }
76
+
77
+ // Merge for next pages
78
+ state.messageList = {
79
+ ...newData,
80
+ rows: {
81
+ ...newData.rows,
82
+ data: [
83
+ ...newData.rows.data, // older messages (top)
84
+ ...state.messageList.rows.data, // existing messages
85
+ ],
86
+ },
87
+ };
88
+ },
89
+ addNewMessage: (state, action) => {
90
+ if (!state.messageList?.rows?.data) return;
91
+ state.messageList.rows.data.push(action.payload);
92
+ },
93
+ addUserToChatList: (state, action) => {
94
+ const newUser = action.payload;
95
+
96
+ if (!state.messagedUserList) {
97
+ state.messagedUserList = {
98
+ hasMore: false,
99
+ rows: {
100
+ success: true,
101
+ status: true,
102
+ message: "",
103
+ data: [newUser],
104
+ },
105
+ };
106
+ return;
107
+ }
108
+
109
+ let list = state.messagedUserList.rows.data;
110
+
111
+ state.messagedUserList.rows.data = list = list.filter(
112
+ (item: any) => item?.chatId,
113
+ );
114
+
115
+ const index = list.findIndex(
116
+ (item: any) => item.chatId === newUser.chatId,
117
+ );
118
+
119
+ if (index !== -1) {
120
+ // ✅ Remove existing
121
+ const existing = list.splice(index, 1)[0];
122
+
123
+ const updatedUser = {
124
+ ...existing,
125
+ ...newUser,
126
+ };
127
+
128
+ list.unshift(updatedUser);
129
+ } else {
130
+ list.unshift(newUser);
131
+ }
132
+ },
133
+ resetMessageList: (state) => {
134
+ state.messageList = [];
135
+ },
136
+ updateUnreadCount: (state, action) => {
137
+ const { chatId, isActive } = action.payload;
138
+
139
+ const list = state.messagedUserList?.rows?.data;
140
+ if (!list) return;
141
+
142
+ const item = list.find((chat: any) => chat.chatId === chatId);
143
+ if (!item) return;
144
+
145
+ if (isActive) {
146
+ item.unreadCount = 0;
147
+ } else {
148
+ item.unreadCount = (item.unreadCount || 0) + 1;
149
+ }
150
+ },
151
+ resetUnreadCount: (state, action) => {
152
+ const chatId = action.payload;
153
+
154
+ const list = state.messagedUserList?.rows?.data;
155
+ if (!list) return;
156
+
157
+ const item = list.find((chat: any) => chat.chatId === chatId);
158
+ if (!item) return;
159
+
160
+ item.unreadCount = 0;
161
+ },
162
+ },
163
+ });
164
+
165
+ export const fetchMessageListByUserId =
166
+ (payload: { userId: number | string; page?: number }) =>
167
+ async (dispatch: any) => {
168
+ return axios
169
+ .get(
170
+ `/api/messages/userList/${payload?.userId}?page=${payload?.page || 0}`,
171
+ )
172
+ .then((response: any) => {
173
+ console.log("Fetched messages for userId:", response.data);
174
+ dispatch(setMessagedUserList(response.data));
175
+ return response.data;
176
+ })
177
+ .catch((err: any) => {
178
+ const message =
179
+ err?.response?.data?.message || "Failed to fetch messages";
180
+ notifications.show({
181
+ title: "Message Fetch Failed",
182
+ message,
183
+ color: "red",
184
+ });
185
+ return err;
186
+ });
187
+ };
188
+
189
+ export const fetchMessagesByChatId =
190
+ (payload: { chatId: number | string; page?: number; userId?: number | string }) =>
191
+ async (dispatch: any) => {
192
+ return axios
193
+ .get(`/api/messages/${payload?.chatId}?page=${payload?.page || 0}&userId=${payload?.userId}`)
194
+ .then((response: any) => {
195
+ dispatch(setMessageList(response.data));
196
+ return response.data;
197
+ })
198
+ .catch((err: any) => {
199
+ const message =
200
+ err?.response?.data?.message || "Failed to fetch messages";
201
+ notifications.show({
202
+ title: "Message Fetch Failed",
203
+ message,
204
+ color: "red",
205
+ });
206
+ return err;
207
+ });
208
+ };
209
+
210
+ export const resetMessages = () => async (dispatch: any) => {
211
+ dispatch(resetMessageList());
212
+ };
213
+
214
+ export const deleteMessagesByChatId =
215
+ (payload: { chatId: number | string; userId: number | string }) =>
216
+ async (dispatch: any) => {
217
+ return axios
218
+ .delete(`/api/messages`, { data: { chatId: payload?.chatId, userId: payload?.userId } })
219
+ .then((response: any) => {
220
+ dispatch(resetMessageList());
221
+ notifications.show({
222
+ title: "Messages Deleted",
223
+ message: "All messages in this chat have been deleted.",
224
+ color: "green",
225
+ });
226
+ return response.data;
227
+ })
228
+ .catch((err: any) => {
229
+ const message =
230
+ err?.response?.data?.message || "Failed to delete messages";
231
+ notifications.show({
232
+ title: "Message Deletion Failed",
233
+ message,
234
+ color: "red",
235
+ });
236
+ return err;
237
+ });
238
+ };
239
+
240
+ export const {
241
+ setMessagedUserList,
242
+ setMessageList,
243
+ addNewMessage,
244
+ addUserToChatList,
245
+ resetMessageList,
246
+ updateUnreadCount,
247
+ resetUnreadCount,
248
+ } = MessagesSlice.actions;
249
+ export default MessagesSlice.reducer;
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { getSocket } from "../../lib/socket";
5
+
6
+ interface SocketProviderProps {
7
+ children: React.ReactNode;
8
+ userId?: string;
9
+ }
10
+
11
+ export function SocketProvider({ children, userId }: SocketProviderProps) {
12
+ useEffect(() => {
13
+ if (!userId) return;
14
+
15
+ const socket = getSocket(userId);
16
+ if (!socket) return;
17
+
18
+ if (!socket.connected) {
19
+ socket.connect();
20
+ console.log("✅ Socket connected for:", userId);
21
+ }
22
+
23
+ return () => {
24
+ if (socket.connected) {
25
+ socket.disconnect();
26
+ console.log("Socket disconnected for:", userId);
27
+ }
28
+ };
29
+ }, [userId]);
30
+
31
+ return <>{children}</>;
32
+ }
@@ -0,0 +1,11 @@
1
+ import { configureStore } from '@reduxjs/toolkit';
2
+ import chatReducer from './slices/ChatSlice';
3
+
4
+ export const store = configureStore({
5
+ reducer: {
6
+ chat: chatReducer,
7
+ },
8
+ });
9
+
10
+ export type RootState = ReturnType<typeof store.getState>;
11
+ export type AppDispatch = typeof store.dispatch;
package/src/theme.ts ADDED
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import { createTheme, MantineColorsTuple } from "@mantine/core";
4
+
5
+ const customBrand: MantineColorsTuple = [
6
+ "#e6f9ff",
7
+ "#b3ecff",
8
+ "#80dfff",
9
+ "#4dd2ff",
10
+ "#1ac5ff",
11
+ "#00b8e6",
12
+ "#0091b3",
13
+ "#006a80",
14
+ "#00434d",
15
+ "#001c1a",
16
+ ];
17
+
18
+ const emerald: MantineColorsTuple = [
19
+ "#e6f9f2",
20
+ "#c0f0de",
21
+ "#88e4c3",
22
+ "#4dd6a4",
23
+ "#26cb90",
24
+ "#00bf7d",
25
+ "#00a36c",
26
+ "#008d5d",
27
+ "#00764d",
28
+ "#005f3e",
29
+ ];
30
+
31
+ const ruby: MantineColorsTuple = [
32
+ "#ffe6e9",
33
+ "#fbbdc4",
34
+ "#f38b98",
35
+ "#ea576b",
36
+ "#e12c48",
37
+ "#d80a2d",
38
+ "#b30625",
39
+ "#8f041d",
40
+ "#6b0215",
41
+ "#47010e",
42
+ ];
43
+
44
+ const sunset: MantineColorsTuple = [
45
+ "#fff1e6",
46
+ "#ffd7b3",
47
+ "#ffb380",
48
+ "#ff8f4d",
49
+ "#ff6b1a",
50
+ "#e65c00",
51
+ "#b34700",
52
+ "#803300",
53
+ "#4d1f00",
54
+ "#1a0a00",
55
+ ];
56
+
57
+ const ocean: MantineColorsTuple = [
58
+ "#e6f9ff",
59
+ "#b3ecff",
60
+ "#80dfff",
61
+ "#4dd2ff",
62
+ "#1ac5ff",
63
+ "#00b8e6",
64
+ "#0091b3",
65
+ "#006a80",
66
+ "#00434d",
67
+ "#001c1a",
68
+ ];
69
+
70
+ export const colorThemes = {
71
+ customBrand,
72
+ emerald,
73
+ ruby,
74
+ sunset,
75
+ ocean,
76
+ };
77
+
78
+ export const createAppTheme = (primaryColor: keyof typeof colorThemes) =>
79
+ createTheme({
80
+ primaryColor,
81
+ colors: {
82
+ ...colorThemes,
83
+ },
84
+ });
@@ -0,0 +1,5 @@
1
+ export const environment = {
2
+ BASE_URL: 'http://localhost:4000',
3
+ SOCKET_URL: "http://localhost:4000",
4
+ AUTHORIZATION: 'hgshygsydgsydgsydgsyd'
5
+ };
@@ -0,0 +1,36 @@
1
+ export const getChatDisplayTime = (
2
+ timestamp?: string | number | Date
3
+ ) => {
4
+ if (!timestamp) return "";
5
+
6
+ const msgDate = new Date(timestamp);
7
+
8
+ const today = new Date();
9
+ const yesterday = new Date();
10
+ yesterday.setDate(today.getDate() - 1);
11
+
12
+ const isToday =
13
+ msgDate.toDateString() === today.toDateString();
14
+
15
+ const isYesterday =
16
+ msgDate.toDateString() === yesterday.toDateString();
17
+
18
+ const time = msgDate.toLocaleTimeString("en-GB", {
19
+ hour: "2-digit",
20
+ minute: "2-digit",
21
+ hour12: false,
22
+ });
23
+
24
+ if (isToday) {
25
+ return time;
26
+ }
27
+
28
+ if (isYesterday) {
29
+ return "Yesterday";
30
+ }
31
+
32
+ return msgDate.toLocaleDateString(undefined, {
33
+ day: "numeric",
34
+ month: "short",
35
+ });
36
+ };