@ermis-network/ermis-chat-react 1.0.2 → 1.0.3
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 +2112 -1279
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +880 -165
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +219 -2
- package/dist/index.d.ts +219 -2
- package/dist/index.mjs +2023 -1194
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/ChannelHeader.tsx +50 -9
- package/src/components/ChannelList.tsx +4 -3
- package/src/components/ErmisCallProvider.tsx +279 -0
- package/src/components/ErmisCallUI.tsx +634 -0
- package/src/components/MessageRenderers.tsx +37 -10
- package/src/components/Modal.tsx +2 -1
- package/src/context/ChatProvider.tsx +49 -1
- package/src/context/ErmisCallContext.tsx +37 -0
- package/src/hooks/useCallContext.ts +10 -0
- package/src/index.ts +16 -0
- package/src/styles/_add-member-modal.css +12 -29
- package/src/styles/_call-ui.css +743 -0
- package/src/styles/_channel-info.css +34 -34
- package/src/styles/_channel-list.css +7 -7
- package/src/styles/_create-channel-modal.css +15 -15
- package/src/styles/_message-bubble.css +108 -16
- package/src/styles/_message-input.css +4 -4
- package/src/styles/_message-list.css +11 -11
- package/src/styles/_modal.css +23 -36
- package/src/styles/_panel.css +1 -1
- package/src/styles/_search-panel.css +9 -9
- package/src/styles/_tokens.css +42 -0
- package/src/styles/_typing-indicator.css +15 -2
- package/src/styles/_user-picker.css +16 -16
- package/src/styles/index.css +1 -1
- package/src/types.ts +193 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState, useMemo } from 'react';
|
|
2
2
|
import { preloadImage, isImagePreloaded } from '../utils';
|
|
3
3
|
import type { FormatMessageResponse, Attachment, MessageLabel } from '@ermis-network/ermis-chat-sdk';
|
|
4
|
-
import { parseSystemMessage, parseSignalMessage } from '@ermis-network/ermis-chat-sdk';
|
|
4
|
+
import { parseSystemMessage, parseSignalMessage, CallType } from '@ermis-network/ermis-chat-sdk';
|
|
5
5
|
import { useChatClient } from '../hooks/useChatClient';
|
|
6
6
|
import { buildUserMap } from '../utils';
|
|
7
7
|
import type { AttachmentProps, MessageRendererProps, MessageBubbleProps } from '../types';
|
|
@@ -482,19 +482,46 @@ export const SystemMessage: React.FC<MessageRendererProps> = ({ message }) => {
|
|
|
482
482
|
|
|
483
483
|
/** Signal message: call events */
|
|
484
484
|
export const SignalMessage: React.FC<MessageRendererProps> = ({ message }) => {
|
|
485
|
-
const {
|
|
486
|
-
|
|
487
|
-
const userMap = useMemo<Record<string, string>>(() => {
|
|
488
|
-
return buildUserMap(activeChannel?.state);
|
|
489
|
-
}, [activeChannel?.state]);
|
|
485
|
+
const { client } = useChatClient();
|
|
490
486
|
|
|
491
487
|
const rawText = message.text ?? '';
|
|
492
|
-
const
|
|
488
|
+
const result = rawText ? parseSignalMessage(rawText, client.userID || '') : null;
|
|
489
|
+
|
|
490
|
+
if (!result) {
|
|
491
|
+
return (
|
|
492
|
+
<span className="ermis-message-list__signal-text">
|
|
493
|
+
{rawText}
|
|
494
|
+
</span>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const isSuccess = !!result.duration;
|
|
499
|
+
const colorModifier = isSuccess ? 'success' : 'missed';
|
|
500
|
+
const isAudio = result.callType === CallType.AUDIO;
|
|
493
501
|
|
|
494
502
|
return (
|
|
495
|
-
<
|
|
496
|
-
{
|
|
497
|
-
|
|
503
|
+
<div className="ermis-signal-message">
|
|
504
|
+
<div className={`ermis-signal-message__icon ermis-signal-message__icon--${colorModifier}`}>
|
|
505
|
+
{isAudio ? (
|
|
506
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
507
|
+
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
|
|
508
|
+
</svg>
|
|
509
|
+
) : (
|
|
510
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
511
|
+
<polygon points="23 7 16 12 23 17 23 7" />
|
|
512
|
+
<rect x="1" y="5" width="15" height="14" rx="2" ry="2" />
|
|
513
|
+
</svg>
|
|
514
|
+
)}
|
|
515
|
+
</div>
|
|
516
|
+
<div className="ermis-signal-message__body">
|
|
517
|
+
<span className={`ermis-signal-message__text ermis-signal-message__text--${colorModifier}`}>
|
|
518
|
+
{result.text}
|
|
519
|
+
</span>
|
|
520
|
+
{result.duration && (
|
|
521
|
+
<span className="ermis-signal-message__duration">{result.duration}</span>
|
|
522
|
+
)}
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
498
525
|
);
|
|
499
526
|
};
|
|
500
527
|
|
package/src/components/Modal.tsx
CHANGED
|
@@ -9,6 +9,7 @@ export const Modal: React.FC<ModalProps> = ({
|
|
|
9
9
|
footer,
|
|
10
10
|
maxWidth = '480px',
|
|
11
11
|
hideCloseButton = false,
|
|
12
|
+
closeOnOutsideClick = true,
|
|
12
13
|
}) => {
|
|
13
14
|
useEffect(() => {
|
|
14
15
|
const handleKey = (e: KeyboardEvent) => {
|
|
@@ -21,7 +22,7 @@ export const Modal: React.FC<ModalProps> = ({
|
|
|
21
22
|
if (!isOpen) return null;
|
|
22
23
|
|
|
23
24
|
return (
|
|
24
|
-
<div className="ermis-modal-overlay" onClick={onClose}>
|
|
25
|
+
<div className="ermis-modal-overlay" onClick={closeOnOutsideClick ? onClose : undefined}>
|
|
25
26
|
<div
|
|
26
27
|
className="ermis-modal-content"
|
|
27
28
|
style={{ maxWidth }}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import React, { createContext, useState, useCallback } from 'react';
|
|
2
2
|
import type { Channel, FormatMessageResponse } from '@ermis-network/ermis-chat-sdk';
|
|
3
3
|
import type { Theme, ChatContextValue, ChatProviderProps, ReadStateEntry } from '../types';
|
|
4
|
+
import { ErmisCallProvider } from '../components/ErmisCallProvider';
|
|
5
|
+
import { ErmisCallUI } from '../components/ErmisCallUI';
|
|
4
6
|
|
|
5
7
|
export type { Theme, ChatContextValue, ChatProviderProps } from '../types';
|
|
6
8
|
|
|
@@ -10,6 +12,19 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
10
12
|
client,
|
|
11
13
|
children,
|
|
12
14
|
initialTheme = 'light',
|
|
15
|
+
enableCall = false,
|
|
16
|
+
callSessionId,
|
|
17
|
+
callWasmPath,
|
|
18
|
+
callRelayUrl,
|
|
19
|
+
CallUIComponent,
|
|
20
|
+
incomingCallAudioPath,
|
|
21
|
+
outgoingCallAudioPath,
|
|
22
|
+
onCallStart,
|
|
23
|
+
onCallEnd,
|
|
24
|
+
onCallError,
|
|
25
|
+
onIncomingCall,
|
|
26
|
+
onCallAccepted,
|
|
27
|
+
onCallRejected,
|
|
13
28
|
}) => {
|
|
14
29
|
const [activeChannelRaw, setActiveChannelRaw] = useState<Channel | null>(null);
|
|
15
30
|
const [theme, setTheme] = useState<Theme>(initialTheme);
|
|
@@ -61,13 +76,46 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
61
76
|
setForwardingMessage,
|
|
62
77
|
jumpToMessageId,
|
|
63
78
|
setJumpToMessageId,
|
|
79
|
+
enableCall,
|
|
64
80
|
};
|
|
65
81
|
|
|
66
|
-
|
|
82
|
+
const CallUIView = CallUIComponent ? <CallUIComponent /> : (
|
|
83
|
+
<ErmisCallUI
|
|
84
|
+
incomingCallAudioPath={incomingCallAudioPath}
|
|
85
|
+
outgoingCallAudioPath={outgoingCallAudioPath}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const content = (
|
|
67
90
|
<ChatContext.Provider value={value}>
|
|
68
91
|
<div className={`ermis-chat ermis-chat--${theme}`}>
|
|
69
92
|
{children}
|
|
93
|
+
{enableCall && CallUIView}
|
|
70
94
|
</div>
|
|
71
95
|
</ChatContext.Provider>
|
|
72
96
|
);
|
|
97
|
+
|
|
98
|
+
if (enableCall) {
|
|
99
|
+
if (!callSessionId) {
|
|
100
|
+
console.warn('ErmisChat React: enableCall is true but callSessionId is missing.');
|
|
101
|
+
}
|
|
102
|
+
return (
|
|
103
|
+
<ErmisCallProvider
|
|
104
|
+
client={client}
|
|
105
|
+
sessionId={callSessionId || ''}
|
|
106
|
+
wasmPath={callWasmPath}
|
|
107
|
+
relayUrl={callRelayUrl}
|
|
108
|
+
onCallStart={onCallStart}
|
|
109
|
+
onCallEnd={onCallEnd}
|
|
110
|
+
onCallError={onCallError}
|
|
111
|
+
onIncomingCall={onIncomingCall}
|
|
112
|
+
onCallAccepted={onCallAccepted}
|
|
113
|
+
onCallRejected={onCallRejected}
|
|
114
|
+
>
|
|
115
|
+
{content}
|
|
116
|
+
</ErmisCallProvider>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return content;
|
|
73
121
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { CallStatus, type ErmisCallNode, type UserCallInfo } from '@ermis-network/ermis-chat-sdk';
|
|
3
|
+
|
|
4
|
+
export type CallContextValue = {
|
|
5
|
+
callNode: ErmisCallNode | null;
|
|
6
|
+
callStatus: CallStatus | '';
|
|
7
|
+
localStream: MediaStream | null;
|
|
8
|
+
remoteStream: MediaStream | null;
|
|
9
|
+
callType: string;
|
|
10
|
+
callerInfo?: UserCallInfo | undefined;
|
|
11
|
+
receiverInfo?: UserCallInfo | undefined;
|
|
12
|
+
isIncoming: boolean;
|
|
13
|
+
createCall: (type: 'audio' | 'video', cid: string) => Promise<void>;
|
|
14
|
+
acceptCall: () => Promise<void>;
|
|
15
|
+
rejectCall: () => Promise<void>;
|
|
16
|
+
endCall: () => Promise<void>;
|
|
17
|
+
toggleMic: () => void;
|
|
18
|
+
toggleVideo: () => void;
|
|
19
|
+
isMicMuted: boolean;
|
|
20
|
+
isVideoMuted: boolean;
|
|
21
|
+
audioDevices: MediaDeviceInfo[];
|
|
22
|
+
videoDevices: MediaDeviceInfo[];
|
|
23
|
+
selectedAudioDeviceId: string;
|
|
24
|
+
selectedVideoDeviceId: string;
|
|
25
|
+
isScreenSharing: boolean;
|
|
26
|
+
errorMessage: string | null;
|
|
27
|
+
toggleScreenShare: () => Promise<void>;
|
|
28
|
+
switchAudioDevice: (id: string) => Promise<void>;
|
|
29
|
+
switchVideoDevice: (id: string) => Promise<void>;
|
|
30
|
+
clearError: () => void;
|
|
31
|
+
isRemoteMicMuted: boolean;
|
|
32
|
+
isRemoteVideoMuted: boolean;
|
|
33
|
+
upgradeCall: () => Promise<void>;
|
|
34
|
+
callDuration: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const ErmisCallContext = React.createContext<CallContextValue | undefined>(undefined);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { ErmisCallContext } from '../context/ErmisCallContext';
|
|
3
|
+
|
|
4
|
+
export const useCallContext = () => {
|
|
5
|
+
const context = useContext(ErmisCallContext);
|
|
6
|
+
if (context === undefined) {
|
|
7
|
+
throw new Error('useCallContext must be used within an ErmisCallProvider');
|
|
8
|
+
}
|
|
9
|
+
return context;
|
|
10
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -138,3 +138,19 @@ export type {
|
|
|
138
138
|
|
|
139
139
|
export { CreateChannelModal } from './components/CreateChannelModal';
|
|
140
140
|
export type { CreateChannelModalProps } from './types';
|
|
141
|
+
|
|
142
|
+
// Call Components
|
|
143
|
+
export { ErmisCallContext } from './context/ErmisCallContext';
|
|
144
|
+
export type { CallContextValue } from './context/ErmisCallContext';
|
|
145
|
+
export { useCallContext } from './hooks/useCallContext';
|
|
146
|
+
export { ErmisCallProvider } from './components/ErmisCallProvider';
|
|
147
|
+
export type { ErmisCallProviderProps } from './components/ErmisCallProvider';
|
|
148
|
+
export { ErmisCallUI } from './components/ErmisCallUI';
|
|
149
|
+
export type {
|
|
150
|
+
ErmisCallUIProps,
|
|
151
|
+
ErmisCallRingingProps,
|
|
152
|
+
ErmisCallConnectedAudioProps,
|
|
153
|
+
ErmisCallConnectedVideoProps,
|
|
154
|
+
ErmisCallErrorProps,
|
|
155
|
+
ErmisCallControlsBarProps,
|
|
156
|
+
} from './types';
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
.ermis-modal-search svg {
|
|
9
9
|
position: absolute;
|
|
10
10
|
left: 12px;
|
|
11
|
-
color: var(--ermis-text-secondary
|
|
11
|
+
color: var(--ermis-text-secondary);
|
|
12
12
|
pointer-events: none;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -16,23 +16,17 @@
|
|
|
16
16
|
width: 100%;
|
|
17
17
|
padding: 10px 12px 10px 36px;
|
|
18
18
|
border-radius: 8px;
|
|
19
|
-
border: 1px solid var(--ermis-border
|
|
20
|
-
background-color: var(--ermis-bg-
|
|
21
|
-
color: var(--ermis-text-primary
|
|
19
|
+
border: 1px solid var(--ermis-border);
|
|
20
|
+
background-color: var(--ermis-bg-secondary);
|
|
21
|
+
color: var(--ermis-text-primary);
|
|
22
22
|
font-size: 14px;
|
|
23
23
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
24
24
|
outline: none;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
[data-theme='dark'] .ermis-modal-search input {
|
|
28
|
-
background-color: var(--ermis-bg-base, #121212);
|
|
29
|
-
border: 1px solid var(--ermis-border-color, #444444);
|
|
30
|
-
color: var(--ermis-text-primary, #ffffff);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
27
|
.ermis-modal-search input:focus {
|
|
34
|
-
border-color: var(--ermis-
|
|
35
|
-
box-shadow: 0 0 0 2px rgba(
|
|
28
|
+
border-color: var(--ermis-accent);
|
|
29
|
+
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
|
|
36
30
|
}
|
|
37
31
|
|
|
38
32
|
.ermis-modal-user-list {
|
|
@@ -52,17 +46,14 @@
|
|
|
52
46
|
background: transparent;
|
|
53
47
|
}
|
|
54
48
|
.ermis-modal-user-list::-webkit-scrollbar-thumb {
|
|
55
|
-
background-color: var(--ermis-border
|
|
49
|
+
background-color: var(--ermis-border);
|
|
56
50
|
border-radius: 3px;
|
|
57
51
|
}
|
|
58
|
-
[data-theme='dark'] .ermis-modal-user-list::-webkit-scrollbar-thumb {
|
|
59
|
-
background-color: var(--ermis-border-color, #444444);
|
|
60
|
-
}
|
|
61
52
|
|
|
62
53
|
.ermis-modal-loading,
|
|
63
54
|
.ermis-modal-empty {
|
|
64
55
|
text-align: center;
|
|
65
|
-
color: var(--ermis-text-secondary
|
|
56
|
+
color: var(--ermis-text-secondary);
|
|
66
57
|
padding: 32px 0;
|
|
67
58
|
font-size: 14px;
|
|
68
59
|
}
|
|
@@ -76,11 +67,7 @@
|
|
|
76
67
|
}
|
|
77
68
|
|
|
78
69
|
.ermis-modal-user-item:hover {
|
|
79
|
-
background-color: var(--ermis-bg-hover
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
[data-theme='dark'] .ermis-modal-user-item:hover {
|
|
83
|
-
background-color: var(--ermis-bg-hover, #2a2a2a);
|
|
70
|
+
background-color: var(--ermis-bg-hover);
|
|
84
71
|
}
|
|
85
72
|
|
|
86
73
|
.ermis-modal-user-info {
|
|
@@ -93,15 +80,11 @@
|
|
|
93
80
|
.ermis-modal-user-name {
|
|
94
81
|
font-size: 14px;
|
|
95
82
|
font-weight: 500;
|
|
96
|
-
color: var(--ermis-text-primary
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
[data-theme='dark'] .ermis-modal-user-name {
|
|
100
|
-
color: var(--ermis-text-primary, #ffffff);
|
|
83
|
+
color: var(--ermis-text-primary);
|
|
101
84
|
}
|
|
102
85
|
|
|
103
86
|
.ermis-modal-add-btn {
|
|
104
|
-
background-color: var(--ermis-
|
|
87
|
+
background-color: var(--ermis-accent);
|
|
105
88
|
color: #ffffff;
|
|
106
89
|
border: none;
|
|
107
90
|
border-radius: 6px;
|
|
@@ -113,7 +96,7 @@
|
|
|
113
96
|
}
|
|
114
97
|
|
|
115
98
|
.ermis-modal-add-btn:hover:not(:disabled) {
|
|
116
|
-
background-color: var(--ermis-
|
|
99
|
+
background-color: var(--ermis-accent-hover);
|
|
117
100
|
}
|
|
118
101
|
|
|
119
102
|
.ermis-modal-add-btn:disabled {
|