@unif/react-native-chat 2.2.0 → 3.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 (59) hide show
  1. package/lib/module/components/Composer/Composer.js +12 -7
  2. package/lib/module/components/Composer/Composer.js.map +1 -1
  3. package/lib/module/components/Composer/ComposerIconAction.js +2 -2
  4. package/lib/module/components/Composer/ComposerIconAction.js.map +1 -1
  5. package/lib/module/components/Composer/ComposerVoiceWave.js +44 -0
  6. package/lib/module/components/Composer/ComposerVoiceWave.js.map +1 -0
  7. package/lib/module/components/Composer/constants.js +20 -1
  8. package/lib/module/components/Composer/constants.js.map +1 -1
  9. package/lib/module/components/Composer/styles.js +30 -3
  10. package/lib/module/components/Composer/styles.js.map +1 -1
  11. package/lib/module/components/Message/MarkdownContent/MarkdownContent.js +9 -3
  12. package/lib/module/components/Message/MarkdownContent/MarkdownContent.js.map +1 -1
  13. package/lib/module/components/Message/MarkdownContent/styles.js +21 -11
  14. package/lib/module/components/Message/MarkdownContent/styles.js.map +1 -1
  15. package/lib/module/components/Message/Message.js +16 -4
  16. package/lib/module/components/Message/Message.js.map +1 -1
  17. package/lib/module/components/Message/styles.js +20 -14
  18. package/lib/module/components/Message/styles.js.map +1 -1
  19. package/lib/module/components/MessageList/MessageList.js +29 -8
  20. package/lib/module/components/MessageList/MessageList.js.map +1 -1
  21. package/lib/module/components/MessageList/styles.js +14 -4
  22. package/lib/module/components/MessageList/styles.js.map +1 -1
  23. package/lib/typescript/src/components/Composer/Composer.d.ts.map +1 -1
  24. package/lib/typescript/src/components/Composer/ComposerIconAction.d.ts.map +1 -1
  25. package/lib/typescript/src/components/Composer/ComposerVoiceWave.d.ts +2 -0
  26. package/lib/typescript/src/components/Composer/ComposerVoiceWave.d.ts.map +1 -0
  27. package/lib/typescript/src/components/Composer/constants.d.ts +29 -0
  28. package/lib/typescript/src/components/Composer/constants.d.ts.map +1 -1
  29. package/lib/typescript/src/components/Composer/styles.d.ts +27 -0
  30. package/lib/typescript/src/components/Composer/styles.d.ts.map +1 -1
  31. package/lib/typescript/src/components/Message/MarkdownContent/MarkdownContent.d.ts +1 -1
  32. package/lib/typescript/src/components/Message/MarkdownContent/MarkdownContent.d.ts.map +1 -1
  33. package/lib/typescript/src/components/Message/MarkdownContent/styles.d.ts +104 -0
  34. package/lib/typescript/src/components/Message/MarkdownContent/styles.d.ts.map +1 -1
  35. package/lib/typescript/src/components/Message/MarkdownContent/types.d.ts +1 -0
  36. package/lib/typescript/src/components/Message/MarkdownContent/types.d.ts.map +1 -1
  37. package/lib/typescript/src/components/Message/Message.d.ts.map +1 -1
  38. package/lib/typescript/src/components/Message/styles.d.ts +11 -5
  39. package/lib/typescript/src/components/Message/styles.d.ts.map +1 -1
  40. package/lib/typescript/src/components/MessageList/MessageList.d.ts +1 -1
  41. package/lib/typescript/src/components/MessageList/MessageList.d.ts.map +1 -1
  42. package/lib/typescript/src/components/MessageList/styles.d.ts +26 -1
  43. package/lib/typescript/src/components/MessageList/styles.d.ts.map +1 -1
  44. package/lib/typescript/src/components/MessageList/types.d.ts +6 -2
  45. package/lib/typescript/src/components/MessageList/types.d.ts.map +1 -1
  46. package/package.json +4 -4
  47. package/src/components/Composer/Composer.tsx +21 -14
  48. package/src/components/Composer/ComposerIconAction.tsx +6 -1
  49. package/src/components/Composer/ComposerVoiceWave.tsx +67 -0
  50. package/src/components/Composer/constants.ts +14 -1
  51. package/src/components/Composer/styles.ts +23 -2
  52. package/src/components/Message/MarkdownContent/MarkdownContent.tsx +15 -3
  53. package/src/components/Message/MarkdownContent/styles.ts +23 -11
  54. package/src/components/Message/MarkdownContent/types.ts +1 -0
  55. package/src/components/Message/Message.tsx +46 -15
  56. package/src/components/Message/styles.ts +23 -12
  57. package/src/components/MessageList/MessageList.tsx +33 -12
  58. package/src/components/MessageList/styles.ts +22 -6
  59. package/src/components/MessageList/types.ts +6 -2
