@ion299/sdk-react-native 0.1.0-beta.5 → 0.1.0-beta.7

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/CHANGELOG.md +26 -0
  2. package/README.md +69 -11
  3. package/android/src/main/java/com/chatplatform/sdk/ChatSdkAudioPlayerModule.kt +201 -0
  4. package/android/src/main/java/com/chatplatform/sdk/ChatSdkPackage.kt +1 -0
  5. package/ios/ChatSdkAudioPlayer.m +25 -0
  6. package/ios/ChatSdkAudioPlayer.swift +193 -0
  7. package/lib/commonjs/ChatSDK.js +16 -3
  8. package/lib/commonjs/ChatSDK.js.map +1 -1
  9. package/lib/commonjs/api.js +29 -0
  10. package/lib/commonjs/api.js.map +1 -1
  11. package/lib/commonjs/audio/audioController.js +145 -0
  12. package/lib/commonjs/audio/audioController.js.map +1 -0
  13. package/lib/commonjs/components/AudioMessage.js +198 -0
  14. package/lib/commonjs/components/AudioMessage.js.map +1 -0
  15. package/lib/commonjs/components/MessageBubble.js +9 -0
  16. package/lib/commonjs/components/MessageBubble.js.map +1 -1
  17. package/lib/commonjs/native/NativeChatSdkAudioPlayer.js +28 -0
  18. package/lib/commonjs/native/NativeChatSdkAudioPlayer.js.map +1 -0
  19. package/lib/module/ChatSDK.js +16 -3
  20. package/lib/module/ChatSDK.js.map +1 -1
  21. package/lib/module/api.js +29 -0
  22. package/lib/module/api.js.map +1 -1
  23. package/lib/module/audio/audioController.js +140 -0
  24. package/lib/module/audio/audioController.js.map +1 -0
  25. package/lib/module/components/AudioMessage.js +193 -0
  26. package/lib/module/components/AudioMessage.js.map +1 -0
  27. package/lib/module/components/MessageBubble.js +9 -0
  28. package/lib/module/components/MessageBubble.js.map +1 -1
  29. package/lib/module/native/NativeChatSdkAudioPlayer.js +23 -0
  30. package/lib/module/native/NativeChatSdkAudioPlayer.js.map +1 -0
  31. package/lib/typescript/commonjs/ChatSDK.d.ts +3 -0
  32. package/lib/typescript/commonjs/ChatSDK.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/api.d.ts +3 -0
  34. package/lib/typescript/commonjs/api.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/audio/audioController.d.ts +25 -0
  36. package/lib/typescript/commonjs/audio/audioController.d.ts.map +1 -0
  37. package/lib/typescript/commonjs/components/AudioMessage.d.ts +11 -0
  38. package/lib/typescript/commonjs/components/AudioMessage.d.ts.map +1 -0
  39. package/lib/typescript/commonjs/components/MessageBubble.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/native/NativeChatSdkAudioPlayer.d.ts +20 -0
  41. package/lib/typescript/commonjs/native/NativeChatSdkAudioPlayer.d.ts.map +1 -0
  42. package/lib/typescript/module/ChatSDK.d.ts +3 -0
  43. package/lib/typescript/module/ChatSDK.d.ts.map +1 -1
  44. package/lib/typescript/module/api.d.ts +3 -0
  45. package/lib/typescript/module/api.d.ts.map +1 -1
  46. package/lib/typescript/module/audio/audioController.d.ts +25 -0
  47. package/lib/typescript/module/audio/audioController.d.ts.map +1 -0
  48. package/lib/typescript/module/components/AudioMessage.d.ts +11 -0
  49. package/lib/typescript/module/components/AudioMessage.d.ts.map +1 -0
  50. package/lib/typescript/module/components/MessageBubble.d.ts.map +1 -1
  51. package/lib/typescript/module/native/NativeChatSdkAudioPlayer.d.ts +20 -0
  52. package/lib/typescript/module/native/NativeChatSdkAudioPlayer.d.ts.map +1 -0
  53. package/package.json +1 -1
  54. package/src/ChatSDK.ts +18 -3
  55. package/src/api.ts +37 -0
  56. package/src/audio/audioController.ts +144 -0
  57. package/src/components/AudioMessage.tsx +198 -0
  58. package/src/components/MessageBubble.tsx +6 -0
  59. package/src/native/NativeChatSdkAudioPlayer.ts +54 -0
