@ermis-network/ermis-chat-react 1.0.8 → 2.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.
Files changed (99) hide show
  1. package/dist/index.cjs +15295 -4209
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +701 -195
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.mts +862 -94
  6. package/dist/index.d.ts +862 -94
  7. package/dist/index.mjs +15246 -4186
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +9 -4
  10. package/src/channelTypeUtils.ts +1 -1
  11. package/src/components/Avatar.tsx +2 -1
  12. package/src/components/Channel.tsx +6 -2
  13. package/src/components/ChannelActions.tsx +61 -2
  14. package/src/components/ChannelHeader.tsx +19 -5
  15. package/src/components/ChannelInfo/AddMemberModal.tsx +5 -1
  16. package/src/components/ChannelInfo/ChannelInfo.tsx +330 -187
  17. package/src/components/ChannelInfo/ChannelInfoTabs.tsx +59 -297
  18. package/src/components/ChannelInfo/ChannelSettingsPanel.tsx +30 -174
  19. package/src/components/ChannelInfo/EditChannelModal.tsx +4 -1
  20. package/src/components/ChannelInfo/MediaGridItem.tsx +12 -2
  21. package/src/components/ChannelInfo/MemberListItem.tsx +2 -3
  22. package/src/components/ChannelInfo/MessageSearchPanel.tsx +27 -126
  23. package/src/components/ChannelInfo/States.tsx +1 -1
  24. package/src/components/ChannelInfo/index.ts +3 -0
  25. package/src/components/ChannelInfo/useChannelInfoTabs.tsx +386 -0
  26. package/src/components/ChannelInfo/useChannelSettings.ts +212 -0
  27. package/src/components/ChannelInfo/useMessageSearch.tsx +141 -0
  28. package/src/components/ChannelList.tsx +177 -290
  29. package/src/components/CreateChannelModal.tsx +166 -88
  30. package/src/components/Dropdown.tsx +1 -16
  31. package/src/components/EditPreview.tsx +1 -0
  32. package/src/components/ErmisCallProvider.tsx +72 -17
  33. package/src/components/ErmisCallUI.tsx +43 -20
  34. package/src/components/FlatTopicGroupItem.tsx +232 -0
  35. package/src/components/ForwardMessageModal.tsx +31 -77
  36. package/src/components/MediaLightbox.tsx +62 -40
  37. package/src/components/MentionSuggestions.tsx +47 -35
  38. package/src/components/MessageActionsBox.tsx +4 -1
  39. package/src/components/MessageInput.tsx +137 -16
  40. package/src/components/MessageInputDefaults.tsx +127 -1
  41. package/src/components/MessageItem.tsx +93 -26
  42. package/src/components/MessageQuickReactions.tsx +153 -26
  43. package/src/components/MessageReactions.tsx +2 -1
  44. package/src/components/MessageRenderers.tsx +111 -39
  45. package/src/components/Panel.tsx +1 -14
  46. package/src/components/PinnedMessages.tsx +17 -5
  47. package/src/components/PreviewOverlay.tsx +24 -0
  48. package/src/components/ReadReceipts.tsx +2 -1
  49. package/src/components/TopicList.tsx +221 -0
  50. package/src/components/TopicModal.tsx +4 -1
  51. package/src/components/TypingIndicator.tsx +14 -5
  52. package/src/components/UserPicker.tsx +87 -10
  53. package/src/components/VirtualMessageList.tsx +106 -20
  54. package/src/context/ChatComponentsContext.tsx +14 -0
  55. package/src/context/ChatProvider.tsx +18 -14
  56. package/src/context/ErmisCallContext.tsx +4 -0
  57. package/src/hooks/useChannelCapabilities.ts +7 -4
  58. package/src/hooks/useChannelData.ts +10 -3
  59. package/src/hooks/useChannelListUpdates.ts +72 -20
  60. package/src/hooks/useChannelMessages.ts +72 -10
  61. package/src/hooks/useChannelRowUpdates.ts +24 -5
  62. package/src/hooks/useChatUser.ts +31 -0
  63. package/src/hooks/useContactChannels.ts +45 -0
  64. package/src/hooks/useContactCount.ts +50 -0
  65. package/src/hooks/useDownloadHandler.ts +36 -0
  66. package/src/hooks/useDragAndDrop.ts +79 -0
  67. package/src/hooks/useForwardMessage.ts +112 -0
  68. package/src/hooks/useInviteChannels.ts +88 -0
  69. package/src/hooks/useInviteCount.ts +104 -0
  70. package/src/hooks/useMentions.ts +0 -1
  71. package/src/hooks/useMessageActions.ts +13 -10
  72. package/src/hooks/usePendingState.ts +21 -4
  73. package/src/hooks/usePreviewState.ts +69 -0
  74. package/src/hooks/useStickerPicker.ts +62 -0
  75. package/src/hooks/useTopicGroupUpdates.ts +197 -0
  76. package/src/index.ts +56 -6
  77. package/src/messageTypeUtils.ts +13 -1
  78. package/src/styles/_base.css +0 -1
  79. package/src/styles/_call-ui.css +59 -2
  80. package/src/styles/_channel-info.css +41 -4
  81. package/src/styles/_channel-list.css +97 -57
  82. package/src/styles/_create-channel-modal.css +10 -0
  83. package/src/styles/_forward-modal.css +16 -1
  84. package/src/styles/_media-lightbox.css +32 -0
  85. package/src/styles/_mentions.css +1 -1
  86. package/src/styles/_message-actions.css +3 -4
  87. package/src/styles/_message-bubble.css +286 -107
  88. package/src/styles/_message-input.css +131 -0
  89. package/src/styles/_message-list.css +33 -17
  90. package/src/styles/_message-quick-reactions.css +40 -9
  91. package/src/styles/_message-reactions.css +4 -0
  92. package/src/styles/_modal.css +2 -1
  93. package/src/styles/_preview-overlay.css +38 -0
  94. package/src/styles/_tokens.css +17 -15
  95. package/src/styles/_typing-indicator.css +7 -1
  96. package/src/styles/index.css +1 -0
  97. package/src/types.ts +362 -14
  98. package/src/utils/avatarColors.ts +48 -0
  99. package/src/utils.ts +193 -10
