@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.
- package/CHANGELOG.md +26 -0
- package/README.md +69 -11
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkAudioPlayerModule.kt +201 -0
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkPackage.kt +1 -0
- package/ios/ChatSdkAudioPlayer.m +25 -0
- package/ios/ChatSdkAudioPlayer.swift +193 -0
- package/lib/commonjs/ChatSDK.js +16 -3
- package/lib/commonjs/ChatSDK.js.map +1 -1
- package/lib/commonjs/api.js +29 -0
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/audio/audioController.js +145 -0
- package/lib/commonjs/audio/audioController.js.map +1 -0
- package/lib/commonjs/components/AudioMessage.js +198 -0
- package/lib/commonjs/components/AudioMessage.js.map +1 -0
- package/lib/commonjs/components/MessageBubble.js +9 -0
- package/lib/commonjs/components/MessageBubble.js.map +1 -1
- package/lib/commonjs/native/NativeChatSdkAudioPlayer.js +28 -0
- package/lib/commonjs/native/NativeChatSdkAudioPlayer.js.map +1 -0
- package/lib/module/ChatSDK.js +16 -3
- package/lib/module/ChatSDK.js.map +1 -1
- package/lib/module/api.js +29 -0
- package/lib/module/api.js.map +1 -1
- package/lib/module/audio/audioController.js +140 -0
- package/lib/module/audio/audioController.js.map +1 -0
- package/lib/module/components/AudioMessage.js +193 -0
- package/lib/module/components/AudioMessage.js.map +1 -0
- package/lib/module/components/MessageBubble.js +9 -0
- package/lib/module/components/MessageBubble.js.map +1 -1
- package/lib/module/native/NativeChatSdkAudioPlayer.js +23 -0
- package/lib/module/native/NativeChatSdkAudioPlayer.js.map +1 -0
- package/lib/typescript/commonjs/ChatSDK.d.ts +3 -0
- package/lib/typescript/commonjs/ChatSDK.d.ts.map +1 -1
- package/lib/typescript/commonjs/api.d.ts +3 -0
- package/lib/typescript/commonjs/api.d.ts.map +1 -1
- package/lib/typescript/commonjs/audio/audioController.d.ts +25 -0
- package/lib/typescript/commonjs/audio/audioController.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/AudioMessage.d.ts +11 -0
- package/lib/typescript/commonjs/components/AudioMessage.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/commonjs/native/NativeChatSdkAudioPlayer.d.ts +20 -0
- package/lib/typescript/commonjs/native/NativeChatSdkAudioPlayer.d.ts.map +1 -0
- package/lib/typescript/module/ChatSDK.d.ts +3 -0
- package/lib/typescript/module/ChatSDK.d.ts.map +1 -1
- package/lib/typescript/module/api.d.ts +3 -0
- package/lib/typescript/module/api.d.ts.map +1 -1
- package/lib/typescript/module/audio/audioController.d.ts +25 -0
- package/lib/typescript/module/audio/audioController.d.ts.map +1 -0
- package/lib/typescript/module/components/AudioMessage.d.ts +11 -0
- package/lib/typescript/module/components/AudioMessage.d.ts.map +1 -0
- package/lib/typescript/module/components/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/module/native/NativeChatSdkAudioPlayer.d.ts +20 -0
- package/lib/typescript/module/native/NativeChatSdkAudioPlayer.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/ChatSDK.ts +18 -3
- package/src/api.ts +37 -0
- package/src/audio/audioController.ts +144 -0
- package/src/components/AudioMessage.tsx +198 -0
- package/src/components/MessageBubble.tsx +6 -0
- 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
|
+
}
|