@unif/react-native-chat 2.0.0 → 2.1.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 (46) hide show
  1. package/README.md +39 -30
  2. package/lib/module/components/Composer/Composer.js +131 -83
  3. package/lib/module/components/Composer/Composer.js.map +1 -1
  4. package/lib/module/components/Composer/ComposerIconAction.js +60 -0
  5. package/lib/module/components/Composer/ComposerIconAction.js.map +1 -0
  6. package/lib/module/components/Composer/ComposerMenuItem.js +57 -0
  7. package/lib/module/components/Composer/ComposerMenuItem.js.map +1 -0
  8. package/lib/module/components/Composer/constants.js +9 -1
  9. package/lib/module/components/Composer/constants.js.map +1 -1
  10. package/lib/module/components/Composer/styles.js +143 -13
  11. package/lib/module/components/Composer/styles.js.map +1 -1
  12. package/lib/module/components/Composer/useMousePress.js +28 -0
  13. package/lib/module/components/Composer/useMousePress.js.map +1 -0
  14. package/lib/module/components/Suggestions/Suggestions.js +6 -2
  15. package/lib/module/components/Suggestions/Suggestions.js.map +1 -1
  16. package/lib/module/components/Suggestions/constants.js +4 -0
  17. package/lib/module/components/Suggestions/constants.js.map +1 -0
  18. package/lib/typescript/src/components/Composer/Composer.d.ts.map +1 -1
  19. package/lib/typescript/src/components/Composer/ComposerIconAction.d.ts +3 -0
  20. package/lib/typescript/src/components/Composer/ComposerIconAction.d.ts.map +1 -0
  21. package/lib/typescript/src/components/Composer/ComposerMenuItem.d.ts +3 -0
  22. package/lib/typescript/src/components/Composer/ComposerMenuItem.d.ts.map +1 -0
  23. package/lib/typescript/src/components/Composer/constants.d.ts +8 -1
  24. package/lib/typescript/src/components/Composer/constants.d.ts.map +1 -1
  25. package/lib/typescript/src/components/Composer/styles.d.ts +150 -7
  26. package/lib/typescript/src/components/Composer/styles.d.ts.map +1 -1
  27. package/lib/typescript/src/components/Composer/types.d.ts +19 -1
  28. package/lib/typescript/src/components/Composer/types.d.ts.map +1 -1
  29. package/lib/typescript/src/components/Composer/useMousePress.d.ts +9 -0
  30. package/lib/typescript/src/components/Composer/useMousePress.d.ts.map +1 -0
  31. package/lib/typescript/src/components/Suggestions/Suggestions.d.ts.map +1 -1
  32. package/lib/typescript/src/components/Suggestions/constants.d.ts +2 -0
  33. package/lib/typescript/src/components/Suggestions/constants.d.ts.map +1 -0
  34. package/lib/typescript/src/components/Suggestions/types.d.ts +1 -0
  35. package/lib/typescript/src/components/Suggestions/types.d.ts.map +1 -1
  36. package/package.json +10 -10
  37. package/src/components/Composer/Composer.tsx +165 -95
  38. package/src/components/Composer/ComposerIconAction.tsx +71 -0
  39. package/src/components/Composer/ComposerMenuItem.tsx +56 -0
  40. package/src/components/Composer/constants.ts +10 -1
  41. package/src/components/Composer/styles.ts +123 -13
  42. package/src/components/Composer/types.ts +21 -1
  43. package/src/components/Composer/useMousePress.ts +32 -0
  44. package/src/components/Suggestions/Suggestions.tsx +12 -2
  45. package/src/components/Suggestions/constants.ts +1 -0
  46. package/src/components/Suggestions/types.ts +1 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@unif/react-native-chat",
3
- "version": "2.0.0",
4
- "description": "基于 Unif Design 的独立 React Native 聊天组件",
3
+ "version": "2.1.0",
4
+ "description": "Unif React Native 聊天组件:消息、输入、附件和交互展示",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
7
7
  "exports": {
@@ -29,10 +29,10 @@
29
29
  "typecheck": "tsc --noEmit",
30
30
  "test": "jest",
31
31
  "release": "release-it --only-version",
32
- "web": "yarn example web",
33
- "build:web": "yarn example build:web",
32
+ "web": "yarn workspace @unif/react-native-chat-website start",
34
33
  "lint": "eslint \"**/*.{js,jsx,mjs,ts,tsx}\"",
35
- "format:check": "prettier --check ."
34
+ "format:check": "prettier --check .",
35
+ "website": "yarn workspace @unif/react-native-chat-website"
36
36
  },