@@ -0,0 +1,197 @@
1
+ import React, { useState, useEffect, useMemo, useCallback } from 'react';
2
+ import type { Channel } from '@ermis-network/ermis-chat-sdk';
3
+ import { isPendingMember, isSkippedMember } from '../channelRoleUtils';
4
+ import { isDirectChannel } from '../channelTypeUtils';
5
+ import { getLastMessagePreview } from '../utils';
6
+ import { SystemMessageTranslations, SignalMessageTranslations } from '@ermis-network/ermis-chat-sdk';
7
+
8
+ /** Preview data for the most recent message across the topic group */
9
+ export type LatestMessagePreview = {
10
+ text: React.ReactNode;
11
+ user: string;
12
+ timestamp?: string | Date;
13
+ /** Topic name if the message came from a sub-topic, null if from general/parent */
14
+ sourceName: string | null;
15
+ };
16
+
17
+ export type TopicGroupUpdatesOptions = {
18
+ deletedMessageLabel?: React.ReactNode;
19
+ stickerMessageLabel?: React.ReactNode;
20
+ photoMessageLabel?: React.ReactNode;
21
+ videoMessageLabel?: React.ReactNode;
22
+ voiceRecordingMessageLabel?: React.ReactNode;
23
+ fileMessageLabel?: React.ReactNode;
24
+ systemMessageTranslations?: SystemMessageTranslations;
25
+ signalMessageTranslations?: SignalMessageTranslations;
26
+ };
27
+
28
+ /**
29
+ * Hook encapsulating realtime logic for a topic-enabled channel group.
30
+ *
31
+ * Subscribes to message and pin events on the parent channel AND all its
32
+ * topics to compute:
33
+ * - sorted topics list (pinned first, then by last activity)
34
+ * - aggregated unread count across parent + all topics
35
+ * - boolean flag indicating if any unread exists
36
+ * - latest message preview across parent + all topics
37
+ */
38
+ export function useTopicGroupUpdates(
39
+ channel: Channel,
40
+ currentUserId?: string,
41
+ options?: TopicGroupUpdatesOptions
42
+ ): {
43
+ topics: Channel[];
44
+ aggregatedUnreadCount: number;
45
+ hasUnread: boolean;
46
+ updateCount: number;
47
+ latestMessagePreview: LatestMessagePreview | null;
48
+ } {
49
+ const [updateCount, setUpdateCount] = useState(0);
50
+ const bump = useCallback(() => setUpdateCount((c) => c + 1), []);
51
+
52
+ // Subscribe to realtime events on parent + all topics
53
+ useEffect(() => {
54
+ const subs: { unsubscribe: () => void }[] = [];
55
+
56
+ // Parent channel events
57
+ subs.push(channel.on('message.new', bump));
58
+ subs.push(channel.on('message.read', bump));
59
+ subs.push(channel.on('message.deleted', bump));
60
+ subs.push(channel.on('channel.updated', bump));
61
+ subs.push(channel.on('channel.topic.created', bump));
62
+ subs.push(channel.on('channel.pinned', bump));
63
+ subs.push(channel.on('channel.unpinned', bump));
64
+
65
+ // Topic children events
66
+ const currentTopics = channel.state?.topics || [];
67
+ currentTopics.forEach((t: Channel) => {
68
+ subs.push(t.on('message.new', bump));
69
+ subs.push(t.on('message.read', bump));
70
+ subs.push(t.on('message.deleted', bump));
71
+ subs.push(t.on('channel.updated', bump));
72
+ subs.push(t.on('channel.deleted', bump));
73
+ subs.push(t.on('channel.pinned', bump));
74
+ subs.push(t.on('channel.unpinned', bump));
75
+ });
76
+
77
+ return () => {
78
+ subs.forEach((s) => s.unsubscribe());
79
+ };
80
+ }, [channel, channel.state?.topics, channel.state?.topics?.length, bump]);
81
+
82
+ // Helper: get sort timestamp for a channel/topic
83
+ const getTopicTime = (t: Channel): number => {
84
+ const lastMsg = t.state?.latestMessages?.slice(-1)[0];
85
+ if (lastMsg?.created_at) return new Date(lastMsg.created_at).getTime();
86
+ if (t.data?.last_message_at) return new Date(t.data.last_message_at as string | Date).getTime();
87
+ if (t.data?.created_at) return new Date(t.data.created_at as string | Date).getTime();
88
+ return 0;
89
+ };
90
+
91
+ // Helper: check if user is excluded from unread counting
92
+ const isExcludedUser = (ch: Channel): boolean => {
93
+ const ms = ch.state?.membership as Record<string, unknown> | undefined;
94
+ if (!ms) return false;
95
+ const isBannedSelf = Boolean(ms.banned);
96
+
97
+ // Topic support: check parent channel's ban status
98
+ const parentCid = ch.data?.parent_cid as string | undefined;
99
+ const parentChannel = parentCid ? ch.getClient().activeChannels[parentCid] : undefined;
100
+ const isBannedParent = Boolean(parentChannel?.state?.membership?.banned);
101
+
102
+ const isBanned = isBannedSelf || isBannedParent;
103
+ const isBlocked = isDirectChannel(ch) && Boolean(ms.blocked);
104
+ const isPending = isPendingMember(ms.channel_role as string);
105
+ const isSkipped = isSkippedMember(ms.channel_role as string);
106
+ return isBanned || isBlocked || isPending || isSkipped;
107
+ };
108
+
109
+ // Helper: get unread count for a channel (reads from SDK state directly)
110
+ const getUnreadCount = (ch: Channel): number => {
111
+ if (!currentUserId || isExcludedUser(ch)) return 0;
112
+ // Primary: use the SDK's tracked unreadCount
113
+ const state = ch.state as unknown as Record<string, unknown> | undefined;
114
+ const count = (state?.unreadCount as number) ?? 0;
115
+ return count;
116
+ };
117
+
118
+ // Sort topics: pinned first → last activity descending
119
+ const topics = useMemo(() => {
120
+ const allTopics = channel.state?.topics || [];
121
+ return [...allTopics].sort((a: Channel, b: Channel) => {
122
+ const aPinned = a.data?.is_pinned === true;
123
+ const bPinned = b.data?.is_pinned === true;
124
+ if (aPinned && !bPinned) return -1;
125
+ if (!aPinned && bPinned) return 1;
126
+ return getTopicTime(b) - getTopicTime(a);
127
+ });
128
+ // eslint-disable-next-line react-hooks/exhaustive-deps
129
+ }, [channel.state?.topics, updateCount]);
130
+
131
+ // Aggregated unread count across parent + all topics
132
+ const aggregatedUnreadCount = useMemo(() => {
133
+ let total = getUnreadCount(channel);
134
+
135
+ const allTopics = channel.state?.topics || [];
136
+ allTopics.forEach((topic: Channel) => {
137
+ total += getUnreadCount(topic);
138
+ });
139
+
140
+ return total;
141
+ // eslint-disable-next-line react-hooks/exhaustive-deps
142
+ }, [channel, channel.state?.topics, currentUserId, updateCount]);
143
+
144
+ const hasUnread = aggregatedUnreadCount > 0;
145
+
146
+ // Latest message preview across parent + all topics (Option B: prefix topic name)
147
+ const latestMessagePreview = useMemo((): LatestMessagePreview | null => {
148
+ // If banned from the main group, hide previews for all sub-items
149
+ if (isExcludedUser(channel)) return null;
150
+
151
+ const allChannels = [channel, ...(channel.state?.topics || [])];
152
+
153
+ let bestTime = 0;
154
+ let bestChannel: Channel | null = null;
155
+
156
+ for (const ch of allChannels) {
157
+ const time = getTopicTime(ch);
158
+ if (time > bestTime) {
159
+ bestTime = time;
160
+ bestChannel = ch;
161
+ }
162
+ }
163
+
164
+ if (!bestChannel) return null;
165
+
166
+ const preview = getLastMessagePreview(bestChannel, currentUserId, options);
167
+ if (!preview.text && !preview.user) return null;
168
+
169
+ // sourceName is non-null only when the message comes from a sub-topic (not the parent/general)
170
+ const isFromSubTopic = bestChannel !== channel;
171
+ const sourceName = isFromSubTopic ? (bestChannel.data?.name as string || null) : null;
172
+
173
+ return {
174
+ text: preview.text,
175
+ user: preview.user,
176
+ timestamp: preview.timestamp,
177
+ sourceName,
178
+ };
179
+ // eslint-disable-next-line react-hooks/exhaustive-deps
180
+ }, [
181
+ channel,
182
+ channel.state?.topics,
183
+ currentUserId,
184
+ updateCount,
185
+ options?.deletedMessageLabel,
186
+ options?.stickerMessageLabel,
187
+ options?.photoMessageLabel,
188
+ options?.videoMessageLabel,
189
+ options?.voiceRecordingMessageLabel,
190
+ options?.fileMessageLabel,
191
+ options?.systemMessageTranslations,
192
+ options?.signalMessageTranslations,
193
+ ]);
194
+
195
+ return { topics, aggregatedUnreadCount, hasUnread, updateCount, latestMessagePreview };
196
+ }
197
+
package/src/index.ts CHANGED
@@ -7,6 +7,11 @@ export type { ChatProviderProps, ChatContextValue, Theme } from './context/ChatP
7
7
 
