@ermis-network/ermis-chat-react 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/dist/index.cjs +6593 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +3375 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.mts +1138 -0
- package/dist/index.d.ts +1138 -0
- package/dist/index.mjs +6500 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +42 -0
- package/src/components/Avatar.tsx +102 -0
- package/src/components/Channel.tsx +77 -0
- package/src/components/ChannelHeader.tsx +85 -0
- package/src/components/ChannelInfo/AddMemberModal.tsx +204 -0
- package/src/components/ChannelInfo/ChannelInfo.tsx +455 -0
- package/src/components/ChannelInfo/ChannelInfoTabs.tsx +282 -0
- package/src/components/ChannelInfo/ChannelSettingsPanel.tsx +479 -0
- package/src/components/ChannelInfo/EditChannelModal.tsx +272 -0
- package/src/components/ChannelInfo/FileListItem.tsx +49 -0
- package/src/components/ChannelInfo/LinkListItem.tsx +62 -0
- package/src/components/ChannelInfo/MediaGridItem.tsx +90 -0
- package/src/components/ChannelInfo/MemberListItem.tsx +85 -0
- package/src/components/ChannelInfo/MessageSearchPanel.tsx +333 -0
- package/src/components/ChannelInfo/States.tsx +36 -0
- package/src/components/ChannelInfo/index.ts +10 -0
- package/src/components/ChannelInfo/utils.tsx +49 -0
- package/src/components/ChannelList.tsx +395 -0
- package/src/components/Dropdown.tsx +120 -0
- package/src/components/EditPreview.tsx +102 -0
- package/src/components/FilesPreview.tsx +108 -0
- package/src/components/ForwardMessageModal.tsx +234 -0
- package/src/components/MentionSuggestions.tsx +59 -0
- package/src/components/MessageActionsBox.tsx +186 -0
- package/src/components/MessageInput.tsx +513 -0
- package/src/components/MessageInputDefaults.tsx +50 -0
- package/src/components/MessageItem.tsx +218 -0
- package/src/components/MessageQuickReactions.tsx +73 -0
- package/src/components/MessageReactions.tsx +59 -0
- package/src/components/MessageRenderers.tsx +565 -0
- package/src/components/Modal.tsx +58 -0
- package/src/components/Panel.tsx +64 -0
- package/src/components/PinnedMessages.tsx +165 -0
- package/src/components/QuotedMessagePreview.tsx +55 -0
- package/src/components/ReadReceipts.tsx +80 -0
- package/src/components/ReplyPreview.tsx +98 -0
- package/src/components/TypingIndicator.tsx +57 -0
- package/src/components/VirtualMessageList.tsx +425 -0
- package/src/context/ChatProvider.tsx +73 -0
- package/src/hooks/useBannedState.ts +48 -0
- package/src/hooks/useBlockedState.ts +55 -0
- package/src/hooks/useChannel.ts +18 -0
- package/src/hooks/useChannelCapabilities.ts +42 -0
- package/src/hooks/useChannelData.ts +55 -0
- package/src/hooks/useChannelListUpdates.ts +224 -0
- package/src/hooks/useChannelMessages.ts +159 -0
- package/src/hooks/useChannelRowUpdates.ts +78 -0
- package/src/hooks/useChatClient.ts +11 -0
- package/src/hooks/useEmojiPicker.ts +53 -0
- package/src/hooks/useFileUpload.ts +128 -0
- package/src/hooks/useLoadMessages.ts +178 -0
- package/src/hooks/useMentions.ts +287 -0
- package/src/hooks/useMessageActions.ts +87 -0
- package/src/hooks/useMessageSend.ts +164 -0
- package/src/hooks/usePendingState.ts +63 -0
- package/src/hooks/useScrollToMessage.ts +155 -0
- package/src/hooks/useTypingIndicator.ts +86 -0
- package/src/index.ts +129 -0
- package/src/styles/_add-member-modal.css +122 -0
- package/src/styles/_base.css +32 -0
- package/src/styles/_channel-info.css +941 -0
- package/src/styles/_channel-list.css +217 -0
- package/src/styles/_dropdown.css +69 -0
- package/src/styles/_forward-modal.css +191 -0
- package/src/styles/_mentions.css +102 -0
- package/src/styles/_message-actions.css +61 -0
- package/src/styles/_message-bubble.css +656 -0
- package/src/styles/_message-input.css +389 -0
- package/src/styles/_message-list.css +416 -0
- package/src/styles/_message-quick-reactions.css +62 -0
- package/src/styles/_message-reactions.css +67 -0
- package/src/styles/_modal.css +113 -0
- package/src/styles/_panel.css +69 -0
- package/src/styles/_pinned-messages.css +140 -0
- package/src/styles/_search-panel.css +219 -0
- package/src/styles/_tokens.css +92 -0
- package/src/styles/_typing-indicator.css +59 -0
- package/src/styles/index.css +24 -0
- package/src/types.ts +955 -0
- package/src/utils.ts +242 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { MentionMember } from './types';
|
|
2
|
+
import type { Attachment, FormatMessageResponse } from '@ermis-network/ermis-chat-sdk';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Format a Date or date-string to a short time string (HH:MM).
|
|
6
|
+
*/
|
|
7
|
+
export function formatTime(date: Date | string | undefined): string {
|
|
8
|
+
if (!date) return '';
|
|
9
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
10
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Format a date as "HH:MM, Today", "HH:MM, Yesterday", or "HH:MM, MM/DD/YYYY".
|
|
15
|
+
*/
|
|
16
|
+
export function formatReadTimestamp(date: Date | string | undefined): string {
|
|
17
|
+
if (!date) return '';
|
|
18
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
21
|
+
const msgDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
22
|
+
const diffMs = today.getTime() - msgDay.getTime();
|
|
23
|
+
const diffDays = Math.round(diffMs / 86400000);
|
|
24
|
+
const time = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
25
|
+
|
|
26
|
+
if (diffDays === 0) return `${time}, Today`;
|
|
27
|
+
if (diffDays === 1) return `${time}, Yesterday`;
|
|
28
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
29
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
30
|
+
const yyyy = d.getFullYear();
|
|
31
|
+
return `${time}, ${mm}/${dd}/${yyyy}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Return a YYYY-M-D key for date comparison (used by date separators).
|
|
36
|
+
*/
|
|
37
|
+
export function getDateKey(date: Date | string | undefined): string {
|
|
38
|
+
if (!date) return '';
|
|
39
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
40
|
+
return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Format a date into a human-friendly label (Today / Yesterday / full date).
|
|
45
|
+
*/
|
|
46
|
+
export function formatDateLabel(date: Date | string | undefined): string {
|
|
47
|
+
if (!date) return '';
|
|
48
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
49
|
+
const now = new Date();
|
|
50
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
51
|
+
const msgDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
52
|
+
const diffMs = today.getTime() - msgDay.getTime();
|
|
53
|
+
const diffDays = Math.round(diffMs / 86400000);
|
|
54
|
+
|
|
55
|
+
if (diffDays === 0) return 'Today';
|
|
56
|
+
if (diffDays === 1) return 'Yesterday';
|
|
57
|
+
return d.toLocaleDateString(undefined, {
|
|
58
|
+
year: 'numeric',
|
|
59
|
+
month: 'long',
|
|
60
|
+
day: 'numeric',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the user id from a message, checking multiple possible sources.
|
|
66
|
+
*/
|
|
67
|
+
export function getMessageUserId(message: FormatMessageResponse): string {
|
|
68
|
+
return message.user?.id || message.user_id || '';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Replace @user_id with @UserName for plain text previews.
|
|
73
|
+
* Returns the formatted string.
|
|
74
|
+
*/
|
|
75
|
+
export function replaceMentionsForPreview(
|
|
76
|
+
text: string,
|
|
77
|
+
message: FormatMessageResponse | { mentioned_users?: string[]; mentioned_all?: boolean },
|
|
78
|
+
userMap: Record<string, string>,
|
|
79
|
+
renderWrapper?: (userId: string, name: string) => string
|
|
80
|
+
): string {
|
|
81
|
+
const mentionedUsers: string[] = (message as any).mentioned_users ?? [];
|
|
82
|
+
const mentionedAll: boolean = (message as any).mentioned_all ?? false;
|
|
83
|
+
|
|
84
|
+
// If no mentions, nothing to replace
|
|
85
|
+
if (mentionedUsers.length === 0 && !mentionedAll) {
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const replacements: { pattern: string; label: string }[] = [];
|
|
90
|
+
|
|
91
|
+
for (const userId of mentionedUsers) {
|
|
92
|
+
if (!userId) continue;
|
|
93
|
+
const name = userMap[userId] ?? userId;
|
|
94
|
+
replacements.push({
|
|
95
|
+
pattern: `@${userId}`,
|
|
96
|
+
label: renderWrapper ? renderWrapper(userId, name) : `@${name}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (mentionedAll) {
|
|
101
|
+
replacements.push({
|
|
102
|
+
pattern: '@all',
|
|
103
|
+
label: renderWrapper ? renderWrapper('__all__', 'all') : '@all'
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (replacements.length === 0) return text;
|
|
108
|
+
|
|
109
|
+
// Escape special regex characters in the patterns
|
|
110
|
+
const escaped = replacements.map((r) => r.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
111
|
+
const regex = new RegExp(`(${escaped.join('|')})`, 'g');
|
|
112
|
+
|
|
113
|
+
// Map pattern back to label for quick lookup
|
|
114
|
+
const patternToLabel = new Map(replacements.map((r) => [r.pattern, r.label]));
|
|
115
|
+
|
|
116
|
+
return text.replace(regex, (match) => patternToLabel.get(match) || match);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Common helper to build a dictionary of User ID -> Display Name
|
|
121
|
+
* from the channel state, used for rendering Mentions and System logs.
|
|
122
|
+
*/
|
|
123
|
+
export function buildUserMap(channelState: any): Record<string, string> {
|
|
124
|
+
const map: Record<string, string> = {};
|
|
125
|
+
const members = channelState?.members;
|
|
126
|
+
if (members && typeof members === 'object') {
|
|
127
|
+
for (const [id, member] of Object.entries<any>(members)) {
|
|
128
|
+
map[id] = member?.user?.name || member?.user_id || id;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return map;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Move caret to the very end of a contenteditable element.
|
|
136
|
+
*/
|
|
137
|
+
export function moveCaretToEnd(el: HTMLElement) {
|
|
138
|
+
const sel = window.getSelection();
|
|
139
|
+
if (!sel) return;
|
|
140
|
+
const range = document.createRange();
|
|
141
|
+
range.selectNodeContents(el);
|
|
142
|
+
range.collapse(false);
|
|
143
|
+
sel.removeAllRanges();
|
|
144
|
+
sel.addRange(range);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Move caret immediately after a specific DOM node.
|
|
149
|
+
*/
|
|
150
|
+
export function moveCaretAfterNode(node: Node) {
|
|
151
|
+
const sel = window.getSelection();
|
|
152
|
+
if (!sel) return;
|
|
153
|
+
const range = document.createRange();
|
|
154
|
+
range.setStartAfter(node);
|
|
155
|
+
range.collapse(true);
|
|
156
|
+
sel.removeAllRanges();
|
|
157
|
+
sel.addRange(range);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Checks if a given attachment represents user-managed media (e.g., photo, video, text file, voice)
|
|
162
|
+
* as opposed to backend-injected automated system cards (like linkPreviews or slash commands).
|
|
163
|
+
*/
|
|
164
|
+
export function isUserManagedAttachment(attachment: Attachment): boolean {
|
|
165
|
+
const type = attachment.type || 'file';
|
|
166
|
+
return ['image', 'video', 'file', 'voiceRecording'].includes(type);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Lightweight in-memory image preloader.
|
|
171
|
+
*/
|
|
172
|
+
const preloadedUrls = new Set<string>();
|
|
173
|
+
const MAX_CACHE_SIZE = 500;
|
|
174
|
+
|
|
175
|
+
export function preloadImage(url: string): void {
|
|
176
|
+
if (!url || preloadedUrls.has(url)) return;
|
|
177
|
+
|
|
178
|
+
if (preloadedUrls.size >= MAX_CACHE_SIZE) {
|
|
179
|
+
const first = preloadedUrls.values().next().value;
|
|
180
|
+
if (first) preloadedUrls.delete(first);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const img = new Image();
|
|
184
|
+
img.src = url;
|
|
185
|
+
preloadedUrls.add(url);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function isImagePreloaded(url: string): boolean {
|
|
189
|
+
return preloadedUrls.has(url);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Format bytes into a human-readable file size (e.g. "1.2 MB").
|
|
194
|
+
*/
|
|
195
|
+
export function formatFileSize(bytes: number): string {
|
|
196
|
+
if (bytes === 0) return '0 B';
|
|
197
|
+
const k = 1024;
|
|
198
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
199
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
200
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Format a date string into a relative label:
|
|
205
|
+
* "Today", "Yesterday", "Xd ago", or "Mon DD" / "Mon DD, YYYY".
|
|
206
|
+
*/
|
|
207
|
+
export function formatRelativeDate(dateStr: string): string {
|
|
208
|
+
const date = new Date(dateStr);
|
|
209
|
+
const now = new Date();
|
|
210
|
+
const diffMs = now.getTime() - date.getTime();
|
|
211
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
212
|
+
|
|
213
|
+
if (diffDays === 0) return 'Today';
|
|
214
|
+
if (diffDays === 1) return 'Yesterday';
|
|
215
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
216
|
+
|
|
217
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get a cleaned display name from a raw file_name.
|
|
222
|
+
* Strips UUID-heavy prefixes that the API sometimes prepends.
|
|
223
|
+
*/
|
|
224
|
+
export function getDisplayName(fileName: string): string {
|
|
225
|
+
const parts = fileName.split('-');
|
|
226
|
+
if (parts.length > 5) {
|
|
227
|
+
const ext = fileName.split('.').pop() || '';
|
|
228
|
+
return `file.${ext}`;
|
|
229
|
+
}
|
|
230
|
+
return fileName;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Extract the hostname from a URL string. Returns the original string on error.
|
|
235
|
+
*/
|
|
236
|
+
export function extractDomain(url: string): string {
|
|
237
|
+
try {
|
|
238
|
+
return new URL(url).hostname;
|
|
239
|
+
} catch {
|
|
240
|
+
return url;
|
|
241
|
+
}
|
|
242
|
+
}
|