37
37
  "keywords": [
38
38
  "react-native",
@@ -48,7 +48,7 @@
48
48
  "bugs": {
49
49
  "url": "https://github.com/unif-design/react-native-chat/issues"
50
50
  },
51
- "homepage": "https://github.com/unif-design/react-native-chat#readme",
51
+ "homepage": "https://unif-design.github.io/react-native-chat/",
52
52
  "publishConfig": {
53
53
  "registry": "https://registry.npmjs.org/",
54
54
  "access": "public"
@@ -68,7 +68,7 @@
68
68
  "@sbaiahmed1/react-native-blur": "6.0.1",
69
69
  "@testing-library/react-native": "13.3.3",
70
70
  "@types/react": "^19.2.0",
71
- "@unif/react-native-design": "0.32.0",
71
+ "@unif/react-native-design": "0.33.0",
72
72
  "commitlint": "^21.2.1",
73
73
  "del-cli": "^7.0.0",
74
74
  "eslint": "^9.39.5",
@@ -96,13 +96,14 @@
96
96
  "typescript": "^6.0.3"
97
97
  },
98
98
  "peerDependencies": {
99
- "@unif/react-native-design": "^0.32.0",
99
+ "@unif/react-native-design": "^0.33.0",
100
100
  "react": ">=19.2.3 <20.0.0",
101
101
  "react-native": ">=0.86.0",
102
102
  "react-native-svg": ">=15"
103
103
  },
104
104
  "workspaces": [
105
- "example"
105
+ "example",
106
+ "website"
106
107
  ],
107
108
  "packageManager": "yarn@4.11.0",
108
109
  "react-native-builder-bob": {
@@ -164,7 +165,6 @@
164
165
  "jest",
165
166
  "lefthook",
166
167
  "release-it",
167
- "vite",
168
168
  "eslint"
169
169
  ],
170
170
  "version": "0.63.1"
@@ -5,21 +5,23 @@ import {
5
5
  useRef,
6
6
  useState,
7
7
  } from 'react';
8
- import { Text, View } from 'react-native';
8
+ import { Pressable, Text, View, useWindowDimensions } from 'react-native';
9
9
  import {
10
- Button,
11
10
  fixed,
12
- IconButton,
13
11
  Textarea,
14
12
  useTheme,
15
13
  useThemedStyles,
16
14
  } from '@unif/react-native-design';
17
15
  import type { TextFieldHandle } from '@unif/react-native-design';
18
16
  import {
19
- DEFAULT_MAX_INPUT_HEIGHT,
17
+ COMPOSER_INPUT_LINE_HEIGHT,
18
+ COMPOSER_INPUT_MAX_LINES,
19
+ COMPOSER_PLAIN_VERTICAL_INSET,
20
20
  PRIMARY_LABELS,
21
21
  VOICE_LABELS,
22
22
  } from './constants';
23
+ import { ComposerIconAction } from './ComposerIconAction';
24
+ import { ComposerMenuItem } from './ComposerMenuItem';
23
25
  import { createStyles } from './styles';
24
26
  import type { ComposerHandle, ComposerProps } from './types';
25
27
 
@@ -49,20 +51,35 @@ export const Composer = forwardRef<ComposerHandle, ComposerProps>(
49
51
  ) {
50
52
  const styles = useThemedStyles(createStyles);
51
53
  const { fontScale } = useTheme();
54
+ const { height: windowHeight } = useWindowDimensions();
52
55
  const inputRef = useRef<TextFieldHandle>(null);
53
56
  const lastHeight = useRef<number | undefined>(undefined);
54
57
  const [menuOpen, setMenuOpen] = useState(false);
58
+ const [focused, setFocused] = useState(false);
55
59
  const voiceActive = voice !== undefined && voice.status !== 'idle';
60
+ const expanded = focused || value.length > 0;
56
61
  const minHeight = Number.isFinite(minInputHeight)
57
62
  ? Math.max(fixed.hitTarget, minInputHeight!)
58
63
  : fixed.hitTarget;
59
64
  const defaultMaxHeight = Math.max(
60
65
  minHeight,
61
- DEFAULT_MAX_INPUT_HEIGHT * fontScale
66
+ COMPOSER_INPUT_LINE_HEIGHT * COMPOSER_INPUT_MAX_LINES * fontScale +
67
+ COMPOSER_PLAIN_VERTICAL_INSET
62
68
  );
63
69
  const maxHeight = Number.isFinite(maxInputHeight)
64
70
  ? Math.max(minHeight, maxInputHeight!)
65
71
  : defaultMaxHeight;
72
+ const showPrimary =
73
+ primaryAction.kind !== 'send' ||
74
+ primaryAction.allowEmpty ||
75
+ value.trim().length > 0;
76
+ const primaryDisabled =
77
+ disabled || primaryAction.kind === 'busy' || primaryAction.disabled;
78
+ const showToolbar =
79
+ (expanded && actions.length > 0) || Boolean(voice) || showPrimary;
80
+ const primaryLabel = primaryAction.label?.trim()
81
+ ? primaryAction.label
82
+ : PRIMARY_LABELS[primaryAction.kind];
66
83
 
67
84
  useImperativeHandle(
68
85
  ref,
@@ -76,17 +93,23 @@ export const Composer = forwardRef<ComposerHandle, ComposerProps>(
76
93
  );
77
94
 
78
95
  useEffect(() => {
79
- if (voiceActive) inputRef.current?.blur();
96
+ if (voiceActive) {
97
+ inputRef.current?.blur();
98
+ setFocused(false);
99
+ }
80
100
  if (voiceActive || disabled) setMenuOpen(false);
81
101
  }, [voiceActive, disabled]);
82
102
 
83
- const primaryDisabled =
84
- disabled ||
85
- primaryAction.kind === 'busy' ||
86
- primaryAction.disabled ||
87
- (primaryAction.kind === 'send' &&
88
- !primaryAction.allowEmpty &&
89
- value.trim().length === 0);
103
+ const moreAction =
104
+ actions.length > 0 ? (
105
+ <ComposerIconAction
106
+ icon="plus"
107
+ label="更多操作"
108
+ expanded={menuOpen}
109
+ disabled={disabled}
110
+ onPress={() => setMenuOpen((open) => !open)}
111
+ />
112
+ ) : null;
90
113
 
91
114
  return (
92
115
  <View
@@ -99,109 +122,156 @@ export const Composer = forwardRef<ComposerHandle, ComposerProps>(
99
122
  }
100
123
  }}
101
124
  >
102
- {header}
125
+ {header != null ? <View style={styles.accessory}>{header}</View> : null}
126
+ {menuOpen && !disabled && !voiceActive && actions.length > 0 ? (
127
+ <>
128
+ <Pressable
129
+ style={[styles.backdrop, { height: windowHeight }]}
130
+ accessibilityRole="button"
131
+ accessibilityLabel="收起更多操作"
132
+ onPress={() => setMenuOpen(false)}
133
+ />
134
+ <View style={styles.menu} testID="composer-menu">
135
+ <View style={styles.menuClip} testID="composer-menu-content">
136
+ {actions.map((action) => (
137
+ <ComposerMenuItem
138
+ key={action.id}
139
+ action={action}
140
+ disabled={disabled}
141
+ onPress={() => {
142
+ if (disabled || action.disabled || action.loading) return;
143
+ setMenuOpen(false);
144
+ action.onPress();
145
+ }}
146
+ />
147
+ ))}
148
+ </View>
149
+ </View>
150
+ </>
151
+ ) : null}
103
152
  <View
104
- style={[styles.regular, voiceActive && styles.hidden]}
153
+ testID="composer-regular"
154
+ style={[
155
+ styles.regular,
156
+ expanded && styles.expanded,
157
+ voiceActive && styles.hidden,
158
+ ]}
105
159
  importantForAccessibility={
106
160
  voiceActive ? 'no-hide-descendants' : 'auto'
107
161
  }
108
162
  >
109
- <Textarea
110
- ref={inputRef}
111
- value={value}
112
- onChangeText={onChangeText}
113
- editable={editable}
114
- disabled={disabled || voiceActive}
115
- placeholder={placeholder}
116
- accessibilityLabel={inputAccessibilityLabel}
117
- minHeight={minHeight}
118
- maxHeight={maxHeight}
119
- submitBehavior="newline"
120
- onFocus={() => {
121
- setMenuOpen(false);
122
- onFocusChange?.(true);
123
- }}
124
- onBlur={() => onFocusChange?.(false)}
125
- />
126
- <View style={styles.toolbar}>
127
- {actions.length > 0 ? (
128
- <IconButton
129
- icon="more-h"
130
- size="lg"
131
- accessibilityLabel="更多操作"
132
- accessibilityState={{ expanded: menuOpen }}
133
- disabled={disabled}
134
- onPress={() => setMenuOpen((open) => !open)}
135
- />
136
- ) : null}
137
- {voice ? (
138
- <IconButton
139
- icon="mic"
140
- size="lg"
141
- accessibilityLabel="开始语音输入"
142
- disabled={disabled || voice.disabled}
143
- onPress={voice.onStart}
144
- />
145
- ) : null}
146
- <Button
147
- label={primaryAction.label ?? PRIMARY_LABELS[primaryAction.kind]}
148
- size="lg"
149
- style={styles.primary}
150
- disabled={primaryDisabled}
151
- loading={primaryAction.kind === 'busy'}
152
- onPress={() => {
153
- if (primaryDisabled) return;
154
- if (primaryAction.kind === 'send') primaryAction.onPress(value);
155
- else if (primaryAction.kind === 'stop') primaryAction.onPress();
163
+ {moreAction ? (
164
+ <View
165
+ key="more"
166
+ style={[styles.more, expanded && styles.moreExpanded]}
167
+ >
168
+ {moreAction}
169
+ </View>
170
+ ) : null}
171
+ <View
172
+ key="input"
173
+ style={[styles.input, expanded && styles.inputExpanded]}
174
+ >
175
+ <Textarea
176
+ ref={inputRef}
177
+ surface="plain"
178
+ value={value}
179
+ onChangeText={onChangeText}
180
+ editable={editable}
181
+ disabled={disabled || voiceActive}
182
+ placeholder={placeholder}
183
+ accessibilityLabel={inputAccessibilityLabel}
184
+ minHeight={minHeight}
185
+ maxHeight={maxHeight}
186
+ submitBehavior="newline"
187
+ onPressIn={() => setMenuOpen(false)}
188
+ onFocus={() => {
189
+ setMenuOpen(false);
190
+ setFocused(true);
191
+ onFocusChange?.(true);
192
+ }}
193
+ onBlur={() => {
194
+ setFocused(false);
195
+ onFocusChange?.(false);
156
196
  }}
157
197
  />
158
198
  </View>
159
- {menuOpen && actions.length > 0 ? (
160
- <View style={styles.menu}>
161
- {actions.map((action) => (
162
- <Button
163
- key={action.id}
164
- label={action.label}
165
- leftIcon={action.icon}
166
- disabled={disabled || action.disabled}
167
- loading={action.loading}
168
- variant="secondary"
169
- accessibilityHint={action.accessibilityHint}
170
- onPress={() => {
171
- if (disabled || action.disabled || action.loading) return;
172
- setMenuOpen(false);
173
- action.onPress();
174
- }}
175
- />
176
- ))}
199
+ {showToolbar ? (
200
+ <View
201
+ key="toolbar"
202
+ style={[
203
+ styles.toolbar,
204
+ expanded && styles.toolbarExpanded,
205
+ expanded && moreAction != null && styles.toolbarWithMore,
206
+ ]}
207
+ >
208
+ <View style={styles.trailingActions}>
209
+ {voice ? (
210
+ <ComposerIconAction
211
+ icon="mic"
212
+ label="开始语音输入"
213
+ disabled={disabled || voice.disabled}
214
+ onPress={() => {
215
+ setMenuOpen(false);
216
+ voice.onStart();
217
+ }}
218
+ />
219
+ ) : null}
220
+ {showPrimary ? (
221
+ <ComposerIconAction
222
+ icon={primaryAction.kind === 'send' ? 'send' : 'stop'}
223
+ label={primaryLabel}
224
+ visual="primary"
225
+ disabled={primaryDisabled}
226
+ busy={primaryAction.kind === 'busy'}
227
+ onPress={() => {
228
+ if (primaryDisabled) return;
229
+ setMenuOpen(false);
230
+ if (primaryAction.kind === 'send')
231
+ primaryAction.onPress(value);
232
+ else if (primaryAction.kind === 'stop')
233
+ primaryAction.onPress();
234
+ }}
235
+ />
236
+ ) : null}
237
+ </View>
177
238
  </View>
178
239
  ) : null}
179
240
  </View>
180
241
  {voice && voice.status !== 'idle' ? (
181
242
  <View style={styles.voice}>
182
- <Text style={styles.transcript} accessibilityLiveRegion="polite">
243
+ <ComposerIconAction
244
+ icon="close"
245
+ visual="cancel"
246
+ label="取消语音输入"
247
+ disabled={disabled || voice.disabled}
248
+ onPress={voice.onCancel}
249
+ />
250
+ <Text
251
+ numberOfLines={1}
252
+ ellipsizeMode="head"
253
+ style={[
254
+ styles.transcript,
255
+ !voice.transcript && styles.transcriptPlaceholder,
256
+ ]}
257
+ accessibilityLiveRegion="polite"
258
+ >
183
259
  {voice.status === 'starting'
184
260
  ? VOICE_LABELS.starting
185
261
  : voice.transcript || VOICE_LABELS[voice.status]}
186
262
  </Text>
187
- <View style={styles.toolbar}>
188
- {voice.status === 'listening' ? (
189
- <Button
190
- label="停止语音输入"
191
- disabled={disabled || voice.disabled}
192
- onPress={voice.onStop}
193
- />
194
- ) : null}
195
- <Button
196
- label="取消语音输入"
197
- variant="secondary"
263
+ {voice.status === 'listening' ? (
264
+ <ComposerIconAction
265
+ icon="stop"
266
+ visual="primary"
267
+ label="停止语音输入"
198
268
  disabled={disabled || voice.disabled}
199
- onPress={voice.onCancel}
269
+ onPress={voice.onStop}
200
270
  />
201
- </View>
271
+ ) : null}
202
272
  </View>
203
273
  ) : null}
204
- {footer}
274
+ {footer != null ? <View style={styles.accessory}>{footer}</View> : null}
205
275
  </View>
206
276
  );
207
277
  }
@@ -0,0 +1,71 @@
1
+ import { ActivityIndicator, Pressable, View } from 'react-native';
2
+ import { Icon, useColors, useThemedStyles } from '@unif/react-native-design';
3
+ import {
4
+ COMPOSER_ACTION_ICON_SIZE,
5
+ COMPOSER_PRIMARY_ICON_SIZE,
6
+ } from './constants';
7
+ import { createStyles } from './styles';
8
+ import { useMousePress } from './useMousePress';
9
+ import type { ComposerIconActionProps } from './types';
10
+
11
+ export function ComposerIconAction({
12
+ icon,
13
+ label,
14
+ onPress,
15
+ disabled = false,
16
+ busy = false,
17
+ expanded,
18
+ visual = 'icon',
19
+ }: ComposerIconActionProps) {
20
+ const colors = useColors();
21
+ const styles = useThemedStyles(createStyles);
22
+ const unavailable = disabled || busy;
23
+ const { mousePressed, ...mouseHandlers } = useMousePress(unavailable);
24
+ const primary = visual === 'primary';
25
+ const color = primary
26
+ ? disabled
27
+ ? colors.foregroundSubtle
28
+ : colors.onPrimary
29
+ : colors.foregroundMuted;
30
+ return (
31
+ <Pressable
32
+ accessibilityRole="button"
33
+ accessibilityLabel={label}
34
+ accessibilityState={{ disabled: unavailable, busy, expanded }}
35
+ disabled={unavailable}
36
+ onPress={unavailable ? undefined : onPress}
37
+ {...mouseHandlers}
38
+ style={styles.action}
39
+ >
40
+ {({ pressed }) => (
41
+ <View
42
+ pointerEvents="none"
43
+ style={[
44
+ styles.actionVisual,
45
+ primary && styles.primary,
46
+ visual === 'cancel' && styles.cancel,
47
+ (pressed || mousePressed) &&
48
+ (primary ? styles.primaryPressed : styles.actionPressed),
49
+ disabled &&
50
+ (primary ? styles.primaryDisabled : styles.actionDisabled),
51
+ ]}
52
+ >
53
+ {busy ? (
54
+ <ActivityIndicator size="small" color={color} />
55
+ ) : (
56
+ <Icon
57
+ name={icon}
58
+ size={
59
+ primary ? COMPOSER_PRIMARY_ICON_SIZE : COMPOSER_ACTION_ICON_SIZE
60
+ }
61
+ color={color}
62
+ style={
63
+ expanded ? { transform: [{ rotate: '45deg' }] } : undefined
64
+ }
65
+ />
66
+ )}
67
+ </View>
68
+ )}
69
+ </Pressable>
70
+ );
71
+ }
@@ -0,0 +1,56 @@
1
+ import { ActivityIndicator, Pressable, Text, View } from 'react-native';
2
+ import { Icon, useColors, useThemedStyles } from '@unif/react-native-design';
3
+ import { COMPOSER_MENU_ICON_SIZE } from './constants';
4
+ import { useMousePress } from './useMousePress';
5
+ import { createStyles } from './styles';
6
+ import type { ComposerMenuItemProps } from './types';
7
+
8
+ export function ComposerMenuItem({
9
+ action,
10
+ disabled,
11
+ onPress,
12
+ }: ComposerMenuItemProps) {
13
+ const styles = useThemedStyles(createStyles);
14
+ const colors = useColors();
15
+ const busy = action.loading === true;
16
+ const named = action.label.trim().length > 0;
17
+ const unavailable = disabled || action.disabled === true || busy || !named;
18
+ const { mousePressed, ...mouseHandlers } = useMousePress(unavailable);
19
+ return (
20
+ <Pressable
21
+ accessible={named}
22
+ accessibilityRole={named ? 'button' : undefined}
23
+ accessibilityLabel={named ? action.label : undefined}
24
+ accessibilityHint={action.accessibilityHint}
25
+ accessibilityState={{ disabled: unavailable, busy }}
26
+ disabled={unavailable}
27
+ onPress={unavailable ? undefined : onPress}
28
+ {...mouseHandlers}
29
+ style={({ pressed }) => [
30
+ styles.menuRow,
31
+ (pressed || mousePressed) && styles.menuRowPressed,
32
+ unavailable && styles.actionDisabled,
33
+ ]}
34
+ >
35
+ {action.icon || busy ? (
36
+ <View
37
+ testID={`composer-menu-icon-${action.id}`}
38
+ style={styles.menuIcon}
39
+ accessibilityElementsHidden
40
+ importantForAccessibility="no-hide-descendants"
41
+ >
42
+ {busy ? (
43
+ <ActivityIndicator size="small" color={colors.foregroundMuted} />
44
+ ) : action.icon ? (
45
+ <Icon
46
+ name={action.icon}
47
+ size={COMPOSER_MENU_ICON_SIZE}
48
+ color={colors.foreground}
49
+ />
50
+ ) : null}
51
+ </View>
52
+ ) : null}
53
+ <Text style={styles.menuLabel}>{action.label}</Text>
54
+ </Pressable>
55
+ );
56
+ }
@@ -1,4 +1,13 @@
1
- export const DEFAULT_MAX_INPUT_HEIGHT = 120;
1
+ import { space, type } from '@unif/react-native-design';
2
+
3
+ export const COMPOSER_INPUT_LINE_HEIGHT = Math.round(type.body * 1.4);
4
+ export const COMPOSER_INPUT_MAX_LINES = 4;
5
+ export const COMPOSER_PLAIN_VERTICAL_INSET = 2 * space[4];
6
+ export const COMPOSER_ACTION_VISUAL_SIZE = 34;
7
+ export const COMPOSER_ACTION_ICON_SIZE = 22;
8
+ export const COMPOSER_PRIMARY_ICON_SIZE = 18;
9
+ export const COMPOSER_MENU_ICON_CONTAINER_SIZE = 40;
10
+ export const COMPOSER_MENU_ICON_SIZE = 20;
2
11
  export const PRIMARY_LABELS = {
3
12
  send: '发送',
4
13
  stop: '停止回复',