8
8
  // Hooks
9
9
  export { useChatClient } from './hooks/useChatClient';
10
+ export { useChatUser } from './hooks/useChatUser';
11
+ export { useInviteChannels } from './hooks/useInviteChannels';
12
+ export { useContactChannels } from './hooks/useContactChannels';
13
+ export { useInviteCount } from './hooks/useInviteCount';
14
+ export { useContactCount } from './hooks/useContactCount';
10
15
  export { useChannel } from './hooks/useChannel';
11
16
  export type { UseChannelReturn } from './hooks/useChannel';
12
17
  export { useChannelListUpdates } from './hooks/useChannelListUpdates';
@@ -17,14 +22,29 @@ export { useOnlineStatus } from './hooks/useOnlineStatus';
17
22
  export type { OnlineStatus } from './hooks/useOnlineStatus';
18
23
  export { useOnlineUsers } from './hooks/useOnlineUsers';
19
24
  export { usePendingState } from './hooks/usePendingState';
25
+ export { usePreviewState } from './hooks/usePreviewState';
26
+ export { useTopicGroupUpdates } from './hooks/useTopicGroupUpdates';
27
+ export { useDragAndDrop } from './hooks/useDragAndDrop';
28
+ export { useMessageSend } from './hooks/useMessageSend';
29
+ export { useFileUpload } from './hooks/useFileUpload';
30
+ export { useEmojiPicker } from './hooks/useEmojiPicker';
31
+ export { useStickerPicker } from './hooks/useStickerPicker';
32
+ export type { UseStickerPickerOptions } from './hooks/useStickerPicker';
33
+ export { useChannelCapabilities } from './hooks/useChannelCapabilities';
34
+ export { useChannelMembers, useChannelProfile } from './hooks/useChannelData';
20
35
 
