@trtc/calls-uikit-react 4.2.5 → 4.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trtc/calls-uikit-react",
3
- "version": "4.2.5",
3
+ "version": "4.4.2",
4
4
  "main": "./tuicall-uikit-react.umd.js",
5
5
  "module": "./tuicall-uikit-react.es.js",
6
6
  "types": "./types/index.d.ts",
@@ -14,8 +14,8 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@tencentcloud/tui-core-lite": "1.0.0",
17
- "@trtc/call-engine-lite-js": "~3.4.7",
18
- "@tencentcloud/lite-chat": "^1.5.0",
17
+ "@trtc/call-engine-lite-js": "~3.5.0",
18
+ "@tencentcloud/lite-chat": "^1.6.3",
19
19
  "@trtc/call-engine-lite-wx": "~3.4.7"
20
20
  },
21
21
  "bugs": {
@@ -0,0 +1,250 @@
1
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import ReactDOM from 'react-dom/client';
4
+ import type { ReactNode } from 'react';
5
+ import { uikitModalState } from './UIKitModalState';
6
+ import { IS_PC } from '../../../../TUICallService/utils/env';
7
+ import { t } from '../../../../TUICallService';
8
+ import styles from './index.module.scss';
9
+
10
+ // Types
11
+ export interface UIKitModalConfig {
12
+ id: number;
13
+ title: string;
14
+ content: string | ReactNode;
15
+ type: 'info' | 'warning' | 'error' | 'success';
16
+ onConfirm?: () => void;
17
+ onCancel?: () => void;
18
+ }
19
+
20
+ export type UIKitModalOptions = UIKitModalConfig;
21
+
22
+ // Constants
23
+ let root: ReactDOM.Root | null = null;
24
+ let container: HTMLDivElement | null = null;
25
+ let resolvePromise: ((value: { action: string }) => void) | null = null;
26
+
27
+ const URL_REGEX = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/i;
28
+ const FULL_URL_REGEX = /(https?:\/\/[^\s]+)/g;
29
+
30
+ function escapeHtml(text: string): string {
31
+ const div = document.createElement('div');
32
+ div.textContent = text;
33
+ return div.innerHTML;
34
+ }
35
+
36
+ function processAnchorTags(content: string): string {
37
+ const parser = new DOMParser();
38
+ const doc = parser.parseFromString(content, 'text/html');
39
+ const anchors = doc.querySelectorAll('a');
40
+ anchors.forEach((anchor) => {
41
+ const href = anchor.getAttribute('href');
42
+ if (href) {
43
+ anchor.setAttribute('target', '_blank');
44
+ anchor.setAttribute('rel', 'noopener noreferrer');
45
+ anchor.classList.add(styles['uikit-modal-link']);
46
+ const originalText = anchor.textContent || '';
47
+ anchor.innerHTML = `<span style="padding: 0 0.2em; display: inline-block; color: var(--text-color-link)">${originalText}</span>`;
48
+ }
49
+ });
50
+ return doc.body.innerHTML;
51
+ }
52
+
53
+ const TypeIcons: Record<string, () => JSX.Element> = {
54
+ success: () => <span style={{ color: 'green', marginRight: '6px' }}>✓</span>,
55
+ info: () => <span style={{ color: '#1890ff', marginRight: '6px' }}>ℹ</span>,
56
+ error: () => <span style={{ color: 'red', marginRight: '6px' }}>✕</span>,
57
+ warning: () => <span style={{ color: '#faad14', marginRight: '6px' }}>⚠</span>,
58
+ };
59
+
60
+ const isLocalEnvironment = (): boolean => {
61
+ const { hostname: host, href } = window.location;
62
+ if (host === 'localhost' || host === '127.0.0.1' || host.startsWith('192.168.')) {
63
+ return true;
64
+ }
65
+ if (href.startsWith('https://web.sdk.qcloud.com/trtc/livekit')) {
66
+ return true;
67
+ }
68
+ return false;
69
+ };
70
+
71
+ const cleanup = () => {
72
+ if (root) {
73
+ root.unmount();
74
+ root = null;
75
+ }
76
+ if (container && container.parentNode) {
77
+ container.parentNode.removeChild(container);
78
+ container = null;
79
+ }
80
+ resolvePromise = null;
81
+ };
82
+
83
+ interface UIKitModalComponentProps extends UIKitModalOptions {
84
+ visible: boolean;
85
+ }
86
+
87
+ export function UIKitModalComponent(props: UIKitModalComponentProps) {
88
+ const {
89
+ type = 'info',
90
+ title = '',
91
+ content = '',
92
+ visible = false,
93
+ onConfirm,
94
+ onCancel,
95
+ } = props;
96
+
97
+ const cancelText = t('reject');
98
+ const confirmText = t('accept');
99
+
100
+ const modalContainerClass = IS_PC
101
+ ? `${styles['uikit-modal-default']} ${styles['uikit-modal-container']}`
102
+ : `${styles['uikit-modal-default']} ${styles['uikit-modal-container']} ${styles['uikit-modal-container-mobile']}`;
103
+
104
+ const processedContent = useMemo(() => {
105
+ if (!content || typeof content !== 'string') {
106
+ return content;
107
+ }
108
+
109
+ if (URL_REGEX.test(content.trim())) {
110
+ const url = content.trim();
111
+ const href = url.startsWith('http') ? url : `https://${url}`;
112
+ return `<a href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer" class="${styles['uikit-modal-link']}">${escapeHtml(url)}</a>`;
113
+ }
114
+
115
+ if (content.includes('<a ') || content.includes('<a>')) {
116
+ return processAnchorTags(content);
117
+ }
118
+
119
+ if (FULL_URL_REGEX.test(content)) {
120
+ return escapeHtml(content).replace(FULL_URL_REGEX, url => `<a href="${url}" target="_blank" rel="noopener noreferrer" class="${styles['uikit-modal-link']}">${url}</a>`);
121
+ }
122
+
123
+ return escapeHtml(content);
124
+ }, [content]);
125
+
126
+ const TypeIcon = TypeIcons[type] || TypeIcons.info;
127
+
128
+ if (!visible) {
129
+ return null;
130
+ }
131
+
132
+ const modalContent = (
133
+ <div
134
+ className={styles['uikit-modal-mask']}
135
+ onClick={onConfirm}
136
+ >
137
+ <div
138
+ className={modalContainerClass}
139
+ onClick={e => e.stopPropagation()}
140
+ >
141
+ <div className={styles['uikit-modal-header']}>
142
+ <TypeIcon />
143
+ <span className={styles['uikit-modal-title']}>{title}</span>
144
+ </div>
145
+ <div
146
+ className={styles['uikit-modal-body']}
147
+ dangerouslySetInnerHTML={typeof processedContent === 'string' ? { __html: processedContent } : undefined}
148
+ >
149
+ {typeof processedContent !== 'string' ? processedContent : null}
150
+ </div>
151
+ <div className={styles['uikit-modal-footer']}>
152
+ <button
153
+ className={`${styles['uikit-modal-btn']} ${styles['uikit-modal-btn-cancel']}`}
154
+ onClick={onCancel}
155
+ >
156
+ {cancelText || 'Cancel'}
157
+ </button>
158
+ <button
159
+ className={`${styles['uikit-modal-btn']} ${styles['uikit-modal-btn-confirm']}`}
160
+ onClick={onConfirm}
161
+ >
162
+ {confirmText || 'Confirm'}
163
+ </button>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ );
168
+
169
+ return createPortal(modalContent, document.body);
170
+ }
171
+
172
+ interface UIKitModalWrapperProps {
173
+ options: UIKitModalOptions;
174
+ }
175
+
176
+ function UIKitModalWrapper(props: UIKitModalWrapperProps) {
177
+ const { options } = props;
178
+ const [visible, setVisible] = useState(true);
179
+
180
+ const handleAction = useCallback((action: 'confirm' | 'cancel') => {
181
+ const { closeModal } = uikitModalState.getState();
182
+ setVisible(false);
183
+ closeModal(action);
184
+ if (resolvePromise) {
185
+ resolvePromise({ action });
186
+ }
187
+ setTimeout(cleanup, 100);
188
+ }, []);
189
+
190
+ const handleConfirm = useCallback(() => {
191
+ options.onConfirm?.();
192
+ handleAction('confirm');
193
+ }, [options, handleAction]);
194
+
195
+ const handleCancel = useCallback(() => {
196
+ options.onCancel?.();
197
+ handleAction('cancel');
198
+ }, [options, handleAction]);
199
+
200
+ useEffect(() => {
201
+ const handleKeyDown = (e: KeyboardEvent) => {
202
+ if (e.key === 'Escape') {
203
+ handleCancel();
204
+ }
205
+ };
206
+ document.addEventListener('keydown', handleKeyDown);
207
+ return () => document.removeEventListener('keydown', handleKeyDown);
208
+ }, [handleCancel]);
209
+
210
+ return (
211
+ <UIKitModalComponent
212
+ {...options}
213
+ visible={visible}
214
+ onConfirm={handleConfirm}
215
+ onCancel={handleCancel}
216
+ />
217
+ );
218
+ }
219
+
220
+ const createUIKitModal = (options: UIKitModalOptions): Promise<{ action: string }> => new Promise((resolve) => {
221
+ cleanup();
222
+
223
+ const { openModal } = uikitModalState.getState();
224
+
225
+ openModal({
226
+ id: options.id,
227
+ title: options.title,
228
+ content: options.content,
229
+ type: options.type,
230
+ ...(options.onConfirm && { onConfirm: options.onConfirm }),
231
+ ...(options.onCancel && { onCancel: options.onCancel }),
232
+ });
233
+
234
+ if (!isLocalEnvironment()) {
235
+ resolve({ action: 'confirm' });
236
+ return;
237
+ }
238
+
239
+ resolvePromise = resolve;
240
+
241
+ container = document.createElement('div');
242
+ container.id = 'uikit-modal-root';
243
+ document.body.appendChild(container);
244
+ root = ReactDOM.createRoot(container);
245
+ root.render(React.createElement(UIKitModalWrapper, { options }));
246
+ });
247
+
248
+ export const UIKitModal = {
249
+ openModal: (config: UIKitModalOptions) => createUIKitModal(config),
250
+ };
@@ -0,0 +1,177 @@
1
+ import type { ReactNode } from 'react';
2
+ import { TUICallKitAPI } from '../../../../TUICallService/index';
3
+
4
+ // Types
5
+ export interface UIKitModalConfig {
6
+ id: number;
7
+ title: string;
8
+ content: string | ReactNode;
9
+ type: 'info' | 'warning' | 'error' | 'success';
10
+ onConfirm?: () => void;
11
+ onCancel?: () => void;
12
+ }
13
+
14
+ export type UIKitModalOptions = UIKitModalConfig;
15
+
16
+ interface AnalyticsData {
17
+ eventType: number;
18
+ modalId: number;
19
+ title: string;
20
+ type: string;
21
+ content: string | ReactNode;
22
+ timestamp: number;
23
+ stayDuration?: number;
24
+ closeReasonCode?: number;
25
+ sessionId?: string;
26
+ platformCode?: number;
27
+ userAgent?: string;
28
+ }
29
+
30
+ interface UIKitModalState {
31
+ modalData: UIKitModalConfig | null;
32
+ activeModalId: number | null;
33
+ modalOpenTime: number | null;
34
+ }
35
+
36
+ interface UIKitModalActions {
37
+ openModal: (config: UIKitModalConfig) => void;
38
+ closeModal: (action?: 'confirm' | 'cancel' | 'mask' | 'timeout') => void;
39
+ getState: () => UIKitModalState & UIKitModalActions;
40
+ }
41
+
42
+ function getPlatformInfo() {
43
+ const ua = navigator.userAgent;
44
+ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
45
+ return {
46
+ userAgent: ua,
47
+ platformCode: isMobile ? 2 : 1,
48
+ };
49
+ }
50
+
51
+ function getSessionId(): string {
52
+ let sessionId = sessionStorage.getItem('uikit_session_id');
53
+ if (!sessionId) {
54
+ sessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
55
+ sessionStorage.setItem('uikit_session_id', sessionId);
56
+ }
57
+ return sessionId;
58
+ }
59
+
60
+ function reportAnalytics(data: AnalyticsData) {
61
+ try {
62
+ // IM report
63
+ const tim = TUICallKitAPI.getTim();
64
+ if (tim && typeof tim.callExperimentalAPI === 'function') {
65
+ const reportData = {
66
+ id: data.modalId,
67
+ title: data.title,
68
+ type: data.type,
69
+ content: data.content,
70
+ };
71
+ tim.callExperimentalAPI('reportModalView', JSON.stringify(reportData));
72
+ }
73
+ } catch (_error) {
74
+ console.info('[UIKitModal]', data);
75
+ }
76
+ }
77
+
78
+ const createUIKitModalStore = () => {
79
+ let state: UIKitModalState = {
80
+ modalData: null,
81
+ activeModalId: null,
82
+ modalOpenTime: null,
83
+ };
84
+
85
+ const listeners: Set<() => void> = new Set();
86
+
87
+ const notify = () => {
88
+ listeners.forEach(listener => listener());
89
+ };
90
+
91
+ const openModal = (config: UIKitModalConfig) => {
92
+ const now = Date.now();
93
+ const { userAgent, platformCode } = getPlatformInfo();
94
+
95
+ state = {
96
+ modalData: config,
97
+ activeModalId: config.id,
98
+ modalOpenTime: now,
99
+ };
100
+
101
+ reportAnalytics({
102
+ eventType: 1,
103
+ modalId: config.id,
104
+ title: config.title,
105
+ type: config.type,
106
+ content: config.content,
107
+ timestamp: now,
108
+ sessionId: getSessionId(),
109
+ platformCode,
110
+ userAgent,
111
+ });
112
+
113
+ notify();
114
+ };
115
+
116
+ const closeModal = (action: 'confirm' | 'cancel' | 'mask' | 'timeout' = 'confirm') => {
117
+ const { activeModalId, modalOpenTime, modalData } = state;
118
+
119
+ if (activeModalId === null || !modalOpenTime || !modalData) {
120
+ return;
121
+ }
122
+
123
+ const now = Date.now();
124
+ const stayDuration = now - modalOpenTime;
125
+ const { userAgent, platformCode } = getPlatformInfo();
126
+
127
+ const closeReasonCodeMap: Record<string, number> = {
128
+ confirm: 1,
129
+ cancel: 2,
130
+ timeout: 3,
131
+ mask: 2,
132
+ };
133
+
134
+ reportAnalytics({
135
+ eventType: 2,
136
+ modalId: activeModalId,
137
+ title: modalData.title,
138
+ type: modalData.type,
139
+ content: modalData.content,
140
+ timestamp: now,
141
+ stayDuration,
142
+ closeReasonCode: closeReasonCodeMap[action],
143
+ sessionId: getSessionId(),
144
+ platformCode,
145
+ userAgent,
146
+ });
147
+
148
+ state = {
149
+ modalData: null,
150
+ activeModalId: null,
151
+ modalOpenTime: null,
152
+ };
153
+
154
+ notify();
155
+ };
156
+
157
+ const getState = (): UIKitModalState & UIKitModalActions => ({
158
+ ...state,
159
+ openModal,
160
+ closeModal,
161
+ getState,
162
+ });
163
+
164
+ return {
165
+ getState,
166
+ openModal,
167
+ closeModal,
168
+ subscribe: (listener: () => void) => {
169
+ listeners.add(listener);
170
+ return () => listeners.delete(listener);
171
+ },
172
+ };
173
+ };
174
+
175
+ export const uikitModalState = createUIKitModalStore();
176
+
177
+ export default uikitModalState;
@@ -0,0 +1,176 @@
1
+ .uikit-modal-mask {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ width: 100vw;
6
+ height: 100vh;
7
+ background: rgba(0, 0, 0, 0.6);
8
+ z-index: 9999;
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ }
13
+
14
+ .uikit-modal-default {
15
+ width: 480px;
16
+ max-width: calc(100% - 40px);
17
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
18
+ }
19
+
20
+ .uikit-modal-container {
21
+ box-sizing: border-box;
22
+ background: #fff;
23
+ position: relative;
24
+ display: flex;
25
+ flex-direction: column;
26
+ overflow: hidden;
27
+ padding: 24px;
28
+ border-radius: 20px;
29
+ }
30
+
31
+ .uikit-modal-header {
32
+ display: flex;
33
+ justify-content: space-between;
34
+ align-items: center;
35
+ padding-bottom: 20px;
36
+ }
37
+
38
+ .uikit-modal-type-icon {
39
+ margin-right: 6px;
40
+ }
41
+
42
+ .uikit-modal-close-icon {
43
+ cursor: pointer;
44
+ color: #333;
45
+ }
46
+
47
+ .uikit-modal-title {
48
+ font-size: 16px;
49
+ line-height: 24px;
50
+ font-weight: 600;
51
+ color: #333;
52
+ flex: 1;
53
+ }
54
+
55
+ .uikit-modal-body {
56
+ flex: 1;
57
+ display: block;
58
+ font-size: 14px;
59
+ font-weight: 400;
60
+ line-height: 22px;
61
+ color: #333;
62
+ word-break: break-word;
63
+ }
64
+
65
+ .uikit-modal-link {
66
+ color: #006eff;
67
+ text-decoration: none;
68
+ transition: all 0.2s ease;
69
+ cursor: pointer;
70
+ border-bottom: 1px solid transparent;
71
+ margin: 0 0.2em;
72
+ }
73
+
74
+ .uikit-modal-footer {
75
+ padding-top: 20px;
76
+ box-sizing: border-box;
77
+ display: flex;
78
+ justify-content: flex-end;
79
+ gap: 12px;
80
+ }
81
+
82
+ .uikit-modal-btn {
83
+ min-width: 88px;
84
+ height: 32px;
85
+ padding: 0 16px;
86
+ border-radius: 20px;
87
+ font-size: 14px;
88
+ font-weight: 500;
89
+ cursor: pointer;
90
+ transition: all 0.2s ease;
91
+ border: none;
92
+ outline: none;
93
+ white-space: nowrap;
94
+
95
+ &:active {
96
+ transform: scale(0.98);
97
+ }
98
+ }
99
+
100
+ .uikit-modal-btn-cancel {
101
+ background: #f0f2f9;
102
+ color: #333;
103
+
104
+ &:hover {
105
+ background: #e5e7ed;
106
+ }
107
+ }
108
+
109
+ .uikit-modal-btn-confirm {
110
+ background: #1c66e5;
111
+ color: #fff;
112
+
113
+ &:hover {
114
+ background: #1557cc;
115
+ }
116
+ }
117
+
118
+ .uikit-modal-header:empty,
119
+ .uikit-modal-body:empty,
120
+ .uikit-modal-footer:empty {
121
+ padding: 0;
122
+ }
123
+
124
+ .uikit-modal-container-mobile {
125
+ border-radius: 12px;
126
+ max-width: calc(100% - 32px);
127
+ padding: 0;
128
+
129
+ .uikit-modal-header {
130
+ padding: 20px 20px 16px;
131
+ justify-content: flex-start;
132
+ }
133
+
134
+ .uikit-modal-body {
135
+ padding: 0 20px 20px;
136
+ justify-content: flex-start;
137
+ }
138
+
139
+ .uikit-modal-footer {
140
+ padding: 0;
141
+ border-top: 0.5px solid #e5e5e5;
142
+ gap: 0;
143
+
144
+ .uikit-modal-btn {
145
+ flex: 1;
146
+ border-radius: 0;
147
+ height: 56px;
148
+ min-width: auto;
149
+ background: #fff;
150
+ font-size: 17px;
151
+
152
+ &:active {
153
+ transform: none;
154
+ background: #f5f5f5;
155
+ }
156
+
157
+ &:hover {
158
+ background: #fff;
159
+ }
160
+ }
161
+
162
+ .uikit-modal-btn-cancel {
163
+ color: #666;
164
+
165
+ &:first-child:last-child {
166
+ border-radius: 0 0 12px 12px;
167
+ }
168
+ }
169
+
170
+ .uikit-modal-btn-confirm {
171
+ color: #1c66e5;
172
+ border-left: 0.5px solid #e5e5e5;
173
+ font-weight: 600;
174
+ }
175
+ }
176
+ }
@@ -0,0 +1,3 @@
1
+ export { UIKitModal, UIKitModalComponent } from './UIKitModal';
2
+ export type { UIKitModalConfig, UIKitModalOptions } from './UIKitModal';
3
+ export { uikitModalState } from './UIKitModalState';
@@ -0,0 +1,81 @@
1
+ import TuiStore from '../TUIStore/tuiStore';
2
+ import { StoreName } from '../const/call';
3
+ import { NAME } from '../const/index';
4
+ import { t } from '../locales'
5
+ const TUIStore = TuiStore.getInstance();
6
+
7
+ const MODAL_ERROR_CODES = [
8
+ -1001,
9
+ -1002,
10
+ -1101,
11
+ 60003,
12
+ 60004,
13
+ 60006,
14
+ -1201,
15
+ 30000,
16
+ 20007,
17
+ 101002,
18
+ ];
19
+
20
+ const MODAL_ERROR_MAP = {
21
+ '-1001': {
22
+ id: 10001,
23
+ key: 'error.10001'
24
+ },
25
+ '-1002': {
26
+ id: 10002,
27
+ key: 'error.10002'
28
+ },
29
+ '-1101': {
30
+ id: 10004,
31
+ key: 'error.10004'
32
+ },
33
+ '60003': {
34
+ id: 10005,
35
+ key: 'error.10005'
36
+ },
37
+ '60004': {
38
+ id: 10006,
39
+ key: 'error.10006'
40
+ },
41
+ '60006': {
42
+ id: 10007,
43
+ key: 'error.10007'
44
+ },
45
+ '-1201': {
46
+ id: 10008,
47
+ key: 'error.10008'
48
+ },
49
+ '30000': {
50
+ id: 10012,
51
+ key: 'error.10012'
52
+ },
53
+ '20007': {
54
+ id: 10013,
55
+ key: 'error.10013'
56
+ },
57
+ '101002': {
58
+ id: 10014,
59
+ key: 'error.10014'
60
+ }
61
+ };
62
+
63
+ export function handleModalError(error) {
64
+ if (!error || !error?.code) {
65
+ return;
66
+ }
67
+
68
+ if (!MODAL_ERROR_CODES.includes(error.code)) {
69
+ return;
70
+ }
71
+
72
+ const errorInfo = MODAL_ERROR_MAP[error.code.toString()];
73
+ if (errorInfo) {
74
+ let content = t(errorInfo.key);
75
+ TUIStore.update(StoreName.CALL, NAME.MODAL_ERROR, {
76
+ id: errorInfo.id,
77
+ content: content,
78
+ title: t('error')
79
+ });
80
+ }
81
+ }