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.
- package/LICENCE +21 -0
- package/README.md +26 -0
- package/app/layout.tsx +69 -0
- package/app/page.tsx +9 -0
- package/dist/AddReaction-DCDVOMZB.svg +1 -0
- package/dist/TextFormat-R4ZVDKE2.svg +1 -0
- package/dist/index.css +388 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6425 -0
- package/dist/index.mjs +6420 -0
- package/eslint.config.mjs +11 -0
- package/index.ts +1 -0
- package/next-env.d.ts +5 -0
- package/next.config.mjs +30 -0
- package/package.json +69 -0
- package/postcss.config.cjs +14 -0
- package/public/favicon.svg +1 -0
- package/src/components/ChatUserList/ChatUserList.module.css +253 -0
- package/src/components/ChatUserList/ChatUserList.tsx +434 -0
- package/src/components/ChatUserList/ChatUserList.type.tsx +12 -0
- package/src/components/ChatUserList/ChatUserMessage.tsx +362 -0
- package/src/components/ChatUserList/users_list.json +648 -0
- package/src/components/ColorSchemeToggle/ColorSchemeToggle.tsx +15 -0
- package/src/components/EmojiPickerPopover/EmojiPickerPopover.tsx +72 -0
- package/src/components/MrChat/index.tsx +34 -0
- package/src/components/RichTextEditor/DropzoneMenuItem.tsx +33 -0
- package/src/components/RichTextEditor/EmojiNode.tsx +36 -0
- package/src/components/RichTextEditor/RichTextEditor.module.css +95 -0
- package/src/components/RichTextEditor/RichTextEditor.tsx +248 -0
- package/src/components/UserProfile/UserProfileDrawer.module.css +120 -0
- package/src/components/UserProfile/UserProfileDrawer.tsx +115 -0
- package/src/components/VirtualizedList/ChatScrollContainer.tsx +92 -0
- package/src/components/VirtualizedList/index.tsx +31 -0
- package/src/lib/axios.ts +12 -0
- package/src/lib/socket.ts +29 -0
- package/src/store/provider.tsx +8 -0
- package/src/store/slices/ChatSlice.ts +249 -0
- package/src/store/socket/index.tsx +32 -0
- package/src/store/store.ts +11 -0
- package/src/theme.ts +84 -0
- package/src/utils/environment.ts +5 -0
- package/src/utils/helper.ts +36 -0
- package/src/utils/icons/richText/Add.svg +1 -0
- package/src/utils/icons/richText/AddReaction.svg +1 -0
- package/src/utils/icons/richText/Docs.svg +1 -0
- package/src/utils/icons/richText/Image.svg +1 -0
- package/src/utils/icons/richText/TextFormat.svg +1 -0
- 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
|
+
}
|
package/src/lib/axios.ts
ADDED
|
@@ -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,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,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
|
+
};
|