21
36
  // Components
22
37
  export { Avatar } from './components/Avatar';
23
38
  export type { AvatarProps } from './components/Avatar';
24
39
 
25
- export { ChannelList, ChannelItem, ChannelTopicGroup } from './components/ChannelList';
40
+ export { ChannelList, ChannelItem, ChannelRow, DefaultPinnedIcon } from './components/ChannelList';
26
41
  export type { ChannelListProps, ChannelItemProps } from './components/ChannelList';
27
42
 
43
+ export { FlatTopicGroupItem } from './components/FlatTopicGroupItem';
44
+ export type { TopicPillProps, TopicListProps } from './types';
45
+
46
+ export { TopicList } from './components/TopicList';
47
+
28
48
  export { DefaultChannelActions, computeDefaultActions } from './components/ChannelActions';
29
49
  export type { ChannelAction, ChannelActionsProps } from './types';
30
50
 
@@ -54,7 +74,7 @@ export { MessageActionsBox } from './components/MessageActionsBox';
54
74
  export type { MessageActionsBoxProps } from './types';
55
75
 
56
76
  export { Dropdown, closeAllDropdowns } from './components/Dropdown';
57
- export type { DropdownProps } from './components/Dropdown';
77
+ export type { DropdownProps } from './types';
58
78
 
