@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
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
2
|
+
import type { Channel } from '@ermis-network/ermis-chat-sdk';
|
|
3
|
+
import { useChatClient } from '../../hooks/useChatClient';
|
|
4
|
+
import { replaceMentionsForPreview, buildUserMap, formatRelativeDate } from '../../utils';
|
|
5
|
+
import { Avatar } from '../Avatar';
|
|
6
|
+
import { Panel } from '../Panel';
|
|
7
|
+
import type { AvatarProps, SearchResultMessage, MessageSearchPanelProps } from '../../types';
|
|
8
|
+
|
|
9
|
+
/* ----------------------------------------------------------
|
|
10
|
+
Highlight utility (Accent-insensitive)
|
|
11
|
+
---------------------------------------------------------- */
|
|
12
|
+
const removeAccents = (str: string) => str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
13
|
+
|
|
14
|
+
const HighlightedText: React.FC<{ text: string; term: string }> = React.memo(({ text, term }) => {
|
|
15
|
+
if (!term.trim()) return <>{text}</>;
|
|
16
|
+
|
|
17
|
+
const cleanTerm = removeAccents(term).toLowerCase();
|
|
18
|
+
if (!cleanTerm) return <>{text}</>;
|
|
19
|
+
|
|
20
|
+
const parts = [];
|
|
21
|
+
let currentIndex = 0;
|
|
22
|
+
const cleanText = removeAccents(text).toLowerCase();
|
|
23
|
+
|
|
24
|
+
while (true) {
|
|
25
|
+
const startMatch = cleanText.indexOf(cleanTerm, currentIndex);
|
|
26
|
+
if (startMatch === -1) {
|
|
27
|
+
if (currentIndex < text.length) {
|
|
28
|
+
parts.push(<span key={currentIndex}>{text.slice(currentIndex)}</span>);
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (startMatch > currentIndex) {
|
|
34
|
+
parts.push(<span key={`text-${currentIndex}`}>{text.slice(currentIndex, startMatch)}</span>);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const endMatch = startMatch + cleanTerm.length;
|
|
38
|
+
parts.push(
|
|
39
|
+
<mark key={`mark-${startMatch}`} className="ermis-search-panel__highlight">
|
|
40
|
+
{text.slice(startMatch, endMatch)}
|
|
41
|
+
</mark>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
currentIndex = endMatch;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return <>{parts.length > 0 ? parts : text}</>;
|
|
48
|
+
});
|
|
49
|
+
HighlightedText.displayName = 'HighlightedText';
|
|
50
|
+
|
|
51
|
+
/* ----------------------------------------------------------
|
|
52
|
+
MessageSearchPanel
|
|
53
|
+
---------------------------------------------------------- */
|
|
54
|
+
export const MessageSearchPanel: React.FC<MessageSearchPanelProps> = React.memo(({
|
|
55
|
+
isOpen,
|
|
56
|
+
onClose,
|
|
57
|
+
channel,
|
|
58
|
+
AvatarComponent = Avatar,
|
|
59
|
+
placeholder = 'Search messages...',
|
|
60
|
+
title = 'Search Messages',
|
|
61
|
+
emptyText = 'No messages found',
|
|
62
|
+
loadingText = 'Searching...',
|
|
63
|
+
debounceMs = 500,
|
|
64
|
+
}) => {
|
|
65
|
+
const { setJumpToMessageId } = useChatClient();
|
|
66
|
+
|
|
67
|
+
const [query, setQuery] = useState('');
|
|
68
|
+
const [results, setResults] = useState<SearchResultMessage[]>([]);
|
|
69
|
+
const [loading, setLoading] = useState(false);
|
|
70
|
+
const [hasMore, setHasMore] = useState(false);
|
|
71
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
72
|
+
|
|
73
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
74
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
75
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
76
|
+
const offsetRef = useRef(0);
|
|
77
|
+
const queryRef = useRef('');
|
|
78
|
+
|
|
79
|
+
// Reset all state when the channel changes (or panel closes)
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
setQuery('');
|
|
82
|
+
setResults([]);
|
|
83
|
+
setLoading(false);
|
|
84
|
+
setHasMore(false);
|
|
85
|
+
setLoadingMore(false);
|
|
86
|
+
offsetRef.current = 0;
|
|
87
|
+
queryRef.current = '';
|
|
88
|
+
}, [channel?.cid, isOpen]);
|
|
89
|
+
|
|
90
|
+
// Auto-focus the input when panel opens
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (isOpen) {
|
|
93
|
+
setTimeout(() => inputRef.current?.focus(), 300);
|
|
94
|
+
}
|
|
95
|
+
}, [isOpen]);
|
|
96
|
+
|
|
97
|
+
// Debounced search
|
|
98
|
+
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
99
|
+
const value = e.target.value;
|
|
100
|
+
setQuery(value);
|
|
101
|
+
|
|
102
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
103
|
+
|
|
104
|
+
if (!value.trim()) {
|
|
105
|
+
setResults([]);
|
|
106
|
+
setLoading(false);
|
|
107
|
+
setHasMore(false);
|
|
108
|
+
offsetRef.current = 0;
|
|
109
|
+
queryRef.current = '';
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
setLoading(true);
|
|
114
|
+
|
|
115
|
+
debounceRef.current = setTimeout(async () => {
|
|
116
|
+
queryRef.current = value;
|
|
117
|
+
offsetRef.current = 0;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const response = await channel.searchMessage(value, 0);
|
|
121
|
+
// Only apply if this is still the latest query
|
|
122
|
+
if (queryRef.current !== value) return;
|
|
123
|
+
|
|
124
|
+
if (!response) {
|
|
125
|
+
setResults([]);
|
|
126
|
+
setHasMore(false);
|
|
127
|
+
} else {
|
|
128
|
+
setResults(response.messages || []);
|
|
129
|
+
setHasMore((response.messages?.length || 0) >= 25);
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error('Search failed:', err);
|
|
133
|
+
setResults([]);
|
|
134
|
+
setHasMore(false);
|
|
135
|
+
} finally {
|
|
136
|
+
setLoading(false);
|
|
137
|
+
}
|
|
138
|
+
}, debounceMs);
|
|
139
|
+
}, [channel, debounceMs]);
|
|
140
|
+
|
|
141
|
+
// Infinite scroll: load more results
|
|
142
|
+
const handleLoadMore = useCallback(async () => {
|
|
143
|
+
if (loadingMore || !hasMore || !queryRef.current) return;
|
|
144
|
+
|
|
145
|
+
setLoadingMore(true);
|
|
146
|
+
const nextOffset = offsetRef.current + 25; // offset skips records, limit is 25
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const response = await channel.searchMessage(queryRef.current, nextOffset);
|
|
150
|
+
|
|
151
|
+
if (!response || !response.messages?.length) {
|
|
152
|
+
setHasMore(false);
|
|
153
|
+
} else {
|
|
154
|
+
offsetRef.current = nextOffset;
|
|
155
|
+
setResults((prev) => [...prev, ...response.messages]);
|
|
156
|
+
setHasMore(response.messages.length >= 25);
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error('Load more search results failed:', err);
|
|
160
|
+
} finally {
|
|
161
|
+
setLoadingMore(false);
|
|
162
|
+
}
|
|
163
|
+
}, [channel, hasMore, loadingMore]);
|
|
164
|
+
|
|
165
|
+
// Scroll handler for infinite scroll
|
|
166
|
+
const handleScroll = useCallback(() => {
|
|
167
|
+
const el = scrollRef.current;
|
|
168
|
+
if (!el) return;
|
|
169
|
+
|
|
170
|
+
const threshold = 100;
|
|
171
|
+
if (el.scrollTop + el.clientHeight >= el.scrollHeight - threshold) {
|
|
172
|
+
handleLoadMore();
|
|
173
|
+
}
|
|
174
|
+
}, [handleLoadMore]);
|
|
175
|
+
|
|
176
|
+
// Click a result -> jump to that message
|
|
177
|
+
const handleResultClick = useCallback((messageId: string) => {
|
|
178
|
+
setJumpToMessageId(messageId);
|
|
179
|
+
}, [setJumpToMessageId]);
|
|
180
|
+
|
|
181
|
+
// Derived userMap for resolving mentions, with a lowercase variant for fast lookup
|
|
182
|
+
const userMaps = useMemo(() => {
|
|
183
|
+
const original = buildUserMap(channel.state);
|
|
184
|
+
const lower: typeof original = {};
|
|
185
|
+
for (const [id, name] of Object.entries(original)) {
|
|
186
|
+
lower[id.toLowerCase()] = name;
|
|
187
|
+
}
|
|
188
|
+
return { original, lower };
|
|
189
|
+
}, [channel.state]);
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<Panel isOpen={isOpen} onClose={onClose} title={title} className="ermis-search-panel">
|
|
193
|
+
{/* Search Input now inside body */}
|
|
194
|
+
<div className="ermis-search-panel__search-box">
|
|
195
|
+
<div className="ermis-search-panel__input-wrap">
|
|
196
|
+
<svg className="ermis-search-panel__input-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
197
|
+
<circle cx="11" cy="11" r="8" />
|
|
198
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
199
|
+
</svg>
|
|
200
|
+
<input
|
|
201
|
+
ref={inputRef}
|
|
202
|
+
className="ermis-search-panel__input"
|
|
203
|
+
type="text"
|
|
204
|
+
value={query}
|
|
205
|
+
onChange={handleInputChange}
|
|
206
|
+
placeholder={placeholder}
|
|
207
|
+
/>
|
|
208
|
+
{query && (
|
|
209
|
+
<button
|
|
210
|
+
className="ermis-search-panel__input-clear"
|
|
211
|
+
onClick={() => {
|
|
212
|
+
setQuery('');
|
|
213
|
+
setResults([]);
|
|
214
|
+
setHasMore(false);
|
|
215
|
+
offsetRef.current = 0;
|
|
216
|
+
queryRef.current = '';
|
|
217
|
+
inputRef.current?.focus();
|
|
218
|
+
}}
|
|
219
|
+
aria-label="Clear"
|
|
220
|
+
>
|
|
221
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
222
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
223
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
224
|
+
</svg>
|
|
225
|
+
</button>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div
|
|
231
|
+
ref={scrollRef}
|
|
232
|
+
className="ermis-search-panel__results"
|
|
233
|
+
onScroll={handleScroll}
|
|
234
|
+
>
|
|
235
|
+
{/* Initial state — no query yet */}
|
|
236
|
+
{!query.trim() && !loading && results.length === 0 && (
|
|
237
|
+
<div className="ermis-search-panel__idle">
|
|
238
|
+
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
239
|
+
<circle cx="11" cy="11" r="8" />
|
|
240
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
241
|
+
</svg>
|
|
242
|
+
<span>{placeholder}</span>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{/* Loading state */}
|
|
247
|
+
{loading && (
|
|
248
|
+
<div className="ermis-search-panel__loading">
|
|
249
|
+
<div className="ermis-search-panel__spinner" />
|
|
250
|
+
<span>{loadingText}</span>
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{/* Empty state */}
|
|
255
|
+
{!loading && query.trim() && results.length === 0 && (
|
|
256
|
+
<div className="ermis-search-panel__empty">
|
|
257
|
+
<span>{emptyText}</span>
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{/* Results */}
|
|
262
|
+
{!loading && results.map((msg) => {
|
|
263
|
+
let parsedText = '';
|
|
264
|
+
if (msg.text) {
|
|
265
|
+
// Try standard replacement first
|
|
266
|
+
parsedText = replaceMentionsForPreview(msg.text, msg as any, userMaps.original);
|
|
267
|
+
// Fallback: search API may omit mentioned_users array, so we map @0x IDs efficiently
|
|
268
|
+
if (/@0x[a-fA-F0-9]+/i.test(parsedText)) {
|
|
269
|
+
parsedText = parsedText.replace(/@0x[a-fA-F0-9]+/gi, (match) => {
|
|
270
|
+
const matchedId = match.slice(1).toLowerCase();
|
|
271
|
+
return userMaps.lower[matchedId] ? `@${userMaps.lower[matchedId]}` : match;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<div
|
|
278
|
+
key={msg.id}
|
|
279
|
+
role="button"
|
|
280
|
+
tabIndex={0}
|
|
281
|
+
className="ermis-search-panel__result-item"
|
|
282
|
+
onClick={() => handleResultClick(msg.id)}
|
|
283
|
+
onKeyDown={(e) => {
|
|
284
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
285
|
+
e.preventDefault();
|
|
286
|
+
handleResultClick(msg.id);
|
|
287
|
+
}
|
|
288
|
+
}}
|
|
289
|
+
>
|
|
290
|
+
<AvatarComponent
|
|
291
|
+
image={msg.user?.avatar || msg.user?.image || msg.user?.avatar_url}
|
|
292
|
+
name={msg.user?.name || msg.user_id}
|
|
293
|
+
size={36}
|
|
294
|
+
/>
|
|
295
|
+
<div className="ermis-search-panel__result-body">
|
|
296
|
+
<div className="ermis-search-panel__result-meta">
|
|
297
|
+
<span className="ermis-search-panel__result-name">
|
|
298
|
+
{msg.user?.name || msg.user_id || 'Unknown'}
|
|
299
|
+
</span>
|
|
300
|
+
<span className="ermis-search-panel__result-time">
|
|
301
|
+
{msg.created_at ? formatRelativeDate(msg.created_at) : ''}
|
|
302
|
+
</span>
|
|
303
|
+
</div>
|
|
304
|
+
<p className="ermis-search-panel__result-text">
|
|
305
|
+
{parsedText ? (
|
|
306
|
+
<HighlightedText text={parsedText} term={query} />
|
|
307
|
+
) : (
|
|
308
|
+
<em>Attachment</em>
|
|
309
|
+
)}
|
|
310
|
+
</p>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
})}
|
|
315
|
+
|
|
316
|
+
{/* End of results indicator */}
|
|
317
|
+
{!loading && !loadingMore && !hasMore && results.length > 0 && query.trim() && (
|
|
318
|
+
<div className="ermis-search-panel__end-indicator">
|
|
319
|
+
<span>{emptyText}</span>
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
322
|
+
|
|
323
|
+
{/* Loading more indicator */}
|
|
324
|
+
{loadingMore && (
|
|
325
|
+
<div className="ermis-search-panel__loading-more">
|
|
326
|
+
<div className="ermis-search-panel__spinner ermis-search-panel__spinner--small" />
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
330
|
+
</Panel>
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
MessageSearchPanel.displayName = 'MessageSearchPanel';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export const TabEmptyState: React.FC<{ label: string }> = React.memo(({ label }) => (
|
|
4
|
+
<div className="ermis-channel-info__media-empty">
|
|
5
|
+
<div className="ermis-channel-info__media-empty-icon">
|
|
6
|
+
{label === 'media' && (
|
|
7
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
8
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
9
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
10
|
+
<polyline points="21 15 16 10 5 21" />
|
|
11
|
+
</svg>
|
|
12
|
+
)}
|
|
13
|
+
{label === 'links' && (
|
|
14
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
15
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
|
16
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
|
17
|
+
</svg>
|
|
18
|
+
)}
|
|
19
|
+
{label === 'files' && (
|
|
20
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
21
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
22
|
+
<polyline points="14 2 14 8 20 8" />
|
|
23
|
+
</svg>
|
|
24
|
+
)}
|
|
25
|
+
</div>
|
|
26
|
+
<span>No {label} shared yet</span>
|
|
27
|
+
</div>
|
|
28
|
+
));
|
|
29
|
+
(TabEmptyState as any).displayName = 'TabEmptyState';
|
|
30
|
+
|
|
31
|
+
export const TabLoadingState: React.FC = React.memo(() => (
|
|
32
|
+
<div className="ermis-channel-info__media-loading">
|
|
33
|
+
<div className="ermis-channel-info__media-spinner" />
|
|
34
|
+
</div>
|
|
35
|
+
));
|
|
36
|
+
(TabLoadingState as any).displayName = 'TabLoadingState';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './ChannelInfo';
|
|
2
|
+
export * from './ChannelInfoTabs';
|
|
3
|
+
export * from './EditChannelModal';
|
|
4
|
+
export * from './MessageSearchPanel';
|
|
5
|
+
export * from './MediaGridItem';
|
|
6
|
+
export * from './LinkListItem';
|
|
7
|
+
export * from './FileListItem';
|
|
8
|
+
export * from './MemberListItem';
|
|
9
|
+
export * from './States';
|
|
10
|
+
export * from './ChannelSettingsPanel';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { MediaTab } from '../../types';
|
|
3
|
+
|
|
4
|
+
export function getFileIcon(contentType: string, fileName: string): React.ReactNode {
|
|
5
|
+
const ext = fileName.split('.').pop()?.toLowerCase() || '';
|
|
6
|
+
|
|
7
|
+
if (contentType.includes('pdf') || ext === 'pdf') {
|
|
8
|
+
return (
|
|
9
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
10
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
11
|
+
<polyline points="14 2 14 8 20 8" />
|
|
12
|
+
<line x1="16" y1="13" x2="8" y2="13" />
|
|
13
|
+
<line x1="16" y1="17" x2="8" y2="17" />
|
|
14
|
+
<polyline points="10 9 9 9 8 9" />
|
|
15
|
+
</svg>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (contentType.includes('zip') || contentType.includes('rar') || contentType.includes('archive') || ['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) {
|
|
20
|
+
return (
|
|
21
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
22
|
+
<path d="M21 8v13H3V3h13" />
|
|
23
|
+
<path d="M16 3v5h5" />
|
|
24
|
+
<path d="M10 12h4M10 16h4M10 8h1" />
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Default file icon
|
|
30
|
+
return (
|
|
31
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
32
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
33
|
+
<polyline points="14 2 14 8 20 8" />
|
|
34
|
+
</svg>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const ROLE_WEIGHTS: Record<string, number> = {
|
|
39
|
+
owner: 4,
|
|
40
|
+
moder: 3,
|
|
41
|
+
member: 2,
|
|
42
|
+
pending: 1,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const MESSAGING_TABS: MediaTab[] = ['media', 'links', 'files'];
|
|
46
|
+
export const ALL_TABS: MediaTab[] = ['members', 'media', 'links', 'files'];
|
|
47
|
+
|
|
48
|
+
export const PENDING_STYLE = { opacity: 0.7, transition: 'opacity 0.15s ease' } as const;
|
|
49
|
+
export const READY_STYLE = { opacity: 1, transition: 'opacity 0.15s ease' } as const;
|