@ermis-network/ermis-chat-react 1.0.6 → 1.0.8
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 +3802 -1772
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +836 -25
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +304 -1
- package/dist/index.d.ts +304 -1
- package/dist/index.mjs +3755 -1761
- 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/BannedOverlay.tsx +40 -0
- package/src/components/ChannelActions.tsx +233 -0
- package/src/components/ChannelHeader.tsx +126 -5
- package/src/components/ChannelInfo/ChannelInfo.tsx +128 -24
- package/src/components/ChannelInfo/ChannelInfoTabs.tsx +67 -28
- package/src/components/ChannelInfo/ChannelSettingsPanel.tsx +90 -1
- package/src/components/ChannelInfo/EditChannelModal.tsx +5 -4
- package/src/components/ChannelInfo/MemberListItem.tsx +2 -1
- package/src/components/ChannelList.tsx +514 -47
- package/src/components/ClosedTopicOverlay.tsx +38 -0
- 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 +21 -3
- package/src/components/MessageItem.tsx +10 -12
- package/src/components/MessageQuickReactions.tsx +3 -2
- package/src/components/MessageReactions.tsx +8 -3
- package/src/components/MessageRenderers.tsx +174 -54
- package/src/components/PendingOverlay.tsx +51 -0
- package/src/components/PinnedMessages.tsx +2 -1
- package/src/components/ReplyPreview.tsx +2 -1
- package/src/components/SkippedOverlay.tsx +36 -0
- package/src/components/TopicModal.tsx +189 -0
- package/src/components/UserPicker.tsx +1 -1
- package/src/components/VirtualMessageList.tsx +162 -47
- package/src/hooks/useBannedState.ts +27 -3
- package/src/hooks/useBlockedState.ts +3 -2
- package/src/hooks/useChannelCapabilities.ts +10 -8
- package/src/hooks/useChannelData.ts +1 -1
- package/src/hooks/useChannelListUpdates.ts +28 -5
- package/src/hooks/useChannelMessages.ts +2 -3
- package/src/hooks/useChannelRowUpdates.ts +9 -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 +67 -10
- package/src/messageTypeUtils.ts +64 -0
- package/src/styles/_channel-info.css +21 -0
- package/src/styles/_channel-list.css +276 -6
- package/src/styles/_media-lightbox.css +263 -0
- package/src/styles/_message-bubble.css +170 -13
- package/src/styles/_message-input.css +24 -0
- package/src/styles/_message-list.css +76 -6
- package/src/styles/_message-quick-reactions.css +5 -0
- package/src/styles/_message-reactions.css +7 -0
- package/src/styles/_topic-modal.css +154 -0
- package/src/styles/index.css +2 -0
- package/src/types.ts +203 -3
|
@@ -4,6 +4,7 @@ import type { Channel, Event, ChannelFilters } from '@ermis-network/ermis-chat-s
|
|
|
4
4
|
import { parseSystemMessage, parseSignalMessage } from '@ermis-network/ermis-chat-sdk';
|
|
5
5
|
import { useChatClient } from '../hooks/useChatClient';
|
|
6
6
|
import { useChannelListUpdates } from '../hooks/useChannelListUpdates';
|
|
7
|
+
import { useOnlineUsers } from '../hooks/useOnlineUsers';
|
|
7
8
|
import { replaceMentionsForPreview, buildUserMap } from '../utils';
|
|
8
9
|
import { useChannelRowUpdates } from '../hooks/useChannelRowUpdates';
|
|
9
10
|
import { usePendingState } from '../hooks/usePendingState';
|
|
@@ -11,6 +12,14 @@ import { Avatar } from './Avatar';
|
|
|
11
12
|
import type { ChannelItemProps, ChannelListProps } from '../types';
|
|
12
13
|
|
|
13
14
|
export type { ChannelListProps, ChannelItemProps } from '../types';
|
|
15
|
+
import type { ChannelActionsProps } from '../types';
|
|
16
|
+
import { TopicModal } from './TopicModal';
|
|
17
|
+
import { DefaultChannelActions, computeDefaultActions } from './ChannelActions';
|
|
18
|
+
import { isDirectChannel, hasTopicsEnabled } from '../channelTypeUtils';
|
|
19
|
+
import { canManageChannel, isPendingMember, isSkippedMember, isFriendChannel } from '../channelRoleUtils';
|
|
20
|
+
|
|
21
|
+
export { DefaultChannelActions } from './ChannelActions';
|
|
22
|
+
export type { ChannelAction, ChannelActionsProps } from '../types';
|
|
14
23
|
|
|
15
24
|
/**
|
|
16
25
|
* Get a human-readable preview string for the last message,
|
|
@@ -19,26 +28,28 @@ export type { ChannelListProps, ChannelItemProps } from '../types';
|
|
|
19
28
|
function getLastMessagePreview(
|
|
20
29
|
channel: Channel,
|
|
21
30
|
myUserId?: string,
|
|
22
|
-
): { text: string; user: string } {
|
|
31
|
+
): { text: string; user: string; timestamp?: string | Date } {
|
|
23
32
|
const lastMsg = channel.state?.latestMessages?.slice(-1)[0];
|
|
24
33
|
if (!lastMsg) return { text: '', user: '' };
|
|
25
34
|
|
|
35
|
+
const timestamp = lastMsg.created_at;
|
|
36
|
+
|
|
26
37
|
const msgType = lastMsg.type || 'regular';
|
|
27
38
|
const rawText = lastMsg.text ?? '';
|
|
28
39
|
|
|
29
40
|
if (msgType === 'system') {
|
|
30
41
|
const userMap = buildUserMap(channel.state);
|
|
31
|
-
return { text: parseSystemMessage(rawText, userMap), user: '' };
|
|
42
|
+
return { text: parseSystemMessage(rawText, userMap), user: '', timestamp };
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
if (msgType === 'signal') {
|
|
35
46
|
const result = parseSignalMessage(rawText, myUserId || '');
|
|
36
|
-
return { text: result?.text || rawText, user: '' };
|
|
47
|
+
return { text: result?.text || rawText, user: '', timestamp };
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
// Display 'Sticker' if message is a sticker
|
|
40
51
|
if (msgType === 'sticker' || (lastMsg as Record<string, unknown>).sticker_url) {
|
|
41
|
-
return { text: 'Sticker', user: lastMsg.user?.name || lastMsg.user_id || '' };
|
|
52
|
+
return { text: 'Sticker', user: lastMsg.user?.name || lastMsg.user_id || '', timestamp };
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
// Regular / other
|
|
@@ -78,6 +89,7 @@ function getLastMessagePreview(
|
|
|
78
89
|
return {
|
|
79
90
|
text: displayText,
|
|
80
91
|
user: lastMsg.user?.name || lastMsg.user_id || '',
|
|
92
|
+
timestamp,
|
|
81
93
|
};
|
|
82
94
|
}
|
|
83
95
|
|
|
@@ -91,25 +103,69 @@ export const ChannelItem: React.FC<ChannelItemProps> = React.memo(({
|
|
|
91
103
|
unreadCount,
|
|
92
104
|
lastMessageText,
|
|
93
105
|
lastMessageUser,
|
|
106
|
+
lastMessageTimestamp,
|
|
94
107
|
onSelect,
|
|
95
108
|
AvatarComponent,
|
|
96
109
|
isBlocked,
|
|
97
110
|
isPending,
|
|
98
111
|
pendingBadgeLabel,
|
|
99
112
|
blockedBadgeLabel,
|
|
113
|
+
isClosedTopic,
|
|
114
|
+
closedTopicIcon,
|
|
115
|
+
PinnedIconComponent,
|
|
116
|
+
ChannelActionsComponent,
|
|
117
|
+
onAddTopic,
|
|
118
|
+
onEditTopic,
|
|
119
|
+
onToggleCloseTopic,
|
|
120
|
+
hiddenActions,
|
|
121
|
+
actionLabels,
|
|
122
|
+
actionIcons,
|
|
123
|
+
isOnline,
|
|
100
124
|
}) => {
|
|
125
|
+
const { client } = useChatClient();
|
|
126
|
+
const currentUserId = client.userID;
|
|
127
|
+
|
|
101
128
|
// Subscribe to channel.updated so that when name/image/description change,
|
|
102
129
|
// we re-render from within (bypasses React.memo which only blocks parent-driven re-renders)
|
|
103
|
-
const [, forceUpdate] = useState(0);
|
|
130
|
+
const [updateCount, forceUpdate] = useState(0);
|
|
104
131
|
useEffect(() => {
|
|
105
|
-
const
|
|
106
|
-
|
|
132
|
+
const handleUpdate = () => forceUpdate((c) => c + 1);
|
|
133
|
+
const sub1 = channel.on('channel.updated', handleUpdate);
|
|
134
|
+
const sub2 = channel.on('channel.pinned', handleUpdate);
|
|
135
|
+
const sub3 = channel.on('channel.unpinned', handleUpdate);
|
|
136
|
+
return () => {
|
|
137
|
+
sub1.unsubscribe();
|
|
138
|
+
sub2.unsubscribe();
|
|
139
|
+
sub3.unsubscribe();
|
|
140
|
+
};
|
|
107
141
|
}, [channel]);
|
|
108
142
|
|
|
143
|
+
const defaultActions = useMemo(
|
|
144
|
+
() => computeDefaultActions(channel, currentUserId, { onAddTopic, onEditTopic, onToggleCloseTopic, isBlocked, actionLabels, actionIcons }),
|
|
145
|
+
[channel, currentUserId, updateCount, onAddTopic, onEditTopic, onToggleCloseTopic, isBlocked, actionLabels, actionIcons],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const filteredActions = useMemo(() => {
|
|
149
|
+
if (!hiddenActions || hiddenActions.length === 0) return defaultActions;
|
|
150
|
+
return defaultActions.filter(a => !hiddenActions.includes(a.id));
|
|
151
|
+
}, [defaultActions, hiddenActions]);
|
|
152
|
+
const ActionsComponent = ChannelActionsComponent || DefaultChannelActions;
|
|
153
|
+
|
|
109
154
|
const name = channel.data?.name || channel.cid;
|
|
110
155
|
const image = channel.data?.image as string | undefined;
|
|
111
156
|
const showUnread = hasUnread && !isActive;
|
|
112
157
|
|
|
158
|
+
const timestampText = useMemo(() => {
|
|
159
|
+
if (!lastMessageTimestamp) return null;
|
|
160
|
+
const d = new Date(lastMessageTimestamp);
|
|
161
|
+
if (isNaN(d.getTime())) return null;
|
|
162
|
+
const today = new Date();
|
|
163
|
+
const isToday = d.toDateString() === today.toDateString();
|
|
164
|
+
return isToday
|
|
165
|
+
? d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
166
|
+
: d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
167
|
+
}, [lastMessageTimestamp]);
|
|
168
|
+
|
|
113
169
|
const handleClick = useCallback(() => {
|
|
114
170
|
onSelect(channel);
|
|
115
171
|
}, [channel, onSelect]);
|
|
@@ -118,47 +174,90 @@ export const ChannelItem: React.FC<ChannelItemProps> = React.memo(({
|
|
|
118
174
|
'ermis-channel-list__item',
|
|
119
175
|
isActive ? 'ermis-channel-list__item--active' : '',
|
|
120
176
|
showUnread ? 'ermis-channel-list__item--unread' : '',
|
|
121
|
-
isBlocked ? 'ermis-channel-list__item--blocked' : '',
|
|
122
177
|
isPending ? 'ermis-channel-list__item--pending' : '',
|
|
123
178
|
].filter(Boolean).join(' ');
|
|
124
179
|
|
|
125
180
|
return (
|
|
126
181
|
<div className={itemClass} onClick={handleClick}>
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<div className="ermis-channel-list__item-last-message">
|
|
132
|
-
{lastMessageUser && (
|
|
133
|
-
<span className="ermis-channel-list__item-last-message-user">
|
|
134
|
-
{lastMessageUser}:{' '}
|
|
135
|
-
</span>
|
|
136
|
-
)}
|
|
137
|
-
<span>{lastMessageText}</span>
|
|
138
|
-
</div>
|
|
182
|
+
<div className="ermis-channel-list__item-avatar-wrapper">
|
|
183
|
+
<AvatarComponent image={image} name={name} size={40} disableLightbox />
|
|
184
|
+
{isOnline !== undefined && (
|
|
185
|
+
<span className={`ermis-channel-list__online-dot ermis-channel-list__online-dot--${isOnline ? 'online' : 'offline'}`} />
|
|
139
186
|
)}
|
|
140
187
|
</div>
|
|
141
|
-
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
188
|
+
<div className="ermis-channel-list__item-content">
|
|
189
|
+
<div className="ermis-channel-list__item-top-row">
|
|
190
|
+
<div className="ermis-channel-list__item-name">{name}</div>
|
|
191
|
+
{channel.data?.is_pinned === true && !isClosedTopic && PinnedIconComponent && (
|
|
192
|
+
<span className="ermis-channel-list__pinned-icon" title="Pinned">
|
|
193
|
+
<PinnedIconComponent />
|
|
194
|
+
</span>
|
|
195
|
+
)}
|
|
196
|
+
{isClosedTopic && (
|
|
197
|
+
<span className="ermis-channel-list__closed-icon">
|
|
198
|
+
{closedTopicIcon || (
|
|
199
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
200
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
|
201
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
202
|
+
</svg>
|
|
203
|
+
)}
|
|
204
|
+
</span>
|
|
205
|
+
)}
|
|
206
|
+
{!isClosedTopic && timestampText && <div className="ermis-channel-list__item-timestamp">{timestampText}</div>}
|
|
207
|
+
|
|
208
|
+
{isPending && (
|
|
209
|
+
<span className="ermis-channel-list__pending-badge">{pendingBadgeLabel || 'Invited'}</span>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{isBlocked && (
|
|
213
|
+
<span className="ermis-channel-list__blocked-icon" title={blockedBadgeLabel || "Blocked"}>
|
|
214
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
215
|
+
<circle cx="12" cy="12" r="10" />
|
|
216
|
+
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07" />
|
|
217
|
+
</svg>
|
|
218
|
+
</span>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
<div className="ermis-channel-list__item-bottom-row">
|
|
222
|
+
{!isClosedTopic && lastMessageText && (
|
|
223
|
+
<div className="ermis-channel-list__item-last-message">
|
|
224
|
+
{lastMessageUser && (
|
|
225
|
+
<span className="ermis-channel-list__item-last-message-user">
|
|
226
|
+
{lastMessageUser}:{' '}
|
|
227
|
+
</span>
|
|
228
|
+
)}
|
|
229
|
+
<span>{lastMessageText}</span>
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
|
|
233
|
+
{!isClosedTopic && (
|
|
234
|
+
<div className="ermis-channel-list__item-badges">
|
|
235
|
+
{showUnread && unreadCount > 0 && (
|
|
236
|
+
<span className="ermis-channel-list__unread-badge">
|
|
237
|
+
{unreadCount > 99 ? '99+' : unreadCount}
|
|
238
|
+
</span>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
{!isPending && (
|
|
245
|
+
<div className="ermis-channel-list__item-actions-wrapper">
|
|
246
|
+
<ActionsComponent channel={channel} actions={filteredActions} onClose={() => { }} />
|
|
247
|
+
</div>
|
|
156
248
|
)}
|
|
157
249
|
</div>
|
|
158
250
|
);
|
|
159
251
|
});
|
|
160
252
|
ChannelItem.displayName = 'ChannelItem';
|
|
161
253
|
|
|
254
|
+
export const DefaultPinnedIcon = React.memo(() => (
|
|
255
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
256
|
+
<path d="M16 12V4h1V2H7v2h1v8l-2 2v2h5.2v6h1.6v-6H18v-2l-2-2z" />
|
|
257
|
+
</svg>
|
|
258
|
+
));
|
|
259
|
+
DefaultPinnedIcon.displayName = 'DefaultPinnedIcon';
|
|
260
|
+
|
|
162
261
|
const DefaultLoading = React.memo(({ text }: { text?: string }) => (
|
|
163
262
|
<div className="ermis-channel-list__loading">{text || 'Loading channels...'}</div>
|
|
164
263
|
));
|
|
@@ -182,6 +281,16 @@ type ChannelRowProps = {
|
|
|
182
281
|
currentUserId?: string;
|
|
183
282
|
pendingBadgeLabel?: string;
|
|
184
283
|
blockedBadgeLabel?: string;
|
|
284
|
+
closedTopicIcon?: React.ReactNode;
|
|
285
|
+
PinnedIconComponent?: React.ComponentType;
|
|
286
|
+
ChannelActionsComponent?: React.ComponentType<ChannelActionsProps>;
|
|
287
|
+
onAddTopic?: (channel: Channel) => void;
|
|
288
|
+
onEditTopic?: (channel: Channel) => void;
|
|
289
|
+
onToggleCloseTopic?: (channel: Channel, isClosed: boolean) => void;
|
|
290
|
+
hiddenActions?: string[];
|
|
291
|
+
actionLabels?: import('../types').ChannelActionLabels;
|
|
292
|
+
actionIcons?: import('../types').ChannelActionIcons;
|
|
293
|
+
isOnline?: boolean;
|
|
185
294
|
};
|
|
186
295
|
|
|
187
296
|
const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
@@ -194,28 +303,43 @@ const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
|
194
303
|
currentUserId,
|
|
195
304
|
pendingBadgeLabel,
|
|
196
305
|
blockedBadgeLabel,
|
|
306
|
+
closedTopicIcon,
|
|
307
|
+
PinnedIconComponent,
|
|
308
|
+
ChannelActionsComponent,
|
|
309
|
+
onAddTopic,
|
|
310
|
+
onEditTopic,
|
|
311
|
+
onToggleCloseTopic,
|
|
312
|
+
hiddenActions,
|
|
313
|
+
actionLabels,
|
|
314
|
+
actionIcons,
|
|
315
|
+
isOnline,
|
|
197
316
|
}) => {
|
|
198
317
|
// Use the new custom hook to handle all row-level realtime updates
|
|
199
318
|
const { isBannedInChannel, isBlockedInChannel, updateCount } = useChannelRowUpdates(channel, currentUserId);
|
|
200
319
|
const { isPending } = usePendingState(channel, currentUserId);
|
|
320
|
+
const isSkipped = isSkippedMember(channel.state?.membership?.channel_role as string);
|
|
201
321
|
|
|
202
322
|
const channelState = channel.state as unknown as Record<string, unknown> | undefined;
|
|
203
323
|
const rawUnreadCount = (channelState?.unreadCount as number) ?? 0;
|
|
204
|
-
|
|
324
|
+
|
|
325
|
+
const isClosedTopic = channel.data?.is_closed_topic === true;
|
|
326
|
+
|
|
327
|
+
// Render logic continues...
|
|
328
|
+
const unreadCount = (isBannedInChannel || isBlockedInChannel || isPending || isSkipped) ? 0 : rawUnreadCount;
|
|
205
329
|
const hasUnread = unreadCount > 0;
|
|
206
330
|
|
|
207
|
-
// Derive last message preview computation
|
|
208
|
-
|
|
209
|
-
const { text: rawLastMessageText, user: rawLastMessageUser } = useMemo(
|
|
331
|
+
// Derive last message preview computation
|
|
332
|
+
const { text: rawLastMessageText, user: rawLastMessageUser, timestamp: rawLastMessageTimestamp } = useMemo(
|
|
210
333
|
() => getLastMessagePreview(channel, currentUserId),
|
|
211
334
|
// Recompute if latestMessage changes or we get a force update
|
|
212
335
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
213
336
|
[channel, channel.state?.latestMessages, updateCount]
|
|
214
337
|
);
|
|
215
338
|
|
|
216
|
-
// Hide last message preview when banned, blocked, or
|
|
217
|
-
const lastMessageText = (isBannedInChannel || isBlockedInChannel || isPending) ? '' : rawLastMessageText;
|
|
218
|
-
const lastMessageUser = (isBannedInChannel || isBlockedInChannel || isPending) ? '' : rawLastMessageUser;
|
|
339
|
+
// Hide last message preview when banned, blocked, pending or skipped
|
|
340
|
+
const lastMessageText = (isBannedInChannel || isBlockedInChannel || isPending || isSkipped) ? '' : rawLastMessageText;
|
|
341
|
+
const lastMessageUser = (isBannedInChannel || isBlockedInChannel || isPending || isSkipped) ? '' : rawLastMessageUser;
|
|
342
|
+
const lastMessageTimestamp = (isBannedInChannel || isBlockedInChannel || isPending || isSkipped) ? null : rawLastMessageTimestamp;
|
|
219
343
|
|
|
220
344
|
if (renderChannel) {
|
|
221
345
|
return (
|
|
@@ -233,17 +357,208 @@ const ChannelRow: React.FC<ChannelRowProps> = React.memo(({
|
|
|
233
357
|
unreadCount={unreadCount}
|
|
234
358
|
lastMessageText={lastMessageText}
|
|
235
359
|
lastMessageUser={lastMessageUser}
|
|
360
|
+
lastMessageTimestamp={lastMessageTimestamp}
|
|
236
361
|
onSelect={handleSelect}
|
|
237
362
|
AvatarComponent={AvatarComponent}
|
|
238
363
|
isBlocked={isBlockedInChannel}
|
|
239
364
|
isPending={isPending}
|
|
240
365
|
pendingBadgeLabel={pendingBadgeLabel}
|
|
241
366
|
blockedBadgeLabel={blockedBadgeLabel}
|
|
367
|
+
isClosedTopic={isClosedTopic}
|
|
368
|
+
closedTopicIcon={closedTopicIcon}
|
|
369
|
+
PinnedIconComponent={PinnedIconComponent}
|
|
370
|
+
ChannelActionsComponent={ChannelActionsComponent}
|
|
371
|
+
onAddTopic={onAddTopic}
|
|
372
|
+
onEditTopic={onEditTopic}
|
|
373
|
+
onToggleCloseTopic={onToggleCloseTopic}
|
|
374
|
+
hiddenActions={hiddenActions}
|
|
375
|
+
actionLabels={actionLabels}
|
|
376
|
+
actionIcons={actionIcons}
|
|
377
|
+
isOnline={isOnline}
|
|
242
378
|
/>
|
|
243
379
|
);
|
|
244
380
|
});
|
|
245
381
|
ChannelRow.displayName = 'ChannelRow';
|
|
246
382
|
|
|
383
|
+
export const ChannelTopicGroup = React.memo(({
|
|
384
|
+
channel,
|
|
385
|
+
activeChannel,
|
|
386
|
+
handleSelect,
|
|
387
|
+
renderChannel,
|
|
388
|
+
ChannelItemComponent,
|
|
389
|
+
AvatarComponent,
|
|
390
|
+
GeneralTopicAvatarComponent,
|
|
391
|
+
TopicAvatarComponent,
|
|
392
|
+
currentUserId,
|
|
393
|
+
pendingBadgeLabel,
|
|
394
|
+
blockedBadgeLabel,
|
|
395
|
+
generalTopicLabel,
|
|
396
|
+
closedTopicIcon,
|
|
397
|
+
PinnedIconComponent,
|
|
398
|
+
ChannelActionsComponent,
|
|
399
|
+
onAddTopic,
|
|
400
|
+
onEditTopic,
|
|
401
|
+
onToggleCloseTopic,
|
|
402
|
+
hiddenActions,
|
|
403
|
+
actionLabels,
|
|
404
|
+
actionIcons,
|
|
405
|
+
}: any) => {
|
|
406
|
+
const { updateCount } = useChannelRowUpdates(channel, currentUserId);
|
|
407
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
408
|
+
const [topicUpdateCount, setTopicUpdateCount] = useState(0);
|
|
409
|
+
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
const subs: { unsubscribe: () => void }[] = [];
|
|
412
|
+
const handleUpdate = () => setTopicUpdateCount((c) => c + 1);
|
|
413
|
+
const currentTopics = channel.state?.topics || [];
|
|
414
|
+
currentTopics.forEach((t: Channel) => {
|
|
415
|
+
subs.push(t.on('channel.pinned', handleUpdate));
|
|
416
|
+
subs.push(t.on('channel.unpinned', handleUpdate));
|
|
417
|
+
subs.push(t.on('message.new', handleUpdate));
|
|
418
|
+
subs.push(t.on('message.deleted', handleUpdate));
|
|
419
|
+
});
|
|
420
|
+
return () => {
|
|
421
|
+
subs.forEach((s) => s.unsubscribe());
|
|
422
|
+
};
|
|
423
|
+
}, [channel.state?.topics]);
|
|
424
|
+
|
|
425
|
+
const handleToggle = useCallback(() => setIsExpanded((prev) => !prev), []);
|
|
426
|
+
|
|
427
|
+
const userRole = channel.state?.members?.[currentUserId]?.channel_role;
|
|
428
|
+
const hasTopicAddPermission = canManageChannel(userRole);
|
|
429
|
+
|
|
430
|
+
const getTopicTime = (t: Channel) => {
|
|
431
|
+
const lastMsg = t.state?.latestMessages?.slice(-1)[0];
|
|
432
|
+
if (lastMsg?.created_at) return new Date(lastMsg.created_at).getTime();
|
|
433
|
+
if (t.data?.last_message_at) return new Date(t.data.last_message_at as string | Date).getTime();
|
|
434
|
+
if (t.data?.created_at) return new Date(t.data.created_at as string | Date).getTime();
|
|
435
|
+
return 0;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const topics = useMemo(() => {
|
|
439
|
+
const allTopics = channel.state?.topics || [];
|
|
440
|
+
return [...allTopics].sort((a: any, b: any) => {
|
|
441
|
+
const aPinned = a.data?.is_pinned === true;
|
|
442
|
+
const bPinned = b.data?.is_pinned === true;
|
|
443
|
+
if (aPinned && !bPinned) return -1;
|
|
444
|
+
if (!aPinned && bPinned) return 1;
|
|
445
|
+
|
|
446
|
+
return getTopicTime(b) - getTopicTime(a);
|
|
447
|
+
});
|
|
448
|
+
}, [channel.state?.topics, topicUpdateCount]);
|
|
449
|
+
const name = channel.data?.name || channel.cid;
|
|
450
|
+
const image = channel.data?.image as string | undefined;
|
|
451
|
+
|
|
452
|
+
const GeneralAvatar = useCallback(() => (
|
|
453
|
+
<div className="ermis-channel-list__topic-hashtag">#</div>
|
|
454
|
+
), []);
|
|
455
|
+
|
|
456
|
+
const TopicEmojiAvatar = useCallback(({ image }: any) => {
|
|
457
|
+
let emoji = '💬';
|
|
458
|
+
if (image && typeof image === 'string' && image.startsWith('emoji://')) {
|
|
459
|
+
emoji = image.replace('emoji://', '');
|
|
460
|
+
}
|
|
461
|
+
return <div className="ermis-channel-list__topic-hashtag">{emoji}</div>;
|
|
462
|
+
}, []);
|
|
463
|
+
|
|
464
|
+
const generalChannelProxy = useMemo(() => {
|
|
465
|
+
return new Proxy(channel, {
|
|
466
|
+
get(target, prop, receiver) {
|
|
467
|
+
if (prop === 'data') {
|
|
468
|
+
return { ...target.data, name: generalTopicLabel || 'general', is_pinned: false };
|
|
469
|
+
}
|
|
470
|
+
const value = Reflect.get(target, prop, receiver);
|
|
471
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}, [channel, generalTopicLabel]);
|
|
475
|
+
|
|
476
|
+
const defaultActions = useMemo(
|
|
477
|
+
() => computeDefaultActions(channel, currentUserId, { onAddTopic, actionLabels, actionIcons }),
|
|
478
|
+
[channel, currentUserId, updateCount, onAddTopic, actionLabels, actionIcons],
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const filteredActions = useMemo(() => {
|
|
482
|
+
if (!hiddenActions || hiddenActions.length === 0) return defaultActions;
|
|
483
|
+
return defaultActions.filter((a: any) => !hiddenActions.includes(a.id));
|
|
484
|
+
}, [defaultActions, hiddenActions]);
|
|
485
|
+
const ActionsComponent = ChannelActionsComponent || DefaultChannelActions;
|
|
486
|
+
|
|
487
|
+
return (
|
|
488
|
+
<div className="ermis-channel-list__topic-group">
|
|
489
|
+
<div
|
|
490
|
+
className={`ermis-channel-list__topic-header ${isExpanded ? 'ermis-channel-list__topic-header--expanded' : ''}`}
|
|
491
|
+
onClick={handleToggle}
|
|
492
|
+
>
|
|
493
|
+
<AvatarComponent image={image} name={name} size={40} disableLightbox />
|
|
494
|
+
<div className="ermis-channel-list__topic-header-name">{name}</div>
|
|
495
|
+
|
|
496
|
+
{channel.data?.is_pinned === true && PinnedIconComponent && (
|
|
497
|
+
<span className="ermis-channel-list__pinned-icon" title="Pinned">
|
|
498
|
+
<PinnedIconComponent />
|
|
499
|
+
</span>
|
|
500
|
+
)}
|
|
501
|
+
|
|
502
|
+
<div className="ermis-channel-list__topic-actions-wrapper">
|
|
503
|
+
<ActionsComponent channel={channel} actions={filteredActions} onClose={() => { }} />
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<svg
|
|
507
|
+
className="ermis-channel-list__accordion-icon"
|
|
508
|
+
width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
|
|
509
|
+
>
|
|
510
|
+
<polyline points="6 9 12 15 18 9"></polyline>
|
|
511
|
+
</svg>
|
|
512
|
+
</div>
|
|
513
|
+
|
|
514
|
+
{isExpanded && (
|
|
515
|
+
<div className="ermis-channel-list__topic-sublist">
|
|
516
|
+
<ChannelRow
|
|
517
|
+
channel={generalChannelProxy as any}
|
|
518
|
+
isActive={activeChannel?.cid === channel.cid}
|
|
519
|
+
handleSelect={handleSelect}
|
|
520
|
+
renderChannel={renderChannel}
|
|
521
|
+
ChannelItemComponent={ChannelItemComponent}
|
|
522
|
+
AvatarComponent={GeneralTopicAvatarComponent || GeneralAvatar}
|
|
523
|
+
currentUserId={currentUserId}
|
|
524
|
+
pendingBadgeLabel={pendingBadgeLabel}
|
|
525
|
+
blockedBadgeLabel={blockedBadgeLabel}
|
|
526
|
+
closedTopicIcon={closedTopicIcon}
|
|
527
|
+
PinnedIconComponent={PinnedIconComponent}
|
|
528
|
+
ChannelActionsComponent={() => null}
|
|
529
|
+
hiddenActions={hiddenActions}
|
|
530
|
+
actionLabels={actionLabels}
|
|
531
|
+
actionIcons={actionIcons}
|
|
532
|
+
/>
|
|
533
|
+
{topics.map((topicChannel: any) => (
|
|
534
|
+
<ChannelRow
|
|
535
|
+
key={topicChannel.cid}
|
|
536
|
+
channel={topicChannel}
|
|
537
|
+
isActive={activeChannel?.cid === topicChannel.cid}
|
|
538
|
+
handleSelect={handleSelect}
|
|
539
|
+
renderChannel={renderChannel}
|
|
540
|
+
ChannelItemComponent={ChannelItemComponent}
|
|
541
|
+
AvatarComponent={TopicAvatarComponent || TopicEmojiAvatar}
|
|
542
|
+
currentUserId={currentUserId}
|
|
543
|
+
pendingBadgeLabel={pendingBadgeLabel}
|
|
544
|
+
blockedBadgeLabel={blockedBadgeLabel}
|
|
545
|
+
closedTopicIcon={closedTopicIcon}
|
|
546
|
+
PinnedIconComponent={PinnedIconComponent}
|
|
547
|
+
ChannelActionsComponent={ChannelActionsComponent}
|
|
548
|
+
onEditTopic={onEditTopic}
|
|
549
|
+
onToggleCloseTopic={onToggleCloseTopic}
|
|
550
|
+
hiddenActions={hiddenActions}
|
|
551
|
+
actionLabels={actionLabels}
|
|
552
|
+
actionIcons={actionIcons}
|
|
553
|
+
/>
|
|
554
|
+
))}
|
|
555
|
+
</div>
|
|
556
|
+
)}
|
|
557
|
+
</div>
|
|
558
|
+
);
|
|
559
|
+
});
|
|
560
|
+
ChannelTopicGroup.displayName = 'ChannelTopicGroup';
|
|
561
|
+
|
|
247
562
|
export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
248
563
|
filters = { type: ['messaging', 'team', 'meeting'], include_pinned_messages: true } as unknown as ChannelFilters,
|
|
249
564
|
sort = [],
|
|
@@ -261,28 +576,93 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
261
576
|
loadingLabel,
|
|
262
577
|
emptyStateLabel = 'No channels found',
|
|
263
578
|
blockedBadgeLabel = 'Blocked',
|
|
579
|
+
ChannelTopicGroupComponent,
|
|
580
|
+
GeneralTopicAvatarComponent,
|
|
581
|
+
TopicAvatarComponent,
|
|
582
|
+
generalTopicLabel = 'general',
|
|
583
|
+
onAddTopic,
|
|
584
|
+
TopicEmojiPickerComponent,
|
|
585
|
+
closedTopicIcon,
|
|
586
|
+
PinnedIconComponent = DefaultPinnedIcon,
|
|
587
|
+
ChannelActionsComponent,
|
|
588
|
+
onEditTopic,
|
|
589
|
+
onToggleCloseTopic,
|
|
590
|
+
hiddenActions,
|
|
591
|
+
actionLabels,
|
|
592
|
+
actionIcons,
|
|
593
|
+
showOnlineStatus = true,
|
|
264
594
|
}) => {
|
|
265
595
|
const { client, activeChannel, setActiveChannel } = useChatClient();
|
|
266
596
|
const [channels, setChannels] = useState<Channel[]>([]);
|
|
267
597
|
const [loading, setLoading] = useState(true);
|
|
268
598
|
const [isPendingExpanded, setIsPendingExpanded] = useState(true);
|
|
599
|
+
const [addingTopicForChannel, setAddingTopicForChannel] = useState<Channel | null>(null);
|
|
600
|
+
const [editingTopicForChannel, setEditingTopicForChannel] = useState<Channel | null>(null);
|
|
601
|
+
|
|
602
|
+
const handleAddTopicClick = useCallback((channel: Channel) => {
|
|
603
|
+
if (onAddTopic) {
|
|
604
|
+
onAddTopic(channel);
|
|
605
|
+
} else {
|
|
606
|
+
setAddingTopicForChannel(channel);
|
|
607
|
+
}
|
|
608
|
+
}, [onAddTopic]);
|
|
609
|
+
|
|
610
|
+
const handleEditTopicClick = useCallback((channel: Channel) => {
|
|
611
|
+
if (onEditTopic) {
|
|
612
|
+
onEditTopic(channel);
|
|
613
|
+
} else {
|
|
614
|
+
setEditingTopicForChannel(channel);
|
|
615
|
+
}
|
|
616
|
+
}, [onEditTopic]);
|
|
617
|
+
|
|
618
|
+
const handleToggleCloseTopicClick = useCallback(async (channel: Channel, isClosed: boolean) => {
|
|
619
|
+
if (onToggleCloseTopic) {
|
|
620
|
+
onToggleCloseTopic(channel, isClosed);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const parentCid = channel.data?.parent_cid as string | undefined;
|
|
625
|
+
if (!parentCid) return;
|
|
626
|
+
|
|
627
|
+
const parentChannel = client.activeChannels[parentCid];
|
|
628
|
+
if (!parentChannel) return;
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
if (isClosed) {
|
|
632
|
+
await parentChannel.reopenTopic(channel.cid);
|
|
633
|
+
} else {
|
|
634
|
+
await parentChannel.closeTopic(channel.cid);
|
|
635
|
+
}
|
|
636
|
+
} catch (err) {
|
|
637
|
+
console.error('Failed to toggle topic close state', err);
|
|
638
|
+
}
|
|
639
|
+
}, [client.activeChannels, onToggleCloseTopic]);
|
|
269
640
|
|
|
270
641
|
// Group channels into pending and regular
|
|
271
642
|
const { pendingChannels, regularChannels } = useMemo<{ pendingChannels: Channel[], regularChannels: Channel[] }>(() => {
|
|
272
643
|
const pending: Channel[] = [];
|
|
644
|
+
const pinned: Channel[] = [];
|
|
273
645
|
const regular: Channel[] = [];
|
|
274
646
|
|
|
275
647
|
channels.forEach(ch => {
|
|
276
648
|
const ms = ch.state?.membership as Record<string, unknown> | undefined;
|
|
277
|
-
const isPending = ms?.channel_role
|
|
649
|
+
const isPending = isPendingMember(ms?.channel_role as string);
|
|
650
|
+
const isSkipped = isSkippedMember(ms?.channel_role as string);
|
|
651
|
+
|
|
652
|
+
if (isSkipped) {
|
|
653
|
+
return; // Filter out completely
|
|
654
|
+
}
|
|
655
|
+
|
|
278
656
|
if (isPending) {
|
|
279
657
|
pending.push(ch);
|
|
658
|
+
} else if (ch.data?.is_pinned) {
|
|
659
|
+
pinned.push(ch);
|
|
280
660
|
} else {
|
|
281
661
|
regular.push(ch);
|
|
282
662
|
}
|
|
283
663
|
});
|
|
284
664
|
|
|
285
|
-
return { pendingChannels: pending, regularChannels: regular };
|
|
665
|
+
return { pendingChannels: pending, regularChannels: [...pinned, ...regular] };
|
|
286
666
|
}, [channels]);
|
|
287
667
|
|
|
288
668
|
const filtersKey = useMemo(() => JSON.stringify(filters), [filters]);
|
|
@@ -306,6 +686,28 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
306
686
|
// Real-time: List manipulation (move to top, add, delete)
|
|
307
687
|
useChannelListUpdates(channels, setChannels);
|
|
308
688
|
|
|
689
|
+
// Online status: compute set of online friend user IDs (skip if disabled)
|
|
690
|
+
const onlineUsers = useOnlineUsers(showOnlineStatus ? channels : []);
|
|
691
|
+
|
|
692
|
+
// Helper: get the "other" user ID from a direct channel
|
|
693
|
+
const getOtherUserId = useCallback((channel: Channel): string | undefined => {
|
|
694
|
+
if (!isDirectChannel(channel) || !client.userID) return undefined;
|
|
695
|
+
const members = channel.state?.members;
|
|
696
|
+
if (!members) return undefined;
|
|
697
|
+
for (const memberId of Object.keys(members)) {
|
|
698
|
+
if (memberId !== client.userID) return memberId;
|
|
699
|
+
}
|
|
700
|
+
return undefined;
|
|
701
|
+
}, [client.userID]);
|
|
702
|
+
|
|
703
|
+
// Helper: compute isOnline for a channel (undefined for non-friend channels)
|
|
704
|
+
const getIsOnline = useCallback((channel: Channel): boolean | undefined => {
|
|
705
|
+
const otherUserId = getOtherUserId(channel);
|
|
706
|
+
if (!otherUserId || !client.userID) return undefined;
|
|
707
|
+
if (!isFriendChannel(channel, otherUserId, client.userID)) return undefined;
|
|
708
|
+
return onlineUsers.has(otherUserId);
|
|
709
|
+
}, [getOtherUserId, onlineUsers, client.userID]);
|
|
710
|
+
|
|
309
711
|
const handleSelect = useCallback(
|
|
310
712
|
(channel: Channel) => {
|
|
311
713
|
setActiveChannel(channel);
|
|
@@ -315,10 +717,11 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
315
717
|
const ms = channel.state?.membership as Record<string, unknown> | undefined;
|
|
316
718
|
const chState = channel.state as unknown as Record<string, unknown> | undefined;
|
|
317
719
|
const isBannedInChannel = Boolean(ms?.banned);
|
|
318
|
-
const isBlockedInChannel = channel
|
|
319
|
-
const isPending = ms?.channel_role
|
|
720
|
+
const isBlockedInChannel = isDirectChannel(channel) && Boolean(ms?.blocked);
|
|
721
|
+
const isPending = isPendingMember(ms?.channel_role as string);
|
|
722
|
+
const isSkipped = isSkippedMember(ms?.channel_role as string);
|
|
320
723
|
|
|
321
|
-
if (!isBannedInChannel && !isBlockedInChannel && !isPending && (chState?.unreadCount as number) > 0) {
|
|
724
|
+
if (!isBannedInChannel && !isBlockedInChannel && !isPending && !isSkipped && (chState?.unreadCount as number) > 0) {
|
|
322
725
|
channel.markRead().catch(() => { });
|
|
323
726
|
// Optimistically reset unread to update UI immediately
|
|
324
727
|
if (chState) chState.unreadCount = 0;
|
|
@@ -367,6 +770,13 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
367
770
|
currentUserId={client.userID}
|
|
368
771
|
pendingBadgeLabel={pendingBadgeLabel}
|
|
369
772
|
blockedBadgeLabel={blockedBadgeLabel}
|
|
773
|
+
closedTopicIcon={closedTopicIcon}
|
|
774
|
+
PinnedIconComponent={PinnedIconComponent}
|
|
775
|
+
ChannelActionsComponent={ChannelActionsComponent}
|
|
776
|
+
hiddenActions={hiddenActions}
|
|
777
|
+
actionLabels={actionLabels}
|
|
778
|
+
actionIcons={actionIcons}
|
|
779
|
+
isOnline={getIsOnline(channel)}
|
|
370
780
|
/>
|
|
371
781
|
);
|
|
372
782
|
})}
|
|
@@ -377,6 +787,37 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
377
787
|
)}
|
|
378
788
|
{regularChannels.map((channel: Channel) => {
|
|
379
789
|
const isActive = activeChannel?.cid === channel.cid;
|
|
790
|
+
const isTeamWithTopics = hasTopicsEnabled(channel);
|
|
791
|
+
|
|
792
|
+
if (isTeamWithTopics) {
|
|
793
|
+
const GroupComponent = ChannelTopicGroupComponent || ChannelTopicGroup;
|
|
794
|
+
return (
|
|
795
|
+
<GroupComponent
|
|
796
|
+
key={channel.cid}
|
|
797
|
+
channel={channel}
|
|
798
|
+
activeChannel={activeChannel}
|
|
799
|
+
handleSelect={handleSelect}
|
|
800
|
+
renderChannel={renderChannel}
|
|
801
|
+
ChannelItemComponent={ChannelItemComponent}
|
|
802
|
+
AvatarComponent={AvatarComponent}
|
|
803
|
+
GeneralTopicAvatarComponent={GeneralTopicAvatarComponent}
|
|
804
|
+
TopicAvatarComponent={TopicAvatarComponent}
|
|
805
|
+
currentUserId={client.userID}
|
|
806
|
+
pendingBadgeLabel={pendingBadgeLabel}
|
|
807
|
+
blockedBadgeLabel={blockedBadgeLabel}
|
|
808
|
+
generalTopicLabel={generalTopicLabel}
|
|
809
|
+
onAddTopic={handleAddTopicClick}
|
|
810
|
+
closedTopicIcon={closedTopicIcon}
|
|
811
|
+
PinnedIconComponent={PinnedIconComponent}
|
|
812
|
+
ChannelActionsComponent={ChannelActionsComponent}
|
|
813
|
+
onEditTopic={handleEditTopicClick}
|
|
814
|
+
onToggleCloseTopic={handleToggleCloseTopicClick}
|
|
815
|
+
hiddenActions={hiddenActions}
|
|
816
|
+
actionLabels={actionLabels}
|
|
817
|
+
actionIcons={actionIcons}
|
|
818
|
+
/>
|
|
819
|
+
);
|
|
820
|
+
}
|
|
380
821
|
|
|
381
822
|
return (
|
|
382
823
|
<ChannelRow
|
|
@@ -390,10 +831,36 @@ export const ChannelList: React.FC<ChannelListProps> = React.memo(({
|
|
|
390
831
|
currentUserId={client.userID}
|
|
391
832
|
pendingBadgeLabel={pendingBadgeLabel}
|
|
392
833
|
blockedBadgeLabel={blockedBadgeLabel}
|
|
834
|
+
closedTopicIcon={closedTopicIcon}
|
|
835
|
+
PinnedIconComponent={PinnedIconComponent}
|
|
836
|
+
ChannelActionsComponent={ChannelActionsComponent}
|
|
837
|
+
onAddTopic={handleAddTopicClick}
|
|
838
|
+
onEditTopic={handleEditTopicClick}
|
|
839
|
+
onToggleCloseTopic={handleToggleCloseTopicClick}
|
|
840
|
+
hiddenActions={hiddenActions}
|
|
841
|
+
actionLabels={actionLabels}
|
|
842
|
+
actionIcons={actionIcons}
|
|
843
|
+
isOnline={getIsOnline(channel)}
|
|
393
844
|
/>
|
|
394
845
|
);
|
|
395
846
|
})}
|
|
396
847
|
</VList>
|
|
848
|
+
{addingTopicForChannel && (
|
|
849
|
+
<TopicModal
|
|
850
|
+
isOpen={true}
|
|
851
|
+
onClose={() => setAddingTopicForChannel(null)}
|
|
852
|
+
parentChannel={addingTopicForChannel}
|
|
853
|
+
EmojiPickerComponent={TopicEmojiPickerComponent}
|
|
854
|
+
/>
|
|
855
|
+
)}
|
|
856
|
+
{editingTopicForChannel && (
|
|
857
|
+
<TopicModal
|
|
858
|
+
isOpen={true}
|
|
859
|
+
onClose={() => setEditingTopicForChannel(null)}
|
|
860
|
+
topic={editingTopicForChannel}
|
|
861
|
+
EmojiPickerComponent={TopicEmojiPickerComponent}
|
|
862
|
+
/>
|
|
863
|
+
)}
|
|
397
864
|
</div>
|
|
398
865
|
);
|
|
399
866
|
});
|