59
79
  export { MessageReactions } from './components/MessageReactions';
60
80
  export type { MessageReactionsProps, ReactionUser, LatestReaction } from './types';
@@ -63,7 +83,19 @@ export { MessageQuickReactions } from './components/MessageQuickReactions';
63
83
 
64
84
  export { useMessageActions } from './hooks/useMessageActions';
65
85
 
66
- export { formatTime, getDateKey, formatDateLabel, getMessageUserId, replaceMentionsForPreview } from './utils';
86
+ export {
87
+ formatTime,
88
+ getDateKey,
89
+ formatDateLabel,
90
+ getMessageUserId,
91
+ replaceMentionsForPreview,
92
+ getLastMessagePreview,
93
+ buildUserMap,
94
+ removeAccents,
95
+ formatRelativeDate,
96
+ countWords,
97
+ } from './utils';
98
+ export { getAvatarGradient } from './utils/avatarColors';
67
99
  export {
68
100
  isGroupChannel,
69
101
  isDirectChannel,
@@ -100,8 +132,10 @@ export {
100
132
  isLinkPreviewAttachment,
101
133
  isImage,
102
134
  isVideo,
135
+ MESSAGE_DISPLAY_TYPES,
136
+ isDeletedDisplayMessage,
103
137
  } from './messageTypeUtils';
104
- export type { MessageType, AttachmentType } from './messageTypeUtils';
138
+ export type { MessageType, AttachmentType, MessageDisplayType } from './messageTypeUtils';
105
139
 
106
140
  export {
107
141
  defaultMessageRenderers,
@@ -134,7 +168,10 @@ export type { FilePreviewItem, FilesPreviewProps } from './components/FilesPrevi
134
168
  export { MentionSuggestions } from './components/MentionSuggestions';
135
169
  export type { MentionSuggestionsProps } from './components/MentionSuggestions';
136
170
 
137
- export { useMentions } from './hooks/useMentions';
171
+ export { EditPreview } from './components/EditPreview';
172
+ export { PreviewOverlay } from './components/PreviewOverlay';
173
+
174
+ export { useMentions, getMentionHtml } from './hooks/useMentions';
138
175
  export type { MentionMember, MentionPayload, UseMentionsOptions, UseMentionsReturn } from './hooks/useMentions';
139
176
 
140
177
  export { useScrollToMessage } from './hooks/useScrollToMessage';
@@ -143,9 +180,11 @@ export type { UseScrollToMessageOptions, UseScrollToMessageReturn } from './hook
143
180
  export { useLoadMessages, dedupMessages } from './hooks/useLoadMessages';
144
181
  export type { UseLoadMessagesOptions, UseLoadMessagesReturn } from './hooks/useLoadMessages';
145
182
 
146
- export { useChannelMessages } from './hooks/useChannelMessages';
183
+ export { useChannelMessages, markChannelAsFullyQueried } from './hooks/useChannelMessages';
147
184
  export type { UseChannelMessagesOptions } from './hooks/useChannelMessages';
148
185
 
186
+ export { useForwardMessage } from './hooks/useForwardMessage';
187
+
149
188
  export { QuotedMessagePreview } from './components/QuotedMessagePreview';
150
189
  export type { QuotedMessagePreviewProps } from './components/QuotedMessagePreview';
151
190
  export { ReplyPreview } from './components/ReplyPreview';
@@ -167,6 +206,11 @@ export {
167
206
  DefaultChannelInfoCover,
168
207
  DefaultChannelInfoActions,
169
208
  DefaultChannelInfoTabs,
209
+ MessageSearchPanel,
210
+ HighlightedText,
211
+ useMessageSearch,
212
+ ChannelSettingsPanel,
213
+ useChannelSettings,
170
214
  } from './components/ChannelInfo';
171
215
 
172
216
  export { Modal } from './components/Modal';
@@ -177,6 +221,7 @@ export type {
177
221
  ChannelInfoCoverProps,
178
222
  ChannelInfoActionsProps,
179
223
  ChannelInfoTabsProps,
224
+ ChannelInfoTabHeaderProps,
180
225
  ChannelInfoMemberItemProps,
181
226
  ChannelInfoMediaItemProps,
182
227
  ChannelInfoLinkItemProps,
@@ -188,6 +233,11 @@ export type {
188
233
  AddMemberModalProps,
189
234
  AddMemberUserItemProps,
190
235
  AddMemberButtonProps,
236
+ EditChannelModalProps,
237
+ EditChannelData,
238
+ TopicModalProps,
239
+ MessageSearchPanelProps,
240
+ ChannelSettingsPanelProps,
191
241
  } from './types';
192
242
 
193
243
  export { UserPicker } from './components/UserPicker';
@@ -24,7 +24,7 @@ export function isSystemMessage(message: any): boolean {
24
24
  }
25
25
 
26
26
  export function isStickerMessage(message: any): boolean {
27
- return message?.type === MESSAGE_TYPES.STICKER;
27
+ return message?.type === MESSAGE_TYPES.STICKER || Boolean(message?.sticker_url);
28
28
  }
29
29
 
30
30
  export function isRegularMessage(message: any): boolean {
@@ -62,3 +62,15 @@ export function isImage(attachment: any): boolean {
62
62
  export function isVideo(attachment: any): boolean {
63
63
  return !!(isVideoAttachment(attachment) || (!attachment.type && attachment.mime_type?.startsWith('video/')));
64
64
  }
65
+
66
+ export const MESSAGE_DISPLAY_TYPES = {
67
+ NORMAL: 'normal',
68
+ DELETED: 'deleted',
69
+ } as const;
70
+
71
+ export type MessageDisplayType = (typeof MESSAGE_DISPLAY_TYPES)[keyof typeof MESSAGE_DISPLAY_TYPES] | string;
72
+
73
+ /** Check if a message was deleted for current user (display_type === 'deleted') */
74
+ export function isDeletedDisplayMessage(message: any): boolean {
75
+ return message?.display_type === MESSAGE_DISPLAY_TYPES.DELETED;
76
+ }
@@ -23,7 +23,6 @@
23
23
  }
24
24
 
25
25
  .ermis-avatar--fallback {
26
- background: linear-gradient(135deg, var(--ermis-accent) 0%, var(--ermis-accent-hover) 100%);
27
26
  color: #fff;
28
27
  font-weight: 600;
29
28
  font-family: var(--ermis-font-family);
@@ -187,6 +187,12 @@
187
187
  transform: scale(0.95);
188
188
  }
189
189
 
190
+ .ermis-call-ui__action-circle:disabled {
191
+ opacity: 0.7;
192
+ cursor: not-allowed;
193
+ transform: none;
194
+ }
195
+
190
196
  .ermis-call-ui__action-circle--reject {
191
197
  background-color: var(--ermis-color-danger);
192
198
  box-shadow: 0 4px 20px rgba(239, 68, 68, 0.35);
@@ -242,7 +248,7 @@
242
248
  .ermis-call-ui__video-remote {
243
249
  width: 100%;
244
250
  height: 100%;
245
- object-fit: cover;
251
+ object-fit: contain;
246
252
  }
247
253
 
248
254
  .ermis-call-ui__video-local {
@@ -268,7 +274,7 @@
268
274
  .ermis-call-ui__video-local-stream {
269
275
  width: 100%;
270
276
  height: 100%;
271
- object-fit: cover;
277
+ object-fit: contain;
272
278
  transform: scaleX(-1);
273
279
  }
274
280
 
@@ -325,6 +331,39 @@
325
331
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.6));
326
332
  }
327
333
 
334
+ /* Video call status bar: mic-muted icon + duration timer in one row */
335
+ .ermis-call-ui__video-timer {
336
+ position: absolute;
337
+ top: 16px;
338
+ left: 16px;
339
+ z-index: 15;
340
+ display: flex;
341
+ align-items: center;
342
+ gap: 6px;
343
+ padding: 4px 12px;
344
+ border-radius: var(--ermis-radius-full);
345
+ background: rgba(0, 0, 0, 0.45);
346
+ backdrop-filter: blur(12px);
347
+ -webkit-backdrop-filter: blur(12px);
348
+ border: 1px solid rgba(255, 255, 255, 0.1);
349
+ color: rgba(255, 255, 255, 0.85);
350
+ font-size: var(--ermis-font-size-sm);
351
+ font-variant-numeric: tabular-nums;
352
+ font-weight: 500;
353
+ user-select: none;
354
+ }
355
+
356
+ .ermis-call-ui__video-timer-mic {
357
+ display: flex;
358
+ align-items: center;
359
+ color: #f87171; /* red-400 */
360
+ }
361
+
362
+ .ermis-call-ui__video-timer-mic svg {
363
+ width: 16px;
364
+ height: 16px;
365
+ }
366
+
328
367
  /* -- Audio Layout -- */
329
368
  .ermis-call-ui__audio-container {
330
369
  text-align: center;
@@ -741,3 +780,21 @@
741
780
  transform: none;
742
781
  }
743
782
  }
783
+
784
+ /* ============================================================
785
+ SPINNER
786
+ ============================================================ */
787
+ .ermis-call-ui__spinner {
788
+ width: 20px;
789
+ height: 20px;
790
+ border: 2px solid rgba(255, 255, 255, 0.3);
791
+ border-radius: 50%;
792
+ border-top-color: #ffffff;
793
+ animation: ermis-call-spin 0.8s linear infinite;
794
+ }
795
+
796
+ @keyframes ermis-call-spin {
797
+ to {
798
+ transform: rotate(360deg);
799
+ }
800
+ }
@@ -6,9 +6,17 @@
6
6
  height: 100%;
7
7
  background: var(--ermis-bg-primary);
8
8
  border-left: 1px solid var(--ermis-border-base);
9
+ display: flex;
10
+ flex-direction: column;
11
+ overflow: hidden;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ .ermis-channel-info__body {
16
+ flex: 1;
9
17
  overflow-y: auto;
10
18
  overflow-x: hidden;
11
- box-sizing: border-box;
19
+ scroll-behavior: smooth;
12
20
  }
13
21
 
14
22
  .ermis-channel-info__header {
@@ -323,7 +331,6 @@
323
331
  ============================================ */
324
332
 
325
333
  .ermis-channel-info__media-section {
326
- flex: 1;
327
334
  display: flex;
328
335
  flex-direction: column;
329
336
  padding: 0;
@@ -336,6 +343,10 @@
336
343
  padding: 0 4px;
337
344
  gap: 0;
338
345
  flex-shrink: 0;
346
+ position: sticky;
347
+ top: 0;
348
+ z-index: 10;
349
+ background-color: var(--ermis-bg-primary);
339
350
  }
340
351
 
341
352
  .ermis-channel-info__media-tab {
@@ -393,8 +404,6 @@
393
404
  ============================================ */
394
405
 
395
406
  .ermis-channel-info__media-content {
396
- flex: 1;
397
- overflow: hidden;
398
407
  min-height: 120px;
399
408
  }
400
409
 
@@ -960,3 +969,31 @@
960
969
  font-weight: 500;
961
970
  color: var(--ermis-text-secondary);
962
971
  }
972
+
973
+ .ermis-channel-info__preview-actions {
974
+ display: flex;
975
+ justify-content: center;
976
+ padding: 12px 16px;
977
+ border-bottom: 1px solid var(--ermis-border);
978
+ }
979
+
980
+ .ermis-channel-info__join-btn {
981
+ width: 100%;
982
+ justify-content: center;
983
+ font-weight: 600;
984
+ padding: 10px 16px;
985
+ border-radius: var(--ermis-radius-lg);
986
+ background-color: var(--ermis-accent);
987
+ color: #ffffff;
988
+ border: none;
989
+ cursor: pointer;
990
+ transition: opacity 0.2s ease, transform 0.1s ease;
991
+ }
992
+
993
+ .ermis-channel-info__join-btn:hover {
994
+ opacity: 0.9;
995
+ }
996
+
997
+ .ermis-channel-info__join-btn:active {
998
+ transform: scale(0.98);
999
+ }