@ermis-network/ermis-chat-react 1.0.5 → 1.0.7
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 +2411 -1309
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +471 -16
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +145 -1
- package/dist/index.d.ts +145 -1
- package/dist/index.mjs +2340 -1242
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/BannedOverlay.tsx +40 -0
- package/src/components/ChannelActions.tsx +231 -0
- package/src/components/ChannelHeader.tsx +38 -2
- package/src/components/ChannelInfo/ChannelInfo.tsx +118 -20
- package/src/components/ChannelInfo/ChannelInfoTabs.tsx +10 -2
- package/src/components/ChannelInfo/ChannelSettingsPanel.tsx +88 -1
- package/src/components/ChannelInfo/EditChannelModal.tsx +4 -4
- package/src/components/ChannelList.tsx +467 -45
- package/src/components/ClosedTopicOverlay.tsx +38 -0
- package/src/components/MessageInput.tsx +19 -2
- package/src/components/MessageItem.tsx +8 -11
- package/src/components/MessageQuickReactions.tsx +3 -2
- package/src/components/MessageReactions.tsx +8 -3
- package/src/components/MessageRenderers.tsx +7 -9
- package/src/components/PendingOverlay.tsx +41 -0
- package/src/components/TopicModal.tsx +189 -0
- package/src/components/VirtualMessageList.tsx +74 -43
- package/src/hooks/useBannedState.ts +27 -3
- package/src/hooks/useChannelCapabilities.ts +7 -3
- package/src/hooks/useChannelData.ts +1 -1
- package/src/hooks/useChannelListUpdates.ts +24 -3
- package/src/hooks/useChannelRowUpdates.ts +6 -0
- package/src/hooks/useMessageActions.ts +1 -1
- package/src/index.ts +6 -1
- package/src/styles/_channel-info.css +21 -0
- package/src/styles/_channel-list.css +217 -6
- package/src/styles/_message-bubble.css +75 -9
- package/src/styles/_message-input.css +24 -0
- package/src/styles/_message-list.css +51 -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 +1 -0
- package/src/types.ts +157 -3
|
@@ -6,12 +6,16 @@ import type { Channel } from '@ermis-network/ermis-chat-sdk';
|
|
|
6
6
|
*
|
|
7
7
|
* Reads the initial value from `channel.state.membership.banned` and subscribes
|
|
8
8
|
* to `member.banned` / `member.unbanned` WebSocket events for real-time updates.
|
|
9
|
+
* If the channel is a topic, it also synchronizes with the parent channel's ban state.
|
|
9
10
|
*
|
|
10
11
|
* Only triggers a re-render when the *current user* is the target of the event.
|
|
11
12
|
*/
|
|
12
13
|
export function useBannedState(channel: Channel | null | undefined, currentUserId?: string) {
|
|
13
14
|
const [isBanned, setIsBanned] = useState<boolean>(() => {
|
|
14
|
-
|
|
15
|
+
if (!channel) return false;
|
|
16
|
+
const parentCid = channel.data?.parent_cid as string | undefined;
|
|
17
|
+
const parentChannel = parentCid ? channel.getClient().activeChannels[parentCid] : undefined;
|
|
18
|
+
return Boolean(channel.state?.membership?.banned || parentChannel?.state?.membership?.banned);
|
|
15
19
|
});
|
|
16
20
|
|
|
17
21
|
useEffect(() => {
|
|
@@ -20,8 +24,11 @@ export function useBannedState(channel: Channel | null | undefined, currentUserI
|
|
|
20
24
|
return;
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
const parentCid = channel.data?.parent_cid as string | undefined;
|
|
28
|
+
const parentChannel = parentCid ? channel.getClient().activeChannels[parentCid] : undefined;
|
|
29
|
+
|
|
23
30
|
// Sync initial state when channel changes
|
|
24
|
-
setIsBanned(Boolean(channel.state?.membership?.banned));
|
|
31
|
+
setIsBanned(Boolean(channel.state?.membership?.banned || parentChannel?.state?.membership?.banned));
|
|
25
32
|
|
|
26
33
|
const handleBanned = (event: any) => {
|
|
27
34
|
if (event.member?.user_id === currentUserId) {
|
|
@@ -31,16 +38,33 @@ export function useBannedState(channel: Channel | null | undefined, currentUserI
|
|
|
31
38
|
|
|
32
39
|
const handleUnbanned = (event: any) => {
|
|
33
40
|
if (event.member?.user_id === currentUserId) {
|
|
34
|
-
|
|
41
|
+
const eventCid = event.cid || (event.channel_type ? `${event.channel_type}:${event.channel_id}` : undefined);
|
|
42
|
+
let cBanned = Boolean(channel.state?.membership?.banned);
|
|
43
|
+
let pBanned = Boolean(parentChannel?.state?.membership?.banned);
|
|
44
|
+
|
|
45
|
+
if (eventCid === channel.cid) cBanned = false;
|
|
46
|
+
if (parentChannel && eventCid === parentChannel.cid) pBanned = false;
|
|
47
|
+
|
|
48
|
+
setIsBanned(cBanned || pBanned);
|
|
35
49
|
}
|
|
36
50
|
};
|
|
37
51
|
|
|
38
52
|
const sub1 = channel.on('member.banned', handleBanned);
|
|
39
53
|
const sub2 = channel.on('member.unbanned', handleUnbanned);
|
|
40
54
|
|
|
55
|
+
let sub3: { unsubscribe: () => void } | undefined;
|
|
56
|
+
let sub4: { unsubscribe: () => void } | undefined;
|
|
57
|
+
|
|
58
|
+
if (parentChannel) {
|
|
59
|
+
sub3 = parentChannel.on('member.banned', handleBanned);
|
|
60
|
+
sub4 = parentChannel.on('member.unbanned', handleUnbanned);
|
|
61
|
+
}
|
|
62
|
+
|
|
41
63
|
return () => {
|
|
42
64
|
sub1.unsubscribe();
|
|
43
65
|
sub2.unsubscribe();
|
|
66
|
+
if (sub3) sub3.unsubscribe();
|
|
67
|
+
if (sub4) sub4.unsubscribe();
|
|
44
68
|
};
|
|
45
69
|
}, [channel, currentUserId]);
|
|
46
70
|
|
|
@@ -18,20 +18,24 @@ export const useChannelCapabilities = () => {
|
|
|
18
18
|
|
|
19
19
|
const currentUserId = client?.userID || '';
|
|
20
20
|
const isTeamChannel = activeChannel?.type === 'team';
|
|
21
|
+
const isMeetingChannel = activeChannel?.type === 'meeting';
|
|
22
|
+
const isTeamOrMeetingChannel = isTeamChannel || isMeetingChannel;
|
|
21
23
|
const role = (activeChannel?.state as any)?.members?.[currentUserId]?.channel_role;
|
|
22
24
|
|
|
23
25
|
const isOwner = role === 'owner' || activeChannel?.data?.created_by_id === currentUserId;
|
|
24
26
|
const isModerator = role === 'moder';
|
|
25
27
|
const isOwnerOrModerator = isOwner || isModerator;
|
|
26
28
|
|
|
27
|
-
const capabilities: string[] =
|
|
29
|
+
const capabilities: string[] = isTeamOrMeetingChannel ? (activeChannel?.data as any)?.member_capabilities || [] : [];
|
|
28
30
|
|
|
29
31
|
const hasCapability = useCallback((cap: string) => {
|
|
30
|
-
return !
|
|
31
|
-
}, [
|
|
32
|
+
return !isTeamOrMeetingChannel || isOwnerOrModerator || capabilities.includes(cap);
|
|
33
|
+
}, [isTeamOrMeetingChannel, isOwnerOrModerator, capabilities, updateTick]); // React to updateTick correctly
|
|
32
34
|
|
|
33
35
|
return {
|
|
34
36
|
isTeamChannel,
|
|
37
|
+
isMeetingChannel,
|
|
38
|
+
isTeamOrMeetingChannel,
|
|
35
39
|
isOwner,
|
|
36
40
|
isModerator,
|
|
37
41
|
isOwnerOrModerator,
|
|
@@ -47,7 +47,7 @@ export const useChannelProfile = (channel: Channel | null | undefined) => {
|
|
|
47
47
|
return () => sub.unsubscribe();
|
|
48
48
|
}, [channel]);
|
|
49
49
|
|
|
50
|
-
const channelName = useMemo(() => channel?.data?.name || channel?.cid || 'Unknown Channel', [channel?.data?.name, channel?.cid, channelUpdateCount]);
|
|
50
|
+
const channelName = useMemo(() => channel?.data?.name || channel?.cid || 'Unknown Channel', [channel?.data?.name, channel?.cid, channel?.type, channelUpdateCount]);
|
|
51
51
|
const channelImage = useMemo(() => channel?.data?.image as string | undefined, [channel?.data?.image, channelUpdateCount]);
|
|
52
52
|
const channelDescription = useMemo(() => channel?.data?.description as string | undefined, [channel?.data?.description, channelUpdateCount]);
|
|
53
53
|
|
|
@@ -37,7 +37,8 @@ export function useChannelListUpdates(
|
|
|
37
37
|
const isBannedInActive = Boolean(active.state?.membership?.banned);
|
|
38
38
|
const isBlockedInActive = active.type === 'messaging' && Boolean(active.state?.membership?.blocked);
|
|
39
39
|
const isPendingActive =
|
|
40
|
-
active.state?.membership?.channel_role === 'pending' ||
|
|
40
|
+
active.state?.membership?.channel_role === 'pending' ||
|
|
41
|
+
(active.state?.membership as Record<string, unknown>)?.role === 'pending';
|
|
41
42
|
|
|
42
43
|
if (!isBannedInActive && !isBlockedInActive && !isPendingActive) {
|
|
43
44
|
active.markRead().catch(() => {
|
|
@@ -121,7 +122,10 @@ export function useChannelListUpdates(
|
|
|
121
122
|
// we optimistically inject the membership so it instantly jumps into pending invites!
|
|
122
123
|
// We DO NOT do this for channel.created, because in channel.created, event.member is the creator (owner).
|
|
123
124
|
if (!forceWatch && event.type === 'member.added' && event.member && channelInstance.state) {
|
|
124
|
-
channelInstance.state.membership = {
|
|
125
|
+
channelInstance.state.membership = {
|
|
126
|
+
...channelInstance.state.membership,
|
|
127
|
+
...event.member,
|
|
128
|
+
} as unknown as Record<string, unknown>;
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
// If the caller requested an explicit api call (e.g. for channel.created)
|
|
@@ -183,7 +187,9 @@ export function useChannelListUpdates(
|
|
|
183
187
|
const eventCid =
|
|
184
188
|
event.cid ||
|
|
185
189
|
event.channel?.cid ||
|
|
186
|
-
((event as Record<string, unknown>).channel_id
|
|
190
|
+
((event as Record<string, unknown>).channel_id
|
|
191
|
+
? `${(event as Record<string, unknown>).channel_type}:${(event as Record<string, unknown>).channel_id}`
|
|
192
|
+
: undefined);
|
|
187
193
|
|
|
188
194
|
if (eventCid && event.member) {
|
|
189
195
|
const targetChannel = prev.find((c) => c.cid === eventCid);
|
|
@@ -201,6 +207,11 @@ export function useChannelListUpdates(
|
|
|
201
207
|
}
|
|
202
208
|
};
|
|
203
209
|
|
|
210
|
+
// --- channel.topic.enabled / disabled / created / channel.pinned / channel.unpinned: force re-render so ChannelList toggles Accordion UI, inserts new topic, or updates pinned channels ---
|
|
211
|
+
const handleGenericUpdate = (event: Event) => {
|
|
212
|
+
setChannels((prev) => [...prev]);
|
|
213
|
+
};
|
|
214
|
+
|
|
204
215
|
const sub1 = client.on('message.new', handleNewMessage);
|
|
205
216
|
const sub2 = client.on('channel.deleted', handleChannelDeleted);
|
|
206
217
|
const sub3 = client.on('member.removed', handleMemberRemoved);
|
|
@@ -209,6 +220,11 @@ export function useChannelListUpdates(
|
|
|
209
220
|
const sub6 = client.on('notification.added_to_channel', handleMemberAdded);
|
|
210
221
|
const sub7 = client.on('notification.invite_rejected', handleMemberRemoved);
|
|
211
222
|
const sub8 = client.on('notification.invite_accepted', handleMemberUpdated);
|
|
223
|
+
const sub9 = client.on('channel.topic.enabled', handleGenericUpdate);
|
|
224
|
+
const sub10 = client.on('channel.topic.disabled', handleGenericUpdate);
|
|
225
|
+
const sub11 = client.on('channel.topic.created', handleGenericUpdate);
|
|
226
|
+
const sub12 = client.on('channel.pinned', handleGenericUpdate);
|
|
227
|
+
const sub13 = client.on('channel.unpinned', handleGenericUpdate);
|
|
212
228
|
|
|
213
229
|
return () => {
|
|
214
230
|
sub1.unsubscribe();
|
|
@@ -219,6 +235,11 @@ export function useChannelListUpdates(
|
|
|
219
235
|
sub6.unsubscribe();
|
|
220
236
|
sub7.unsubscribe();
|
|
221
237
|
sub8.unsubscribe();
|
|
238
|
+
sub9.unsubscribe();
|
|
239
|
+
sub10.unsubscribe();
|
|
240
|
+
sub11.unsubscribe();
|
|
241
|
+
sub12.unsubscribe();
|
|
242
|
+
sub13.unsubscribe();
|
|
222
243
|
};
|
|
223
244
|
}, [client, setChannels, setActiveChannel]);
|
|
224
245
|
}
|
|
@@ -58,6 +58,9 @@ export function useChannelRowUpdates(channel: Channel, currentUserId?: string) {
|
|
|
58
58
|
};
|
|
59
59
|
const sub10 = channel.on('member.blocked', handleBlocked);
|
|
60
60
|
const sub11 = channel.on('member.unblocked', handleUnblocked);
|
|
61
|
+
const sub12 = channel.on('channel.topic.created', handleUpdate);
|
|
62
|
+
const sub13 = channel.on('channel.pinned', handleUpdate);
|
|
63
|
+
const sub14 = channel.on('channel.unpinned', handleUpdate);
|
|
61
64
|
|
|
62
65
|
return () => {
|
|
63
66
|
sub1.unsubscribe();
|
|
@@ -71,6 +74,9 @@ export function useChannelRowUpdates(channel: Channel, currentUserId?: string) {
|
|
|
71
74
|
sub9.unsubscribe();
|
|
72
75
|
sub10.unsubscribe();
|
|
73
76
|
sub11.unsubscribe();
|
|
77
|
+
sub12.unsubscribe();
|
|
78
|
+
sub13.unsubscribe();
|
|
79
|
+
sub14.unsubscribe();
|
|
74
80
|
};
|
|
75
81
|
}, [channel, currentUserId]);
|
|
76
82
|
|
|
@@ -23,7 +23,7 @@ export type MessageActionList = {
|
|
|
23
23
|
|
|
24
24
|
export const useMessageActions = (message: FormatMessageResponse, isOwnMessage: boolean): MessageActionList => {
|
|
25
25
|
const { activeChannel, client } = useChatClient();
|
|
26
|
-
const {
|
|
26
|
+
const { isTeamOrMeetingChannel: isTeam, isOwner, hasCapability } = useChannelCapabilities();
|
|
27
27
|
|
|
28
28
|
// Only depend on the specific message fields we actually read
|
|
29
29
|
const messageType = message.type;
|
package/src/index.ts
CHANGED
|
@@ -19,9 +19,12 @@ export { usePendingState } from './hooks/usePendingState';
|
|
|
19
19
|
export { Avatar } from './components/Avatar';
|
|
20
20
|
export type { AvatarProps } from './components/Avatar';
|
|
21
21
|
|
|
22
|
-
export { ChannelList, ChannelItem } from './components/ChannelList';
|
|
22
|
+
export { ChannelList, ChannelItem, ChannelTopicGroup } from './components/ChannelList';
|
|
23
23
|
export type { ChannelListProps, ChannelItemProps } from './components/ChannelList';
|
|
24
24
|
|
|
25
|
+
export { DefaultChannelActions, computeDefaultActions } from './components/ChannelActions';
|
|
26
|
+
export type { ChannelAction, ChannelActionsProps } from './types';
|
|
27
|
+
|
|
25
28
|
export { Channel } from './components/Channel';
|
|
26
29
|
export type { ChannelProps } from './components/Channel';
|
|
27
30
|
|
|
@@ -94,6 +97,8 @@ export type { ReplyPreviewProps } from './types';
|
|
|
94
97
|
export { ForwardMessageModal } from './components/ForwardMessageModal';
|
|
95
98
|
export type { ForwardMessageModalProps, ForwardChannelItemProps } from './components/ForwardMessageModal';
|
|
96
99
|
|
|
100
|
+
export { TopicModal } from './components/TopicModal';
|
|
101
|
+
|
|
97
102
|
export { TypingIndicator } from './components/TypingIndicator';
|
|
98
103
|
export type { TypingIndicatorProps } from './components/TypingIndicator';
|
|
99
104
|
export { useTypingIndicator } from './hooks/useTypingIndicator';
|
|
@@ -74,6 +74,27 @@
|
|
|
74
74
|
margin: 0;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
.ermis-channel-info__parent-name {
|
|
78
|
+
font-size: 13px;
|
|
79
|
+
color: var(--ermis-accent);
|
|
80
|
+
margin-top: -4px;
|
|
81
|
+
margin-bottom: 8px;
|
|
82
|
+
font-weight: 500;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.ermis-channel-info__topic-emoji-avatar {
|
|
86
|
+
font-size: 48px;
|
|
87
|
+
line-height: 80px;
|
|
88
|
+
width: 80px;
|
|
89
|
+
height: 80px;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
background: var(--ermis-bg-secondary);
|
|
94
|
+
border-radius: 50%;
|
|
95
|
+
margin-bottom: 16px;
|
|
96
|
+
}
|
|
97
|
+
|
|
77
98
|
.ermis-channel-info__cover-edit-btn {
|
|
78
99
|
background: transparent;
|
|
79
100
|
border: none;
|
|
@@ -26,6 +26,29 @@
|
|
|
26
26
|
text-overflow: ellipsis;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
.ermis-channel-header__team-name {
|
|
30
|
+
font-size: var(--ermis-font-size-xs);
|
|
31
|
+
color: var(--ermis-text-secondary);
|
|
32
|
+
margin-bottom: 2px;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
text-overflow: ellipsis;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.ermis-channel-header__topic-avatar {
|
|
40
|
+
width: 32px;
|
|
41
|
+
height: 32px;
|
|
42
|
+
min-width: 32px;
|
|
43
|
+
border-radius: var(--ermis-radius-md);
|
|
44
|
+
background-color: var(--ermis-bg-primary);
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
font-size: 16px;
|
|
49
|
+
color: var(--ermis-text-secondary);
|
|
50
|
+
}
|
|
51
|
+
|
|
29
52
|
.ermis-channel-header__subtitle {
|
|
30
53
|
font-size: var(--ermis-font-size-xs);
|
|
31
54
|
color: var(--ermis-text-muted);
|
|
@@ -79,6 +102,7 @@
|
|
|
79
102
|
cursor: pointer;
|
|
80
103
|
border-left: 2px solid transparent;
|
|
81
104
|
transition: background-color var(--ermis-transition), border-color var(--ermis-transition);
|
|
105
|
+
position: relative;
|
|
82
106
|
}
|
|
83
107
|
|
|
84
108
|
.ermis-channel-list__item:hover {
|
|
@@ -97,6 +121,21 @@
|
|
|
97
121
|
flex: 1;
|
|
98
122
|
}
|
|
99
123
|
|
|
124
|
+
.ermis-channel-list__item-top-row {
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: baseline;
|
|
127
|
+
justify-content: space-between;
|
|
128
|
+
gap: var(--ermis-spacing-sm);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.ermis-channel-list__item-bottom-row {
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
justify-content: space-between;
|
|
135
|
+
gap: var(--ermis-spacing-sm);
|
|
136
|
+
margin-top: 2px;
|
|
137
|
+
}
|
|
138
|
+
|
|
100
139
|
.ermis-channel-list__item-name {
|
|
101
140
|
font-size: var(--ermis-font-size-sm);
|
|
102
141
|
font-weight: 500;
|
|
@@ -104,6 +143,15 @@
|
|
|
104
143
|
white-space: nowrap;
|
|
105
144
|
overflow: hidden;
|
|
106
145
|
text-overflow: ellipsis;
|
|
146
|
+
flex: 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.ermis-channel-list__item-timestamp {
|
|
150
|
+
font-size: var(--ermis-font-size-xs);
|
|
151
|
+
color: var(--ermis-text-muted);
|
|
152
|
+
white-space: nowrap;
|
|
153
|
+
flex-shrink: 0;
|
|
154
|
+
margin-top: 2px;
|
|
107
155
|
}
|
|
108
156
|
|
|
109
157
|
.ermis-channel-list__item-last-message {
|
|
@@ -112,13 +160,92 @@
|
|
|
112
160
|
white-space: nowrap;
|
|
113
161
|
overflow: hidden;
|
|
114
162
|
text-overflow: ellipsis;
|
|
115
|
-
|
|
163
|
+
flex: 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.ermis-channel-list__item-closed-indicator {
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
gap: 6px;
|
|
170
|
+
font-size: var(--ermis-font-size-xs);
|
|
171
|
+
color: var(--ermis-text-secondary);
|
|
172
|
+
font-weight: 500;
|
|
173
|
+
white-space: nowrap;
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
text-overflow: ellipsis;
|
|
176
|
+
flex: 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.ermis-channel-list__closed-icon {
|
|
180
|
+
display: inline-flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
justify-content: center;
|
|
183
|
+
flex-shrink: 0;
|
|
184
|
+
color: var(--ermis-color-danger);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.ermis-channel-list__pinned-icon {
|
|
188
|
+
position: absolute;
|
|
189
|
+
top: -5px;
|
|
190
|
+
right: 0px;
|
|
191
|
+
color: var(--ermis-color-danger, #ef4444);
|
|
192
|
+
display: inline-flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
justify-content: center;
|
|
195
|
+
transform: rotate(45deg);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.ermis-channel-list__item-badges {
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 4px;
|
|
202
|
+
flex-shrink: 0;
|
|
116
203
|
}
|
|
117
204
|
|
|
118
205
|
.ermis-channel-list__item-last-message-user {
|
|
119
206
|
color: var(--ermis-text-secondary);
|
|
120
207
|
}
|
|
121
208
|
|
|
209
|
+
.ermis-channel-list__item-actions-wrapper,
|
|
210
|
+
.ermis-channel-list__topic-actions-wrapper {
|
|
211
|
+
position: absolute;
|
|
212
|
+
right: var(--ermis-spacing-sm);
|
|
213
|
+
top: 50%;
|
|
214
|
+
transform: translateY(-50%);
|
|
215
|
+
opacity: 0;
|
|
216
|
+
transition: opacity var(--ermis-transition);
|
|
217
|
+
display: flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
z-index: 1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.ermis-channel-list__item:hover .ermis-channel-list__item-actions-wrapper,
|
|
223
|
+
.ermis-channel-list__topic-header:hover .ermis-channel-list__topic-actions-wrapper,
|
|
224
|
+
.ermis-channel-list__item-actions-wrapper:has(.ermis-channel-list__actions-trigger--active),
|
|
225
|
+
.ermis-channel-list__topic-actions-wrapper:has(.ermis-channel-list__actions-trigger--active) {
|
|
226
|
+
opacity: 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.ermis-channel-list__actions-trigger {
|
|
230
|
+
display: flex;
|
|
231
|
+
align-items: center;
|
|
232
|
+
justify-content: center;
|
|
233
|
+
width: 24px;
|
|
234
|
+
height: 24px;
|
|
235
|
+
border-radius: var(--ermis-radius-sm);
|
|
236
|
+
background-color: var(--ermis-bg-primary);
|
|
237
|
+
color: var(--ermis-text-muted);
|
|
238
|
+
border: none;
|
|
239
|
+
cursor: pointer;
|
|
240
|
+
transition: all var(--ermis-transition);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.ermis-channel-list__actions-trigger:hover,
|
|
244
|
+
.ermis-channel-list__actions-trigger--active {
|
|
245
|
+
background-color: var(--ermis-bg-primary);
|
|
246
|
+
color: var(--ermis-accent);
|
|
247
|
+
}
|
|
248
|
+
|
|
122
249
|
/* --- Unread channel indicator --- */
|
|
123
250
|
.ermis-channel-list__item--unread .ermis-channel-list__item-name {
|
|
124
251
|
font-weight: 700;
|
|
@@ -146,11 +273,6 @@
|
|
|
146
273
|
flex-shrink: 0;
|
|
147
274
|
}
|
|
148
275
|
|
|
149
|
-
/* --- Blocked channel indicator --- */
|
|
150
|
-
.ermis-channel-list__item--blocked {
|
|
151
|
-
opacity: 0.5;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
276
|
.ermis-channel-list__blocked-icon {
|
|
155
277
|
display: inline-flex;
|
|
156
278
|
align-items: center;
|
|
@@ -215,3 +337,92 @@
|
|
|
215
337
|
.ermis-channel-list__accordion-icon--expanded {
|
|
216
338
|
transform: rotate(0deg);
|
|
217
339
|
}
|
|
340
|
+
|
|
341
|
+
/* --- Topic Group --- */
|
|
342
|
+
.ermis-channel-list__topic-group {
|
|
343
|
+
display: flex;
|
|
344
|
+
flex-direction: column;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.ermis-channel-list__topic-header {
|
|
348
|
+
display: flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
gap: var(--ermis-spacing-md);
|
|
351
|
+
padding: var(--ermis-spacing-md) var(--ermis-spacing-lg);
|
|
352
|
+
cursor: pointer;
|
|
353
|
+
border-left: 2px solid transparent;
|
|
354
|
+
transition: background-color var(--ermis-transition);
|
|
355
|
+
position: relative;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.ermis-channel-list__topic-header:hover {
|
|
359
|
+
background-color: var(--ermis-bg-hover);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.ermis-channel-list__topic-header-name {
|
|
363
|
+
font-size: var(--ermis-font-size-sm);
|
|
364
|
+
font-weight: 600;
|
|
365
|
+
color: var(--ermis-text-primary);
|
|
366
|
+
white-space: nowrap;
|
|
367
|
+
overflow: hidden;
|
|
368
|
+
text-overflow: ellipsis;
|
|
369
|
+
flex: 1;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.ermis-channel-list__add-topic-btn {
|
|
373
|
+
display: flex;
|
|
374
|
+
align-items: center;
|
|
375
|
+
justify-content: center;
|
|
376
|
+
width: 24px;
|
|
377
|
+
height: 24px;
|
|
378
|
+
border-radius: var(--ermis-radius-sm);
|
|
379
|
+
background-color: transparent;
|
|
380
|
+
color: var(--ermis-text-muted);
|
|
381
|
+
border: none;
|
|
382
|
+
cursor: pointer;
|
|
383
|
+
transition: all var(--ermis-transition);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.ermis-channel-list__add-topic-btn:hover {
|
|
387
|
+
background-color: var(--ermis-bg-primary);
|
|
388
|
+
color: var(--ermis-accent);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.ermis-channel-list__topic-header--expanded .ermis-channel-list__accordion-icon {
|
|
392
|
+
transform: rotate(0deg);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.ermis-channel-list__topic-sublist {
|
|
396
|
+
display: flex;
|
|
397
|
+
flex-direction: column;
|
|
398
|
+
background-color: var(--ermis-bg-secondary);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* Indent nested items and provide hierarchical line */
|
|
402
|
+
.ermis-channel-list__topic-sublist .ermis-channel-list__item {
|
|
403
|
+
padding-left: var(--ermis-spacing-md);
|
|
404
|
+
margin-left: 20px;
|
|
405
|
+
border-left: 2px solid var(--ermis-border);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.ermis-channel-list__topic-sublist .ermis-channel-list__item:hover {
|
|
409
|
+
border-left-color: var(--ermis-border-hover);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.ermis-channel-list__topic-sublist .ermis-channel-list__item--active {
|
|
413
|
+
border-left-color: var(--ermis-accent);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.ermis-channel-list__topic-hashtag {
|
|
417
|
+
display: flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
justify-content: center;
|
|
420
|
+
width: 24px;
|
|
421
|
+
height: 24px;
|
|
422
|
+
background-color: transparent;
|
|
423
|
+
color: var(--ermis-text-muted);
|
|
424
|
+
border-radius: var(--ermis-radius-full);
|
|
425
|
+
font-size: 0.95rem;
|
|
426
|
+
font-weight: 600;
|
|
427
|
+
flex-shrink: 0;
|
|
428
|
+
}
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
align-items: flex-end;
|
|
191
191
|
padding: var(--ermis-spacing-sm) var(--ermis-spacing-md);
|
|
192
192
|
border-radius: var(--ermis-radius-lg);
|
|
193
|
-
width: 100%;
|
|
193
|
+
/* width: 100%; */
|
|
194
194
|
word-break: break-word;
|
|
195
195
|
}
|
|
196
196
|
|
|
@@ -323,6 +323,10 @@
|
|
|
323
323
|
width: 350px;
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
.ermis-message-list__item-content--has-attachments .ermis-message-bubble {
|
|
327
|
+
width: 100%;
|
|
328
|
+
}
|
|
329
|
+
|
|
326
330
|
/* Container for messages with both text + attachments */
|
|
327
331
|
.ermis-message-content--with-attachments {
|
|
328
332
|
display: flex;
|
|
@@ -365,11 +369,19 @@
|
|
|
365
369
|
height: 100%;
|
|
366
370
|
max-width: none;
|
|
367
371
|
max-height: 200px;
|
|
368
|
-
object-fit: cover;
|
|
369
372
|
border-radius: 0;
|
|
370
373
|
display: block;
|
|
371
374
|
}
|
|
372
375
|
|
|
376
|
+
.ermis-attachment-grid .ermis-attachment--image {
|
|
377
|
+
object-fit: cover;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.ermis-attachment-grid .ermis-attachment--video {
|
|
381
|
+
object-fit: contain;
|
|
382
|
+
background-color: var(--ermis-bg-hover);
|
|
383
|
+
}
|
|
384
|
+
|
|
373
385
|
/* Single media: larger height allowed */
|
|
374
386
|
.ermis-attachment-grid--single .ermis-attachment--image,
|
|
375
387
|
.ermis-attachment-grid--single .ermis-attachment--video {
|
|
@@ -392,6 +404,10 @@
|
|
|
392
404
|
background-color: var(--ermis-bg-hover);
|
|
393
405
|
}
|
|
394
406
|
|
|
407
|
+
.ermis-attachment-aspect-box--4-3 {
|
|
408
|
+
padding-bottom: 75%;
|
|
409
|
+
}
|
|
410
|
+
|
|
395
411
|
/* Blurred thumbnail preview (shown while full image loads) */
|
|
396
412
|
.ermis-attachment-blur-preview {
|
|
397
413
|
position: absolute;
|
|
@@ -427,14 +443,18 @@
|
|
|
427
443
|
}
|
|
428
444
|
}
|
|
429
445
|
|
|
430
|
-
|
|
431
|
-
|
|
446
|
+
.ermis-attachment--hidden-loader {
|
|
447
|
+
display: none;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* Full media — hidden until loaded, fades in over blur/shimmer */
|
|
451
|
+
.ermis-attachment-aspect-box .ermis-attachment--image,
|
|
452
|
+
.ermis-attachment-aspect-box .ermis-attachment--video {
|
|
432
453
|
position: absolute;
|
|
433
454
|
top: 0;
|
|
434
455
|
left: 0;
|
|
435
456
|
width: 100%;
|
|
436
457
|
height: 100%;
|
|
437
|
-
object-fit: cover;
|
|
438
458
|
opacity: 0;
|
|
439
459
|
transition: opacity 0.3s ease;
|
|
440
460
|
z-index: 2;
|
|
@@ -444,7 +464,17 @@
|
|
|
444
464
|
border-radius: 0;
|
|
445
465
|
}
|
|
446
466
|
|
|
447
|
-
.ermis-attachment-aspect-box .ermis-attachment--image
|
|
467
|
+
.ermis-attachment-aspect-box .ermis-attachment--image {
|
|
468
|
+
object-fit: cover;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.ermis-attachment-aspect-box .ermis-attachment--video {
|
|
472
|
+
object-fit: contain;
|
|
473
|
+
background-color: var(--ermis-bg-hover);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.ermis-attachment-aspect-box .ermis-attachment--image.ermis-attachment--loaded,
|
|
477
|
+
.ermis-attachment-aspect-box .ermis-attachment--video.ermis-attachment--loaded {
|
|
448
478
|
opacity: 1;
|
|
449
479
|
}
|
|
450
480
|
|
|
@@ -467,6 +497,7 @@
|
|
|
467
497
|
max-width: 300px;
|
|
468
498
|
max-height: 200px;
|
|
469
499
|
border-radius: var(--ermis-radius-md);
|
|
500
|
+
object-fit: contain;
|
|
470
501
|
}
|
|
471
502
|
|
|
472
503
|
.ermis-attachment--file {
|
|
@@ -557,10 +588,28 @@
|
|
|
557
588
|
border-color: var(--ermis-accent);
|
|
558
589
|
}
|
|
559
590
|
|
|
591
|
+
.ermis-attachment__link-image-wrapper {
|
|
592
|
+
position: relative;
|
|
593
|
+
width: 100%;
|
|
594
|
+
min-height: 120px;
|
|
595
|
+
background-color: var(--ermis-bg-hover);
|
|
596
|
+
overflow: hidden;
|
|
597
|
+
}
|
|
598
|
+
|
|
560
599
|
.ermis-attachment__link-image {
|
|
600
|
+
display: block;
|
|
561
601
|
width: 100%;
|
|
562
|
-
|
|
602
|
+
height: 100%;
|
|
563
603
|
object-fit: cover;
|
|
604
|
+
position: absolute;
|
|
605
|
+
top: 0;
|
|
606
|
+
left: 0;
|
|
607
|
+
opacity: 0;
|
|
608
|
+
transition: opacity 0.3s ease;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.ermis-attachment__link-image.ermis-attachment--loaded {
|
|
612
|
+
opacity: 1;
|
|
564
613
|
}
|
|
565
614
|
|
|
566
615
|
.ermis-attachment__link-info {
|
|
@@ -612,9 +661,26 @@
|
|
|
612
661
|
}
|
|
613
662
|
|
|
614
663
|
/* --- Sticker message --- */
|
|
664
|
+
.ermis-message-sticker-wrapper {
|
|
665
|
+
position: relative;
|
|
666
|
+
width: 120px;
|
|
667
|
+
height: 120px;
|
|
668
|
+
overflow: hidden;
|
|
669
|
+
}
|
|
670
|
+
|
|
615
671
|
.ermis-message-sticker {
|
|
616
|
-
|
|
617
|
-
|
|
672
|
+
position: absolute;
|
|
673
|
+
top: 0;
|
|
674
|
+
left: 0;
|
|
675
|
+
width: 100%;
|
|
676
|
+
height: 100%;
|
|
677
|
+
object-fit: contain;
|
|
678
|
+
opacity: 0;
|
|
679
|
+
transition: opacity 0.3s ease;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.ermis-message-sticker.ermis-attachment--loaded {
|
|
683
|
+
opacity: 1;
|
|
618
684
|
}
|
|
619
685
|
|
|
620
686
|
/* --- Error message --- */
|