@@ -0,0 +1,198 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import {
3
+ ActivityIndicator,
4
+ GestureResponderEvent,
5
+ LayoutChangeEvent,
6
+ StyleSheet,
7
+ Text,
8
+ TouchableOpacity,
9
+ View,
10
+ } from 'react-native'
11
+ import { audioController, type AudioPlaybackState } from '../audio/audioController'
12
+ import type { ChatAttachment } from '../types'
13
+ import type { ChatTheme } from '../theme'
14
+
15
+ interface Props {
16
+ attachment: ChatAttachment
17
+ isOutbound: boolean
18
+ theme: ChatTheme
19
+ }
20
+
21
+ function attachmentKey(attachment: ChatAttachment): string {
22
+ return attachment.id > 0 ? `a${attachment.id}` : `t${attachment.url}`
23
+ }
24
+
25
+ function formatTime(millis: number): string {
26
+ if (!Number.isFinite(millis) || millis <= 0) return '0:00'
27
+ const totalSeconds = Math.floor(millis / 1000)
28
+ const minutes = Math.floor(totalSeconds / 60)
29
+ const seconds = totalSeconds % 60
30
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`
31
+ }
32
+
33
+ export function AudioMessage({ attachment, isOutbound, theme }: Props) {
34
+ const key = attachmentKey(attachment)
35
+ const [pb, setPb] = useState<AudioPlaybackState>(() => audioController.getState(key))
36
+ const [trackWidth, setTrackWidth] = useState(0)
37
+
38
+ useEffect(() => audioController.subscribe(key, setPb), [key])
39
+
40
+ const isPlaying = pb.state === 'playing'
41
+ const isLoading = pb.state === 'loading'
42
+ const isError = pb.state === 'error'
43
+ const duration = pb.durationMillis
44
+ const position = pb.positionMillis
45
+ const progress = duration > 0 ? Math.min(1, position / duration) : 0
46
+
47
+ const accent = isOutbound ? '#ffffff' : theme.primaryColor
48
+ const trackBg = isOutbound ? 'rgba(255,255,255,0.30)' : 'rgba(0,0,0,0.12)'
49
+ const timeColor = isOutbound ? 'rgba(255,255,255,0.75)' : theme.systemText
50
+ const iconColor = isOutbound ? theme.primaryColor : '#ffffff'
51
+
52
+ const onToggle = () => {
53
+ if (isPlaying) {
54
+ void audioController.pause(key)
55
+ } else {
56
+ void audioController.play(key, attachment.url)
57
+ }
58
+ }
59
+
60
+ const onSeek = (e: GestureResponderEvent) => {
61
+ if (duration <= 0 || trackWidth <= 0) return
62
+ const ratio = Math.max(0, Math.min(1, e.nativeEvent.locationX / trackWidth))
63
+ void audioController.seek(key, ratio * duration)
64
+ }
65
+
66
+ const onTrackLayout = (e: LayoutChangeEvent) => {
67
+ setTrackWidth(e.nativeEvent.layout.width)
68
+ }
69
+
70
+ const timeLabel = duration > 0
71
+ ? `${formatTime(position)} / ${formatTime(duration)}`
72
+ : isError
73
+ ? 'Не удалось воспроизвести'
74
+ : formatTime(position)
75
+
76
+ return (
77
+ <View style={styles.root}>
78
+ <TouchableOpacity
79
+ style={[styles.button, { backgroundColor: accent }]}
80
+ onPress={onToggle}
81
+ activeOpacity={0.8}
82
+ disabled={isLoading}
83
+ >
84
+ {isLoading ? (
85
+ <ActivityIndicator size="small" color={iconColor} />
86
+ ) : isPlaying ? (
87
+ <PauseIcon color={iconColor} />
88
+ ) : (
89
+ <PlayIcon color={iconColor} />
90
+ )}
91
+ </TouchableOpacity>
92
+
93
+ <View style={styles.body}>
94
+ <TouchableOpacity
95
+ activeOpacity={1}
96
+ onPress={onSeek}
97
+ onLayout={onTrackLayout}
98
+ style={styles.trackWrap}
99
+ >
100
+ <View style={[styles.track, { backgroundColor: trackBg }]}>
101
+ <View
102
+ style={[styles.trackFill, { backgroundColor: accent, width: `${progress * 100}%` }]}
103
+ />
104
+ <View
105
+ style={[
106
+ styles.thumb,
107
+ { backgroundColor: accent, left: `${progress * 100}%` },
108
+ ]}
109
+ />
110
+ </View>
111
+ </TouchableOpacity>
112
+ <Text style={[styles.time, { color: timeColor }]}>{timeLabel}</Text>
113
+ </View>
114
+ </View>
115
+ )
116
+ }
117
+
118
+ function PlayIcon({ color }: { color: string }) {
119
+ return <View style={[styles.playTriangle, { borderLeftColor: color }]} />
120
+ }
121
+
122
+ function PauseIcon({ color }: { color: string }) {
123
+ return (
124
+ <View style={styles.pauseWrap}>
125
+ <View style={[styles.pauseBar, { backgroundColor: color }]} />
126
+ <View style={[styles.pauseBar, { backgroundColor: color }]} />
127
+ </View>
128
+ )
129
+ }
130
+
131
+ const BUTTON_SIZE = 40
132
+
133
+ const styles = StyleSheet.create({
134
+ root: {
135
+ flexDirection: 'row',
136
+ alignItems: 'center',
137
+ gap: 10,
138
+ minWidth: 200,
139
+ paddingVertical: 2,
140
+ },
141
+ button: {
142
+ width: BUTTON_SIZE,
143
+ height: BUTTON_SIZE,
144
+ borderRadius: BUTTON_SIZE / 2,
145
+ alignItems: 'center',
146
+ justifyContent: 'center',
147
+ flexShrink: 0,
148
+ },
149
+ body: {
150
+ flex: 1,
151
+ gap: 4,
152
+ },
153
+ trackWrap: {
154
+ paddingVertical: 8,
155
+ justifyContent: 'center',
156
+ },
157
+ track: {
158
+ height: 4,
159
+ borderRadius: 2,
160
+ },
161
+ trackFill: {
162
+ height: 4,
163
+ borderRadius: 2,
164
+ },
165
+ thumb: {
166
+ position: 'absolute',
167
+ top: -3,
168
+ width: 10,
169
+ height: 10,
170
+ borderRadius: 5,
171
+ marginLeft: -5,
172
+ },
173
+ time: {
174
+ fontSize: 11,
175
+ lineHeight: 14,
176
+ },
177
+ playTriangle: {
178
+ width: 0,
179
+ height: 0,
180
+ backgroundColor: 'transparent',
181
+ borderStyle: 'solid',
182
+ borderTopWidth: 7,
183
+ borderBottomWidth: 7,
184
+ borderLeftWidth: 12,
185
+ borderTopColor: 'transparent',
186
+ borderBottomColor: 'transparent',
187
+ marginLeft: 3,
188
+ },
189
+ pauseWrap: {
190
+ flexDirection: 'row',
191
+ gap: 4,
192
+ },
193
+ pauseBar: {
194
+ width: 4,
195
+ height: 14,
196
+ borderRadius: 1,
197
+ },
198
+ })
@@ -1,6 +1,8 @@
1
1
  import React from 'react'
2
2
  import { Dimensions, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
3
3
  import { attachmentDisplayName } from '../attachmentUtils'
4
+ import { audioController } from '../audio/audioController'
5
+ import { AudioMessage } from './AudioMessage'
4
6
  import type { ChatAttachment, ChatButton, ChatMessage } from '../types'
5
7
  import type { ChatTheme } from '../theme'
6
8
 
@@ -123,6 +125,10 @@ function AttachmentView({
123
125
  )
124
126
  }
125
127
 
128
+ if (attachment.type === 'audio' && audioController.isAvailable) {
129
+ return <AudioMessage attachment={attachment} isOutbound={isOutbound} theme={theme} />
130
+ }
131
+
126
132
  const nameColor = isOutbound ? theme.outboundText : theme.inboundText
127
133
  const sizeColor = isOutbound ? 'rgba(255,255,255,0.65)' : theme.systemText
128
134
  const displayName = attachmentDisplayName(attachment)
@@ -0,0 +1,54 @@
1
+ import { NativeEventEmitter, NativeModules, TurboModuleRegistry } from 'react-native'
2
+ import type { EventSubscription, TurboModule } from 'react-native'
3
+
4
+ export type NativeAudioState =
5
+ | 'loading'
6
+ | 'playing'
7
+ | 'paused'
8
+ | 'ended'
9
+ | 'stopped'
10
+ | 'error'
11
+
12
+ export interface AudioStateEvent {
13
+ key: string
14
+ positionMillis: number
15
+ durationMillis: number
16
+ state: NativeAudioState
17
+ }
18
+
19
+ export interface Spec extends TurboModule {
20
+ play(key: string, url: string, headers: Record<string, string>): Promise<void>
21
+ pause(key: string): Promise<void>
22
+ seek(key: string, positionMillis: number): Promise<void>
23
+ stop(key: string): Promise<void>
24
+ addListener(eventName: string): void
25
+ removeListeners(count: number): void
26
+ }
27
+
28
+ const MODULE_NAME = 'ChatSdkAudioPlayer'
29
+
30
+ function loadModule(): Spec | null {
31
+ try {
32
+ return TurboModuleRegistry.get<Spec>(MODULE_NAME)
33
+ } catch {
34
+ return (NativeModules[MODULE_NAME] as Spec | undefined) ?? null
35
+ }
36
+ }
37
+
38
+ const moduleRef = loadModule()
39
+
40
+ export default moduleRef
41
+
42
+ let emitter: NativeEventEmitter | null = null
43
+
44
+ function getEmitter(): NativeEventEmitter | null {
45
+ if (!moduleRef) return null
46
+ if (!emitter) emitter = new NativeEventEmitter(moduleRef as unknown as never)
47
+ return emitter
48
+ }
49
+
50
+ export function onAudioState(
51
+ handler: (event: AudioStateEvent) => void,
52
+ ): EventSubscription | null {
53
+ return getEmitter()?.addListener('ChatSdkAudioState', handler) ?? null
54
+ }