@@ -1,4 +1,5 @@
1
- export declare const styles: Readonly<{
1
+ import type { ColorTokens, ShadowTokens } from '@unif/react-native-design';
2
+ export declare const createStyles: (colors: ColorTokens, shadows: ShadowTokens) => Readonly<{
2
3
  root: {
3
4
  flex: number;
4
5
  minHeight: number;
@@ -11,6 +12,30 @@ export declare const styles: Readonly<{
11
12
  position: "absolute";
12
13
  bottom: number;
13
14
  alignSelf: "center";
15
+ width: number;
16
+ height: number;
17
+ alignItems: "center";
18
+ justifyContent: "center";
19
+ };
20
+ returnProgress: Readonly<{
21
+ position: "absolute";
22
+ left: 0;
23
+ right: 0;
24
+ top: 0;
25
+ bottom: 0;
26
+ }>;
27
+ returnButton: {
28
+ shadowColor: string;
29
+ shadowOffset: {
30
+ readonly width: number;
31
+ readonly height: number;
32
+ };
33
+ shadowOpacity: number;
34
+ shadowRadius: number;
35
+ elevation: number;
36
+ alignSelf: "center";
37
+ borderRadius: number;
38
+ backgroundColor: string;
14
39
  };
15
40
  }>;
16
41
  //# sourceMappingURL=styles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../../../src/components/MessageList/styles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;EAIjB,CAAC"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../../../src/components/MessageList/styles.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC3E,eAAO,MAAM,YAAY,GAAI,QAAQ,WAAW,EAAE,SAAS,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoBnE,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { StyleProp, ViewStyle } from 'react-native';
2
+ import type { ScrollViewProps, StyleProp, ViewStyle } from 'react-native';
3
3
  export interface MessageListScrollOptions {
4
4
  animated?: boolean;
5
5
  }
@@ -19,11 +19,15 @@ export interface MessageListProps<T> {
19
19
  followOutput?: 'whenAtEnd' | 'never';
20
20
  endThreshold?: number;
21
21
  showScrollToEnd?: boolean;
22
+ /** 外部仍在生成内容时显示返回入口的进度外环,不影响滚动操作。 */
23
+ scrollToEndBusy?: boolean;
22
24
  onAtEndChange?(atEnd: boolean): void;
23
25
  hasEarlier?: boolean;
24
26
  loadingEarlier?: boolean;
25
27
  onRequestEarlier?(): void;
26
- keyboardDismissMode?: 'none' | 'on-drag';
28
+ keyboardDismissMode?: ScrollViewProps['keyboardDismissMode'];
29
+ contentContainerStyle?: StyleProp<ViewStyle>;
30
+ showsVerticalScrollIndicator?: boolean;
27
31
  style?: StyleProp<ViewStyle>;
28
32
  testID?: string;
29
33
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/components/MessageList/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AACD,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,IAAI,CAAC;CACvD;AACD,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IACpB,YAAY,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;IAC9B,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,eAAe,CAAC,CAAC,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,SAAS,CAAC;IAClD,eAAe,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAClC,YAAY,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,gBAAgB,CAAC,IAAI,IAAI,CAAC;IAC1B,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AACD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,OAAO,CAAC;CACzB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/components/MessageList/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE1E,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AACD,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,IAAI,CAAC;CACvD;AACD,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IACpB,YAAY,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC;IAC9B,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,eAAe,CAAC,CAAC,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,SAAS,CAAC;IAClD,eAAe,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC;IAClC,YAAY,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,oCAAoC;IACpC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IACrC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,gBAAgB,CAAC,IAAI,IAAI,CAAC;IAC1B,mBAAmB,CAAC,EAAE,eAAe,CAAC,qBAAqB,CAAC,CAAC;IAC7D,qBAAqB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7C,4BAA4B,CAAC,EAAE,OAAO,CAAC;IACvC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AACD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,OAAO,CAAC;CACzB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unif/react-native-chat",
3
- "version": "2.2.0",
3
+ "version": "3.1.0",
4
4
  "description": "Unif React Native 聊天组件:消息、输入、附件和交互展示",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -55,6 +55,7 @@
55
55
  },
56
56
  "devDependencies": {
57
57
  "@babel/core": "^7.29.7",
58
+ "@callstack/liquid-glass": "0.8.2",
58
59
  "@commitlint/config-conventional": "^21.2.0",
59
60
  "@eslint/compat": "^2.1.0",
60
61
  "@eslint/eslintrc": "^3.3.6",
@@ -65,10 +66,9 @@
65
66
  "@react-native/jest-preset": "0.86.3",
66
67
  "@react-native/metro-config": "0.86.3",
67
68
  "@release-it/conventional-changelog": "^12.0.0",
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.34.0",
71
+ "@unif/react-native-design": "0.36.0",
72
72
  "commitlint": "^21.2.1",
73
73
  "del-cli": "^7.0.0",
74
74
  "eslint": "^9.39.5",
@@ -96,7 +96,7 @@
96
96
  "typescript": "^6.0.3"
97
97
  },
98
98
  "peerDependencies": {
99
- "@unif/react-native-design": "^0.34.0",
99
+ "@unif/react-native-design": "^0.36.0",
100
100
  "react": ">=19.2.3 <20.0.0",
101
101
  "react-native": ">=0.86.0",
102
102
  "react-native-svg": ">=15"
@@ -22,6 +22,7 @@ import {
22
22
  } from './constants';
23
23
  import { ComposerIconAction } from './ComposerIconAction';
24
24
  import { ComposerMenuItem } from './ComposerMenuItem';
25
+ import { ComposerVoiceWave } from './ComposerVoiceWave';
25
26
  import { createStyles } from './styles';
26
27
  import type { ComposerHandle, ComposerProps } from './types';
27
28
 
@@ -122,7 +123,9 @@ export const Composer = forwardRef<ComposerHandle, ComposerProps>(
122
123
  }
123
124
  }}
124
125
  >
125
- {header != null ? <View style={styles.accessory}>{header}</View> : null}
126
+ {header != null ? (
127
+ <View style={[styles.accessory, styles.header]}>{header}</View>
128
+ ) : null}
126
129
  {menuOpen && !disabled && !voiceActive && actions.length > 0 ? (
127
130
  <>
128
131
  <Pressable
@@ -175,6 +178,7 @@ export const Composer = forwardRef<ComposerHandle, ComposerProps>(
175
178
  <Textarea
176
179
  ref={inputRef}
177
180
  surface="plain"
181
+ contentAlignment="center"
178
182
  value={value}
179
183
  onChangeText={onChangeText}
180
184
  editable={editable}
@@ -247,19 +251,22 @@ export const Composer = forwardRef<ComposerHandle, ComposerProps>(
247
251
  disabled={disabled || voice.disabled}
248
252
  onPress={voice.onCancel}
249
253
  />
250
- <Text
251
- numberOfLines={1}
252
- ellipsizeMode="head"
253
- style={[
254
- styles.transcript,
255
- !voice.transcript && styles.transcriptPlaceholder,
256
- ]}
257
- accessibilityLiveRegion="polite"
258
- >
259
- {voice.status === 'starting'
260
- ? VOICE_LABELS.starting
261
- : voice.transcript || VOICE_LABELS[voice.status]}
262
- </Text>
254
+ <View style={styles.voiceBody}>
255
+ {voice.status !== 'starting' ? <ComposerVoiceWave /> : null}
256
+ <Text
257
+ numberOfLines={1}
258
+ ellipsizeMode="head"
259
+ style={[
260
+ styles.transcript,
261
+ !voice.transcript && styles.transcriptPlaceholder,
262
+ ]}
263
+ accessibilityLiveRegion="polite"
264
+ >
265
+ {voice.status === 'starting'
266
+ ? VOICE_LABELS.starting
267
+ : voice.transcript || VOICE_LABELS[voice.status]}
268
+ </Text>
269
+ </View>
263
270
  {voice.status === 'listening' ? (
264
271
  <ComposerIconAction
265
272
  icon="stop"
@@ -3,6 +3,7 @@ import { Icon, useColors, useThemedStyles } from '@unif/react-native-design';
3
3
  import {
4
4
  COMPOSER_ACTION_ICON_SIZE,
5
5
  COMPOSER_PRIMARY_ICON_SIZE,
6
+ COMPOSER_CANCEL_ICON_SIZE,
6
7
  } from './constants';
7
8
  import { createStyles } from './styles';
8
9
  import { useMousePress } from './useMousePress';
@@ -56,7 +57,11 @@ export function ComposerIconAction({
56
57
  <Icon
57
58
  name={icon}
58
59
  size={
59
- primary ? COMPOSER_PRIMARY_ICON_SIZE : COMPOSER_ACTION_ICON_SIZE
60
+ primary
61
+ ? COMPOSER_PRIMARY_ICON_SIZE
62
+ : visual === 'cancel'
63
+ ? COMPOSER_CANCEL_ICON_SIZE
64
+ : COMPOSER_ACTION_ICON_SIZE
60
65
  }
61
66
  color={color}
62
67
  style={
@@ -0,0 +1,67 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Animated, Easing, Platform, View } from 'react-native';
3
+ import {
4
+ usePrefersReducedMotion,
5
+ useThemedStyles,
6
+ } from '@unif/react-native-design';
7
+ import {
8
+ COMPOSER_VOICE_WAVE_PATTERN,
9
+ COMPOSER_VOICE_WAVE_STAGGER_MS,
10
+ } from './constants';
11
+ import { createStyles } from './styles';
12
+
13
+ export function ComposerVoiceWave() {
14
+ const styles = useThemedStyles(createStyles);
15
+ const reducedMotion = usePrefersReducedMotion();
16
+ const [values] = useState(() =>
17
+ COMPOSER_VOICE_WAVE_PATTERN.map(
18
+ (group) => new Animated.Value(group.from / Math.max(group.from, group.to))
19
+ )
20
+ );
21
+ useEffect(() => {
22
+ const animations = COMPOSER_VOICE_WAVE_PATTERN.map((group, index) => {
23
+ const value = values[index]!;
24
+ const maximum = Math.max(group.from, group.to);
25
+ value.setValue((reducedMotion ? group.to : group.from) / maximum);
26
+ return Animated.sequence([
27
+ Animated.delay(index * COMPOSER_VOICE_WAVE_STAGGER_MS),
28
+ Animated.loop(
29
+ Animated.sequence(
30
+ [group.to, group.from].map((height) =>
31
+ Animated.timing(value, {
32
+ toValue: height / maximum,
33
+ duration: group.duration / 2,
34
+ easing: Easing.inOut(Easing.ease),
35
+ useNativeDriver: Platform.OS !== 'web',
36
+ })
37
+ )
38
+ )
39
+ ),
40
+ ]);
41
+ });
42
+ if (!reducedMotion) animations.forEach((animation) => animation.start());
43
+ return () => animations.forEach((animation) => animation.stop());
44
+ // 波形规格固定,输入文字更新不重启装饰动画。
45
+ }, [reducedMotion, values]);
46
+ return (
47
+ <View
48
+ style={styles.wave}
49
+ pointerEvents="none"
50
+ accessibilityElementsHidden
51
+ importantForAccessibility="no-hide-descendants"
52
+ >
53
+ {COMPOSER_VOICE_WAVE_PATTERN.map((group, index) => (
54
+ <Animated.View
55
+ key={index}
56
+ style={[
57
+ styles.waveBar,
58
+ {
59
+ height: Math.max(group.from, group.to),
60
+ transform: [{ scaleY: values[index]! }],
61
+ },
62
+ ]}
63
+ />
64
+ ))}
65
+ </View>
66
+ );
67
+ }
@@ -2,10 +2,23 @@ import { space, type } from '@unif/react-native-design';
2
2
 
3
3
  export const COMPOSER_INPUT_LINE_HEIGHT = Math.round(type.body * 1.4);
4
4
  export const COMPOSER_INPUT_MAX_LINES = 4;
5
- export const COMPOSER_PLAIN_VERTICAL_INSET = 2 * space[4];
5
+ export const COMPOSER_PLAIN_VERTICAL_INSET = 2 * space[3];
6
6
  export const COMPOSER_ACTION_VISUAL_SIZE = 34;
7
7
  export const COMPOSER_ACTION_ICON_SIZE = 22;
8
8
  export const COMPOSER_PRIMARY_ICON_SIZE = 18;
9
+ export const COMPOSER_CANCEL_ICON_SIZE = 20;
10
+ export const COMPOSER_VOICE_WAVE_COUNT = 5;
11
+ export const COMPOSER_VOICE_WAVE_STAGGER_MS = 90;
12
+ export const COMPOSER_VOICE_WAVE_GROUPS = [
13
+ { from: 6, to: 20, duration: 1100 },
14
+ { from: 14, to: 5, duration: 1000 },
15
+ { from: 8, to: 18, duration: 900 },
16
+ ] as const;
17
+ export const COMPOSER_VOICE_WAVE_PATTERN = Array.from(
18
+ { length: COMPOSER_VOICE_WAVE_COUNT },
19
+ (_, index) =>
20
+ COMPOSER_VOICE_WAVE_GROUPS[index % COMPOSER_VOICE_WAVE_GROUPS.length]!
21
+ );
9
22
  export const COMPOSER_MENU_ICON_CONTAINER_SIZE = 40;
10
23
  export const COMPOSER_MENU_ICON_SIZE = 20;
11
24
  export const PRIMARY_LABELS = {
@@ -17,6 +17,10 @@ export const createStyles = (colors: ColorTokens, shadows: ShadowTokens) =>
17
17
  ...shadows.card,
18
18
  },
19
19
  accessory: { padding: space[3] },
20
+ header: {
21
+ borderBottomWidth: 0.5,
22
+ borderBottomColor: colors.outlineVariant,
23
+ },
20
24
  regular: {
21
25
  flexDirection: 'row',
22
26
  alignItems: 'flex-end',
@@ -26,7 +30,12 @@ export const createStyles = (colors: ColorTokens, shadows: ShadowTokens) =>
26
30
  },
27
31
  expanded: { flexDirection: 'column', alignItems: 'stretch', gap: 0 },
28
32
  hidden: { display: 'none' },
29
- input: { flex: 1, minWidth: 0 },
33
+ input: {
34
+ flex: 1,
35
+ minWidth: 0,
36
+ alignSelf: 'stretch',
37
+ justifyContent: 'center',
38
+ },
30
39
  inputExpanded: { flex: 0, width: '100%' },
31
40
  // 保持原生堆叠特征稳定,避免聚焦展开时重排输入所在的原生视图树。
32
41
  more: { width: fixed.hitTarget, height: fixed.hitTarget, zIndex: 1 },
@@ -119,11 +128,13 @@ export const createStyles = (colors: ColorTokens, shadows: ShadowTokens) =>
119
128
  voice: {
120
129
  flexDirection: 'row',
121
130
  alignItems: 'center',
122
- gap: space[2],
131
+ gap: space[3],
123
132
  paddingVertical: space[1],
124
133
  paddingHorizontal: space[2],
125
134
  borderRadius: radius['3xl'],
126
135
  backgroundColor: colors.primaryContainerSubtle,
136
+ borderWidth: 0.5,
137
+ borderColor: colors.brandTint14,
127
138
  },
128
139
  transcript: {
129
140
  flex: 1,
@@ -131,5 +142,15 @@ export const createStyles = (colors: ColorTokens, shadows: ShadowTokens) =>
131
142
  color: colors.foreground,
132
143
  fontSize: type.body,
133
144
  },
145
+ voiceBody: {
146
+ flex: 1,
147
+ minWidth: 0,
148
+ flexDirection: 'row',
149
+ alignItems: 'center',
150
+ gap: space[4],
151
+ minHeight: r(34),
152
+ },
153
+ wave: { flexDirection: 'row', alignItems: 'center', gap: 3, height: r(20) },
154
+ waveBar: { width: 2.5, borderRadius: 1.5, backgroundColor: colors.primary },
134
155
  transcriptPlaceholder: { color: colors.foregroundSubtle },
135
156
  });
@@ -8,15 +8,27 @@ import {
8
8
  import { Pressable, Text } from 'react-native';
9
9
  import { Renderer, useMarkdown } from 'react-native-marked';
10
10
  import { useTheme, useThemedStyles } from '@unif/react-native-design';
11
- import { createStyles } from './styles';
11
+ import { createStyles, createOutgoingStyles } from './styles';
12
12
  import type { MarkdownContentProps, MarkdownSvgLabelProps } from './types';
13
13
  import { MarkdownImage } from './MarkdownImage/MarkdownImage';
14
14
 
15
- export function MarkdownContent({ text, onLinkPress }: MarkdownContentProps) {
15
+ export function MarkdownContent({
16
+ text,
17
+ onLinkPress,
18
+ outgoing = false,
19
+ }: MarkdownContentProps) {
16
20
  const { scheme } = useTheme();
17
- const styles = useThemedStyles(createStyles);
21
+ const styles = useThemedStyles(
22
+ outgoing ? createOutgoingStyles : createStyles
23
+ );
18
24
  const markdown = useMemo(() => {
19
25
  const instance = new Renderer();
26
+ const renderCode = instance.code.bind(instance);
27
+ instance.code = (value, language, containerStyle, textStyle) =>
28
+ renderCode(value, language, containerStyle, {
29
+ ...textStyle,
30
+ ...styles.codeText,
31
+ });
20
32
  // 上游 table 节点未提供 key,保留其渲染并补齐稳定的节点身份。
21
33
  const renderTable = instance.table.bind(instance);
22
34
  instance.table = (...args) => (
@@ -2,60 +2,72 @@ import { StyleSheet } from 'react-native';
2
2
  import { type, space, fontMono } from '@unif/react-native-design';
3
3
  import type { ColorTokens } from '@unif/react-native-design';
4
4
 
5
- export const createStyles = (colors: ColorTokens) => {
5
+ const makeMarkdownStyles = (colors: ColorTokens, outgoing: boolean) => {
6
+ const textColor = outgoing ? colors.onPrimary : colors.foreground;
6
7
  const body = {
7
- fontSize: type.body,
8
- lineHeight: type.body * 1.5,
9
- color: colors.foreground,
8
+ fontSize: type.sm,
9
+ lineHeight: type.sm * 1.5,
10
+ color: textColor,
10
11
  };
11
12
  return StyleSheet.create({
12
13
  text: body,
13
14
  em: { ...body, fontStyle: 'italic' },
14
15
  strong: { ...body, fontWeight: 'bold' },
15
16
  strikethrough: { ...body, textDecorationLine: 'line-through' },
16
- link: { ...body, color: colors.primary },
17
+ link: {
18
+ ...body,
19
+ color: outgoing ? colors.onPrimary : colors.primary,
20
+ textDecorationLine: 'underline',
21
+ },
17
22
  li: body,
18
23
  codespan: {
19
24
  ...body,
20
25
  fontFamily: fontMono,
26
+ color: colors.foreground,
21
27
  backgroundColor: colors.surfaceContainer,
22
28
  },
23
29
  h1: {
24
30
  fontSize: type.h1,
25
31
  lineHeight: type.h1 * 1.4,
26
- color: colors.foreground,
32
+ color: textColor,
27
33
  borderBottomColor: colors.outline,
28
34
  },
29
35
  h2: {
30
36
  fontSize: type.h2,
31
37
  lineHeight: type.h2 * 1.4,
32
- color: colors.foreground,
38
+ color: textColor,
33
39
  borderBottomColor: colors.outline,
34
40
  },
35
41
  h3: {
36
42
  fontSize: type.h3,
37
43
  lineHeight: type.h3 * 1.4,
38
- color: colors.foreground,
44
+ color: textColor,
39
45
  },
40
46
  h4: {
41
47
  fontSize: type.body,
42
48
  lineHeight: type.body * 1.4,
43
- color: colors.foreground,
49
+ color: textColor,
44
50
  },
45
51
  h5: {
46
52
  fontSize: type.sm,
47
53
  lineHeight: type.sm * 1.4,
48
- color: colors.foreground,
54
+ color: textColor,
49
55
  },
50
56
  h6: {
51
57
  fontSize: type.xs,
52
58
  lineHeight: type.xs * 1.4,
53
- color: colors.foreground,
59
+ color: textColor,
54
60
  },
55
61
  paragraph: { paddingVertical: space[1] },
56
62
  code: { padding: space[2], backgroundColor: colors.surfaceContainer },
63
+ codeText: { ...body, color: colors.foreground, fontFamily: fontMono },
57
64
  blockquote: { borderLeftColor: colors.outline },
58
65
  hr: { borderBottomColor: colors.outline },
59
66
  table: { borderColor: colors.outline },
60
67
  });
61
68
  };
69
+
70
+ export const createStyles = (colors: ColorTokens) =>
71
+ makeMarkdownStyles(colors, false);
72
+ export const createOutgoingStyles = (colors: ColorTokens) =>
73
+ makeMarkdownStyles(colors, true);
@@ -1,6 +1,7 @@
1
1
  export interface MarkdownContentProps {
2
2
  text: string;
3
3
  onLinkPress?(url: string): void;
4
+ outgoing?: boolean;
4
5
  }
5
6
 
6
7
  export interface MarkdownSvgLabelProps {
@@ -1,5 +1,11 @@
1
1
  import { Pressable, Text, View } from 'react-native';
2
- import { Button, Spinner, useThemedStyles } from '@unif/react-native-design';
2
+ import {
3
+ Button,
4
+ IconButton,
5
+ Spinner,
6
+ useThemedStyles,
7
+ useColors,
8
+ } from '@unif/react-native-design';
3
9
  import { MarkdownContent } from './MarkdownContent/MarkdownContent';
4
10
  import { createStyles } from './styles';
5
11
  import { isNestedAction } from './isNestedAction';
@@ -23,6 +29,7 @@ export function Message(props: MessageProps) {
23
29
  testID,
24
30
  } = props;
25
31
  const styles = useThemedStyles(createStyles);
32
+ const colors = useColors();
26
33
  const hasContent =
27
34
  Boolean(props.text?.length) ||
28
35
  (props.children !== undefined &&
@@ -47,11 +54,20 @@ export function Message(props: MessageProps) {
47
54
  {header}
48
55
  {props.text !== undefined ? (
49
56
  props.format === 'markdown' ? (
50
- <MarkdownContent text={props.text} onLinkPress={props.onLinkPress} />
57
+ <MarkdownContent
58
+ text={props.text}
59
+ onLinkPress={props.onLinkPress}
60
+ outgoing={surface === 'bubble' && placement === 'end'}
61
+ />
51
62
  ) : (
52
63
  <Text
53
64
  selectable
54
- style={styles.text}
65
+ style={[
66
+ styles.text,
67
+ surface === 'bubble' &&
68
+ placement === 'end' &&
69
+ styles.outgoingText,
70
+ ]}
55
71
  numberOfLines={props.numberOfLines}
56
72
  >
57
73
  {props.text}
@@ -117,18 +133,33 @@ export function Message(props: MessageProps) {
117
133
  )}
118
134
  {actions.length ? (
119
135
  <View style={styles.actions}>
120
- {actions.map((action) => (
121
- <Button
122
- key={action.id}
123
- label={action.label}
124
- leftIcon={action.icon}
125
- disabled={action.disabled}
126
- loading={action.loading}
127
- accessibilityHint={action.accessibilityHint}
128
- variant="text"
129
- onPress={action.onPress}
130
- />
131
- ))}
136
+ {actions.map((action) =>
137
+ action.icon ? (
138
+ <IconButton
139
+ key={action.id}
140
+ icon={action.icon}
141
+ size="sm"
142
+ variant="ghost"
143
+ color={colors.foregroundSubtle}
144
+ accessibilityLabel={action.label}
145
+ accessibilityHint={action.accessibilityHint}
146
+ disabled={action.disabled}
147
+ loading={action.loading}
148
+ onPress={action.onPress}
149
+ />
150
+ ) : (
151
+ <Button
152
+ key={action.id}
153
+ label={action.label}
154
+ leftIcon={action.icon}
155
+ disabled={action.disabled}
156
+ loading={action.loading}
157
+ accessibilityHint={action.accessibilityHint}
158
+ variant="text"
159
+ onPress={action.onPress}
160
+ />
161
+ )
162
+ )}
132
163
  </View>
133
164
  ) : null}
134
165
  </View>
@@ -7,36 +7,47 @@ export const createStyles = (colors: ColorTokens) =>
7
7
  root: {
8
8
  flexDirection: 'row',
9
9
  alignItems: 'flex-start',
10
- gap: space[2],
10
+ gap: space[3],
11
11
  minWidth: 0,
12
- paddingVertical: space[2],
13
12
  },
14
13
  end: { flexDirection: 'row-reverse' },
15
14
  column: {
16
- flexShrink: 1,
15
+ flex: 1,
17
16
  maxWidth: '100%',
18
17
  gap: space[1],
19
18
  alignItems: 'flex-start',
20
19
  },
21
20
  columnEnd: { alignItems: 'flex-end' },
22
- fullWidth: { flex: 1, alignSelf: 'stretch' },
23
- content: { minWidth: 0, maxWidth: '100%', gap: space[2] },
21
+ fullWidth: { alignSelf: 'stretch', maxWidth: '100%' },
22
+ content: { minWidth: 0, maxWidth: '78%', gap: space[2] },
24
23
  bubble: {
25
- padding: space[3],
26
- borderRadius: radius.lg,
24
+ paddingHorizontal: space[5],
25
+ paddingVertical: space[4],
26
+ borderRadius: radius['2xl'],
27
27
  backgroundColor: colors.surface,
28
28
  },
29
- outgoing: { backgroundColor: colors.primaryContainer },
29
+ outgoing: { backgroundColor: colors.primary },
30
+ outgoingText: { color: colors.onPrimary },
30
31
  outlined: {
31
- padding: space[3],
32
- borderRadius: radius.lg,
32
+ paddingHorizontal: space[5],
33
+ paddingVertical: space[4],
34
+ borderRadius: radius['2xl'],
33
35
  borderWidth: 1,
34
36
  borderColor: colors.outline,
35
37
  },
36
38
  failed: { borderWidth: 1, borderColor: colors.error },
37
- text: { fontSize: type.body, color: colors.foreground },
39
+ text: {
40
+ fontSize: type.sm,
41
+ lineHeight: type.sm * 1.5,
42
+ color: colors.foreground,
43
+ },
38
44
  name: { fontSize: type.sm, color: colors.foregroundMuted },
39
45
  status: { fontSize: type.sm, color: colors.foregroundMuted },
40
46
  errorText: { color: colors.error },
41
- actions: { flexDirection: 'row', flexWrap: 'wrap', gap: space[2] },
47
+ actions: {
48
+ flexDirection: 'row',
49
+ flexWrap: 'wrap',
50
+ gap: space[1],
51
+ marginTop: space[1],
52
+ },
42
53
  });