@ermis-network/ermis-chat-react 1.0.7 → 1.0.9
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 +2787 -1858
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +364 -8
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +160 -1
- package/dist/index.d.ts +160 -1
- package/dist/index.mjs +2787 -1890
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/channelRoleUtils.ts +73 -0
- package/src/channelTypeUtils.ts +46 -0
- package/src/components/Avatar.tsx +57 -31
- package/src/components/ChannelActions.tsx +13 -11
- package/src/components/ChannelHeader.tsx +89 -4
- package/src/components/ChannelInfo/ChannelInfo.tsx +23 -17
- package/src/components/ChannelInfo/ChannelInfoTabs.tsx +57 -26
- package/src/components/ChannelInfo/ChannelSettingsPanel.tsx +4 -2
- package/src/components/ChannelInfo/EditChannelModal.tsx +2 -1
- package/src/components/ChannelInfo/MemberListItem.tsx +2 -1
- package/src/components/ChannelList.tsx +59 -14
- package/src/components/CreateChannelModal.tsx +53 -16
- package/src/components/EditPreview.tsx +2 -1
- package/src/components/ForwardMessageModal.tsx +2 -1
- package/src/components/MediaLightbox.tsx +314 -0
- package/src/components/MessageInput.tsx +14 -11
- package/src/components/MessageItem.tsx +2 -1
- package/src/components/MessageRenderers.tsx +168 -46
- package/src/components/PendingOverlay.tsx +11 -1
- package/src/components/PinnedMessages.tsx +2 -1
- package/src/components/ReplyPreview.tsx +2 -1
- package/src/components/SkippedOverlay.tsx +36 -0
- package/src/components/UserPicker.tsx +1 -1
- package/src/components/VirtualMessageList.tsx +91 -7
- package/src/hooks/useBlockedState.ts +3 -2
- package/src/hooks/useChannelCapabilities.ts +10 -12
- package/src/hooks/useChannelListUpdates.ts +6 -4
- package/src/hooks/useChannelMessages.ts +2 -3
- package/src/hooks/useChannelRowUpdates.ts +3 -2
- package/src/hooks/useMessageActions.ts +23 -9
- package/src/hooks/useOnlineStatus.ts +71 -0
- package/src/hooks/useOnlineUsers.ts +115 -0
- package/src/hooks/usePendingState.ts +8 -3
- package/src/index.ts +61 -9
- package/src/messageTypeUtils.ts +64 -0
- package/src/styles/_channel-list.css +59 -0
- package/src/styles/_media-lightbox.css +263 -0
- package/src/styles/_message-bubble.css +99 -8
- package/src/styles/_message-list.css +25 -0
- package/src/styles/index.css +1 -0
- package/src/types.ts +46 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom';
|
|
3
|
+
import { preloadImage } from '../utils';
|
|
4
|
+
import { useChatClient } from '../hooks/useChatClient';
|
|
5
|
+
import type { MediaLightboxProps } from '../types';
|
|
6
|
+
|
|
7
|
+
/** Extract a reasonable filename from a URL or alt text */
|
|
8
|
+
const getFilename = (src: string, alt?: string): string => {
|
|
9
|
+
if (alt) return alt;
|
|
10
|
+
try {
|
|
11
|
+
const pathname = new URL(src).pathname;
|
|
12
|
+
const segments = pathname.split('/');
|
|
13
|
+
return segments[segments.length - 1] || 'download';
|
|
14
|
+
} catch {
|
|
15
|
+
return 'download';
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* MediaLightbox – full-screen overlay for viewing images & videos.
|
|
21
|
+
* Supports prev/next navigation, keyboard controls, and image zoom.
|
|
22
|
+
* Renders via React portal into document.body.
|
|
23
|
+
*/
|
|
24
|
+
export const MediaLightbox: React.FC<MediaLightboxProps> = React.memo(({
|
|
25
|
+
items,
|
|
26
|
+
initialIndex = 0,
|
|
27
|
+
isOpen,
|
|
28
|
+
onClose,
|
|
29
|
+
}) => {
|
|
30
|
+
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
|
31
|
+
const [zoom, setZoom] = useState(1);
|
|
32
|
+
const [pan, setPan] = useState({ x: 0, y: 0 });
|
|
33
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
34
|
+
const dragStart = useRef({ x: 0, y: 0 });
|
|
35
|
+
const panStart = useRef({ x: 0, y: 0 });
|
|
36
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
37
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
38
|
+
|
|
39
|
+
// Reset state when opening or when items change
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (isOpen) {
|
|
42
|
+
setCurrentIndex(initialIndex);
|
|
43
|
+
setZoom(1);
|
|
44
|
+
setPan({ x: 0, y: 0 });
|
|
45
|
+
}
|
|
46
|
+
}, [isOpen, initialIndex]);
|
|
47
|
+
|
|
48
|
+
// Preload adjacent images
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!isOpen) return;
|
|
51
|
+
const preloadIdx = [currentIndex - 1, currentIndex + 1];
|
|
52
|
+
preloadIdx.forEach(idx => {
|
|
53
|
+
if (idx >= 0 && idx < items.length && items[idx].type === 'image') {
|
|
54
|
+
preloadImage(items[idx].src);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}, [isOpen, currentIndex, items]);
|
|
58
|
+
|
|
59
|
+
// Pause video when navigating away or closing
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
return () => {
|
|
62
|
+
if (videoRef.current) {
|
|
63
|
+
videoRef.current.pause();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}, [currentIndex]);
|
|
67
|
+
|
|
68
|
+
// Lock body scroll when open
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (isOpen) {
|
|
71
|
+
const prev = document.body.style.overflow;
|
|
72
|
+
document.body.style.overflow = 'hidden';
|
|
73
|
+
return () => { document.body.style.overflow = prev; };
|
|
74
|
+
}
|
|
75
|
+
}, [isOpen]);
|
|
76
|
+
|
|
77
|
+
const goTo = useCallback((idx: number) => {
|
|
78
|
+
if (videoRef.current) videoRef.current.pause();
|
|
79
|
+
setCurrentIndex(idx);
|
|
80
|
+
setZoom(1);
|
|
81
|
+
setPan({ x: 0, y: 0 });
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const goPrev = useCallback(() => {
|
|
85
|
+
if (currentIndex > 0) goTo(currentIndex - 1);
|
|
86
|
+
}, [currentIndex, goTo]);
|
|
87
|
+
|
|
88
|
+
const goNext = useCallback(() => {
|
|
89
|
+
if (currentIndex < items.length - 1) goTo(currentIndex + 1);
|
|
90
|
+
}, [currentIndex, items.length, goTo]);
|
|
91
|
+
|
|
92
|
+
// Keyboard navigation
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!isOpen) return;
|
|
95
|
+
const handleKey = (e: KeyboardEvent) => {
|
|
96
|
+
switch (e.key) {
|
|
97
|
+
case 'Escape':
|
|
98
|
+
onClose();
|
|
99
|
+
break;
|
|
100
|
+
case 'ArrowLeft':
|
|
101
|
+
goPrev();
|
|
102
|
+
break;
|
|
103
|
+
case 'ArrowRight':
|
|
104
|
+
goNext();
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
document.addEventListener('keydown', handleKey);
|
|
109
|
+
return () => document.removeEventListener('keydown', handleKey);
|
|
110
|
+
}, [isOpen, onClose, goPrev, goNext]);
|
|
111
|
+
|
|
112
|
+
// Double-click zoom toggle (image only)
|
|
113
|
+
const handleDoubleClick = useCallback(() => {
|
|
114
|
+
const current = items[currentIndex];
|
|
115
|
+
if (current?.type !== 'image') return;
|
|
116
|
+
|
|
117
|
+
if (zoom === 1) {
|
|
118
|
+
setZoom(2);
|
|
119
|
+
} else {
|
|
120
|
+
setZoom(1);
|
|
121
|
+
setPan({ x: 0, y: 0 });
|
|
122
|
+
}
|
|
123
|
+
}, [currentIndex, items, zoom]);
|
|
124
|
+
|
|
125
|
+
// Wheel zoom (image only)
|
|
126
|
+
const handleWheel = useCallback((e: React.WheelEvent) => {
|
|
127
|
+
const current = items[currentIndex];
|
|
128
|
+
if (current?.type !== 'image') return;
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
|
|
131
|
+
setZoom(prev => {
|
|
132
|
+
const next = prev - e.deltaY * 0.002;
|
|
133
|
+
const clamped = Math.max(1, Math.min(3, next));
|
|
134
|
+
if (clamped === 1) setPan({ x: 0, y: 0 });
|
|
135
|
+
return clamped;
|
|
136
|
+
});
|
|
137
|
+
}, [currentIndex, items]);
|
|
138
|
+
|
|
139
|
+
// Mouse drag for panning (image zoomed)
|
|
140
|
+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
141
|
+
if (zoom <= 1) return;
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
setIsDragging(true);
|
|
144
|
+
dragStart.current = { x: e.clientX, y: e.clientY };
|
|
145
|
+
panStart.current = { ...pan };
|
|
146
|
+
}, [zoom, pan]);
|
|
147
|
+
|
|
148
|
+
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
|
149
|
+
if (!isDragging) return;
|
|
150
|
+
const dx = e.clientX - dragStart.current.x;
|
|
151
|
+
const dy = e.clientY - dragStart.current.y;
|
|
152
|
+
setPan({ x: panStart.current.x + dx, y: panStart.current.y + dy });
|
|
153
|
+
}, [isDragging]);
|
|
154
|
+
|
|
155
|
+
const handleMouseUp = useCallback(() => {
|
|
156
|
+
setIsDragging(false);
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
// Click on backdrop closes
|
|
160
|
+
const handleBackdropClick = useCallback((e: React.MouseEvent) => {
|
|
161
|
+
if (e.target === containerRef.current) {
|
|
162
|
+
onClose();
|
|
163
|
+
}
|
|
164
|
+
}, [onClose]);
|
|
165
|
+
|
|
166
|
+
const { client } = useChatClient();
|
|
167
|
+
|
|
168
|
+
const currentItem = items[currentIndex];
|
|
169
|
+
const hasMultiple = items.length > 1;
|
|
170
|
+
|
|
171
|
+
const handleDownload = useCallback(async () => {
|
|
172
|
+
if (!currentItem) return;
|
|
173
|
+
const filename = getFilename(currentItem.src, currentItem.alt);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const blob = await client.downloadMedia(currentItem.src);
|
|
177
|
+
const urlBlob = window.URL.createObjectURL(blob);
|
|
178
|
+
const a = document.createElement('a');
|
|
179
|
+
a.href = urlBlob;
|
|
180
|
+
a.download = filename;
|
|
181
|
+
document.body.appendChild(a);
|
|
182
|
+
a.click();
|
|
183
|
+
a.remove();
|
|
184
|
+
window.URL.revokeObjectURL(urlBlob);
|
|
185
|
+
} catch {
|
|
186
|
+
window.open(currentItem.src, '_blank', 'noopener,noreferrer');
|
|
187
|
+
}
|
|
188
|
+
}, [client, currentItem]);
|
|
189
|
+
|
|
190
|
+
const content = useMemo(() => {
|
|
191
|
+
if (!currentItem) return null;
|
|
192
|
+
|
|
193
|
+
if (currentItem.type === 'video') {
|
|
194
|
+
return (
|
|
195
|
+
<video
|
|
196
|
+
ref={videoRef}
|
|
197
|
+
className="ermis-lightbox__video"
|
|
198
|
+
src={currentItem.src}
|
|
199
|
+
poster={currentItem.posterSrc}
|
|
200
|
+
controls
|
|
201
|
+
autoPlay
|
|
202
|
+
preload="metadata"
|
|
203
|
+
onClick={(e) => e.stopPropagation()}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const imgStyle: React.CSSProperties = {
|
|
209
|
+
transform: `scale(${zoom}) translate(${pan.x / zoom}px, ${pan.y / zoom}px)`,
|
|
210
|
+
cursor: zoom > 1 ? (isDragging ? 'grabbing' : 'grab') : 'default',
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<img
|
|
215
|
+
className={`ermis-lightbox__image${zoom > 1 ? ' ermis-lightbox__image--zoomed' : ''}`}
|
|
216
|
+
src={currentItem.src}
|
|
217
|
+
alt={currentItem.alt || ''}
|
|
218
|
+
style={imgStyle}
|
|
219
|
+
draggable={false}
|
|
220
|
+
onDoubleClick={handleDoubleClick}
|
|
221
|
+
onMouseDown={handleMouseDown}
|
|
222
|
+
onMouseMove={handleMouseMove}
|
|
223
|
+
onMouseUp={handleMouseUp}
|
|
224
|
+
onMouseLeave={handleMouseUp}
|
|
225
|
+
onClick={(e) => e.stopPropagation()}
|
|
226
|
+
/>
|
|
227
|
+
);
|
|
228
|
+
}, [currentItem, zoom, pan, isDragging, handleDoubleClick, handleMouseDown, handleMouseMove, handleMouseUp]);
|
|
229
|
+
|
|
230
|
+
if (!isOpen || !currentItem) return null;
|
|
231
|
+
|
|
232
|
+
return ReactDOM.createPortal(
|
|
233
|
+
<div className="ermis-lightbox" onWheel={handleWheel}>
|
|
234
|
+
<div className="ermis-lightbox__backdrop" />
|
|
235
|
+
|
|
236
|
+
{/* Header: counter + actions */}
|
|
237
|
+
<div className="ermis-lightbox__header">
|
|
238
|
+
{hasMultiple && (
|
|
239
|
+
<span className="ermis-lightbox__counter">
|
|
240
|
+
{currentIndex + 1} / {items.length}
|
|
241
|
+
</span>
|
|
242
|
+
)}
|
|
243
|
+
<div className="ermis-lightbox__actions">
|
|
244
|
+
<button
|
|
245
|
+
className="ermis-lightbox__action-btn"
|
|
246
|
+
onClick={handleDownload}
|
|
247
|
+
aria-label="Download"
|
|
248
|
+
title="Download"
|
|
249
|
+
>
|
|
250
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
251
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
252
|
+
<polyline points="7 10 12 15 17 10" />
|
|
253
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
254
|
+
</svg>
|
|
255
|
+
</button>
|
|
256
|
+
<button
|
|
257
|
+
className="ermis-lightbox__action-btn"
|
|
258
|
+
onClick={onClose}
|
|
259
|
+
aria-label="Close"
|
|
260
|
+
title="Close"
|
|
261
|
+
>
|
|
262
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
263
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
264
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
265
|
+
</svg>
|
|
266
|
+
</button>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
{/* Main content area */}
|
|
271
|
+
<div
|
|
272
|
+
ref={containerRef}
|
|
273
|
+
className="ermis-lightbox__content"
|
|
274
|
+
onClick={handleBackdropClick}
|
|
275
|
+
>
|
|
276
|
+
{/* Prev button */}
|
|
277
|
+
{hasMultiple && currentIndex > 0 && (
|
|
278
|
+
<button
|
|
279
|
+
className="ermis-lightbox__nav ermis-lightbox__nav--prev"
|
|
280
|
+
onClick={(e) => { e.stopPropagation(); goPrev(); }}
|
|
281
|
+
aria-label="Previous"
|
|
282
|
+
>
|
|
283
|
+
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
284
|
+
<polyline points="15 18 9 12 15 6" />
|
|
285
|
+
</svg>
|
|
286
|
+
</button>
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
{/* Media */}
|
|
290
|
+
{content}
|
|
291
|
+
|
|
292
|
+
{/* Next button */}
|
|
293
|
+
{hasMultiple && currentIndex < items.length - 1 && (
|
|
294
|
+
<button
|
|
295
|
+
className="ermis-lightbox__nav ermis-lightbox__nav--next"
|
|
296
|
+
onClick={(e) => { e.stopPropagation(); goNext(); }}
|
|
297
|
+
aria-label="Next"
|
|
298
|
+
>
|
|
299
|
+
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
300
|
+
<polyline points="9 18 15 12 9 6" />
|
|
301
|
+
</svg>
|
|
302
|
+
</button>
|
|
303
|
+
)}
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
{/* Filename */}
|
|
307
|
+
{currentItem.alt && (
|
|
308
|
+
<div className="ermis-lightbox__filename">{currentItem.alt}</div>
|
|
309
|
+
)}
|
|
310
|
+
</div>,
|
|
311
|
+
document.body
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
MediaLightbox.displayName = 'MediaLightbox';
|
|
@@ -15,6 +15,8 @@ import { EditPreview } from './EditPreview';
|
|
|
15
15
|
import { buildUserMap, replaceMentionsForPreview, moveCaretToEnd } from '../utils';
|
|
16
16
|
import { getMentionHtml } from '../hooks/useMentions';
|
|
17
17
|
import { useChannelCapabilities } from '../hooks/useChannelCapabilities';
|
|
18
|
+
import { CHANNEL_ROLES } from '../channelRoleUtils';
|
|
19
|
+
import { isTopicChannel } from '../channelTypeUtils';
|
|
18
20
|
import type { MentionMember, MessageInputProps, FilePreviewItem } from '../types';
|
|
19
21
|
|
|
20
22
|
export type { MessageInputProps, SendButtonProps, AttachButtonProps, EmojiPickerProps, EmojiButtonProps } from '../types';
|
|
@@ -52,7 +54,8 @@ export const MessageInput: React.FC<MessageInputProps> = React.memo(({
|
|
|
52
54
|
const editableRef = React.useRef<HTMLDivElement>(null);
|
|
53
55
|
const [hasContent, setHasContent] = useState(false);
|
|
54
56
|
|
|
55
|
-
const { role,
|
|
57
|
+
const { role, isGroupChannel: isTeamChannel, hasCapability } = useChannelCapabilities();
|
|
58
|
+
const isTopic = isTopicChannel(activeChannel);
|
|
56
59
|
const isClosedTopic = activeChannel?.data?.is_closed_topic === true;
|
|
57
60
|
|
|
58
61
|
// Slow Mode Logic
|
|
@@ -71,7 +74,7 @@ export const MessageInput: React.FC<MessageInputProps> = React.memo(({
|
|
|
71
74
|
};
|
|
72
75
|
}, [activeChannel]);
|
|
73
76
|
|
|
74
|
-
const isSlowModeApplied = isTeamChannel && role ===
|
|
77
|
+
const isSlowModeApplied = isTeamChannel && role === CHANNEL_ROLES.MEMBER && memberMessageCooldown > 0;
|
|
75
78
|
|
|
76
79
|
const [cooldownEnd, setCooldownEnd] = useState<number | null>(null);
|
|
77
80
|
const [cooldown, setCooldown] = useState(0);
|
|
@@ -241,9 +244,9 @@ export const MessageInput: React.FC<MessageInputProps> = React.memo(({
|
|
|
241
244
|
toggleEmojiPicker,
|
|
242
245
|
} = useEmojiPicker({ editableRef, setHasContent });
|
|
243
246
|
|
|
244
|
-
// Build member list from channel state (only for team channels)
|
|
247
|
+
// Build member list from channel state (only for team channels and topics)
|
|
245
248
|
const members = useMemo<MentionMember[]>(() => {
|
|
246
|
-
if (!isTeamChannel) return [];
|
|
249
|
+
if (!(isTeamChannel || isTopic)) return [];
|
|
247
250
|
const list: MentionMember[] = [];
|
|
248
251
|
const stateMembers = activeChannel?.state?.members as Record<string, unknown> | undefined;
|
|
249
252
|
if (stateMembers && typeof stateMembers === 'object') {
|
|
@@ -257,7 +260,7 @@ export const MessageInput: React.FC<MessageInputProps> = React.memo(({
|
|
|
257
260
|
}
|
|
258
261
|
}
|
|
259
262
|
return list;
|
|
260
|
-
}, [activeChannel, isTeamChannel]);
|
|
263
|
+
}, [activeChannel, isTeamChannel, isTopic]);
|
|
261
264
|
|
|
262
265
|
const {
|
|
263
266
|
showSuggestions, filteredMembers, highlightIndex,
|
|
@@ -288,7 +291,7 @@ export const MessageInput: React.FC<MessageInputProps> = React.memo(({
|
|
|
288
291
|
setFiles,
|
|
289
292
|
hasContent,
|
|
290
293
|
setHasContent,
|
|
291
|
-
isTeamChannel,
|
|
294
|
+
isTeamChannel: isTeamChannel || isTopic,
|
|
292
295
|
buildPayload,
|
|
293
296
|
reset,
|
|
294
297
|
syncMessages,
|
|
@@ -323,12 +326,12 @@ export const MessageInput: React.FC<MessageInputProps> = React.memo(({
|
|
|
323
326
|
const content = el?.textContent?.trim() ?? '';
|
|
324
327
|
setHasContent(content.length > 0 || files.length > 0);
|
|
325
328
|
setKeywordError(null); // clear keyword error if user modifies input
|
|
326
|
-
if (isTeamChannel && !disableMentions) {
|
|
329
|
+
if ((isTeamChannel || isTopic) && !disableMentions) {
|
|
327
330
|
mentionHandleInput();
|
|
328
331
|
}
|
|
329
332
|
// Send typing indicator (SDK throttles to 1 event per 2s)
|
|
330
333
|
activeChannel?.keystroke();
|
|
331
|
-
}, [isTeamChannel, disableMentions, mentionHandleInput, files.length, activeChannel]);
|
|
334
|
+
}, [isTeamChannel, isTopic, disableMentions, mentionHandleInput, files.length, activeChannel]);
|
|
332
335
|
|
|
333
336
|
const handleKeyDown = useCallback(
|
|
334
337
|
(e: React.KeyboardEvent) => {
|
|
@@ -345,7 +348,7 @@ export const MessageInput: React.FC<MessageInputProps> = React.memo(({
|
|
|
345
348
|
return;
|
|
346
349
|
}
|
|
347
350
|
}
|
|
348
|
-
if (isTeamChannel && !disableMentions) {
|
|
351
|
+
if ((isTeamChannel || isTopic) && !disableMentions) {
|
|
349
352
|
const consumed = mentionHandleKeyDown(e);
|
|
350
353
|
if (consumed) return;
|
|
351
354
|
}
|
|
@@ -356,7 +359,7 @@ export const MessageInput: React.FC<MessageInputProps> = React.memo(({
|
|
|
356
359
|
}
|
|
357
360
|
}
|
|
358
361
|
},
|
|
359
|
-
[isTeamChannel, disableMentions, mentionHandleKeyDown, handleSend, editingMessage, quotedMessage, setEditingMessage, setQuotedMessage, reset],
|
|
362
|
+
[isTeamChannel, isTopic, disableMentions, mentionHandleKeyDown, handleSend, editingMessage, quotedMessage, setEditingMessage, setQuotedMessage, reset],
|
|
360
363
|
);
|
|
361
364
|
|
|
362
365
|
const handlePaste = useCallback((e: React.ClipboardEvent) => {
|
|
@@ -468,7 +471,7 @@ export const MessageInput: React.FC<MessageInputProps> = React.memo(({
|
|
|
468
471
|
{/* Text input + send row */}
|
|
469
472
|
<div className={`ermis-message-input__row${(!canSendMessage || isSlowModeBlocked || keywordError) ? ' ermis-message-input__row--banners-active' : ''}`}>
|
|
470
473
|
<div className="ermis-message-input__editable-wrapper">
|
|
471
|
-
{canSendMessage && isTeamChannel && !disableMentions && showSuggestions && (
|
|
474
|
+
{canSendMessage && (isTeamChannel || isTopic) && !disableMentions && showSuggestions && (
|
|
472
475
|
<MentionSuggestionsComponent
|
|
473
476
|
members={filteredMembers}
|
|
474
477
|
highlightIndex={highlightIndex}
|
|
@@ -7,6 +7,7 @@ import { MessageQuickReactions } from './MessageQuickReactions';
|
|
|
7
7
|
import { useChannelCapabilities } from '../hooks/useChannelCapabilities';
|
|
8
8
|
import { useChatClient } from '../hooks/useChatClient';
|
|
9
9
|
import { formatTime } from '../utils';
|
|
10
|
+
import { isSystemMessage } from '../messageTypeUtils';
|
|
10
11
|
|
|
11
12
|
export type { MessageItemProps, SystemMessageItemProps } from '../types';
|
|
12
13
|
|
|
@@ -176,7 +177,7 @@ export const MessageItem: React.FC<MessageItemProps> = React.memo(({
|
|
|
176
177
|
</MessageBubble>
|
|
177
178
|
|
|
178
179
|
{/* Actions: hover buttons + dropdown menu */}
|
|
179
|
-
{message
|
|
180
|
+
{!isSystemMessage(message) && (
|
|
180
181
|
<MessageActionsBoxComponent
|
|
181
182
|
message={message}
|
|
182
183
|
isOwnMessage={isOwnMessage}
|