@ermis-network/ermis-chat-react 1.0.2 → 1.0.4

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.
@@ -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 { activeChannel } = useChatClient();
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 parsedText = rawText ? parseSignalMessage(rawText, userMap) : '';
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
- <span className="ermis-message-list__signal-text">
496
- {parsedText || rawText}
497
- </span>
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
 
@@ -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
- return (
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, #666666);
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-color, #cccccc);
20
- background-color: var(--ermis-bg-base, #f9f9f9);
21
- color: var(--ermis-text-primary, #000000);
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-primary-color, #005fff);
35
- box-shadow: 0 0 0 2px rgba(0, 95, 255, 0.2);
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-color, #cccccc);
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, #666666);
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, #f0f0f0);
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, #000000);
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-primary-color, #005fff);
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-primary-hover, #004ecc);
99
+ background-color: var(--ermis-accent-hover);
117
100
  }
118
101
 
119
102
  .ermis-modal-add-btn:disabled {