完善家长留言录音与会话展示
This commit is contained in:
@@ -85,8 +85,8 @@ const DEFAULT_RECORD_PROFILE: RecordProfile = {
|
||||
const COMPAT_RECORD_PROFILE: RecordProfile = {
|
||||
format: 'aac',
|
||||
mimeType: 'audio/aac',
|
||||
sampleRate: 16000,
|
||||
encodeBitRate: 64000,
|
||||
sampleRate: 44100,
|
||||
encodeBitRate: 192000,
|
||||
}
|
||||
|
||||
const FALLBACK_RUNTIME_INFO: VoiceRuntimeInfo = {
|
||||
@@ -105,6 +105,11 @@ const FALLBACK_RUNTIME_INFO: VoiceRuntimeInfo = {
|
||||
}
|
||||
|
||||
const CHAT_BOTTOM_ANCHOR_ID = 'chat-bottom'
|
||||
const RECORD_LONG_PRESS_DELAY_MS = 350
|
||||
|
||||
type LoadChatOptions = {
|
||||
showLoading?: boolean
|
||||
}
|
||||
|
||||
function compareVersion(left: string, right: string): number {
|
||||
const leftParts = left.split('.').map((item) => Number(item) || 0)
|
||||
@@ -162,7 +167,7 @@ function readVoiceRuntimeInfo(): VoiceRuntimeInfo {
|
||||
}
|
||||
|
||||
function getPreferredRecordProfile(runtime: VoiceRuntimeInfo): RecordProfile {
|
||||
if (runtime.isHarmony || !runtime.supportsMp3Record) return COMPAT_RECORD_PROFILE
|
||||
if (runtime.isIOS || runtime.isHarmony || !runtime.supportsMp3Record) return COMPAT_RECORD_PROFILE
|
||||
return DEFAULT_RECORD_PROFILE
|
||||
}
|
||||
|
||||
@@ -230,13 +235,21 @@ export default function ChatDetail() {
|
||||
const audioContextRef = useRef<Taro.InnerAudioContext | null>(null)
|
||||
const recorderManagerRef = useRef<Taro.RecorderManager | null>(null)
|
||||
const activeConversationIdRef = useRef(initialConversationId)
|
||||
const latestLoadChatDataRef = useRef<(conversationId: number) => Promise<void>>(async () => {})
|
||||
const latestLoadChatDataRef = useRef<(conversationId: number, options?: LoadChatOptions) => Promise<void>>(
|
||||
async () => {}
|
||||
)
|
||||
const recordingChildIdRef = useRef<number | null>(null)
|
||||
const recordProfileRef = useRef<RecordProfile>(DEFAULT_RECORD_PROFILE)
|
||||
const runtimeInfoRef = useRef<VoiceRuntimeInfo>(FALLBACK_RUNTIME_INFO)
|
||||
const recordStartedAtRef = useRef<number>(0)
|
||||
const playingMessageIdRef = useRef<number | null>(null)
|
||||
const scrollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const recordStartTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const hasLoadedChatRef = useRef(false)
|
||||
const skipNextAutoLoadRef = useRef(false)
|
||||
const recordPressedRef = useRef(false)
|
||||
const recordStartPendingRef = useRef(false)
|
||||
const recordingActiveRef = useRef(false)
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (scrollTimerRef.current) {
|
||||
@@ -311,6 +324,10 @@ export default function ChatDetail() {
|
||||
clearTimeout(scrollTimerRef.current)
|
||||
scrollTimerRef.current = null
|
||||
}
|
||||
if (recordStartTimerRef.current) {
|
||||
clearTimeout(recordStartTimerRef.current)
|
||||
recordStartTimerRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -323,11 +340,18 @@ export default function ChatDetail() {
|
||||
}, [playingMessageId])
|
||||
|
||||
useEffect(() => {
|
||||
if (skipNextAutoLoadRef.current) {
|
||||
skipNextAutoLoadRef.current = false
|
||||
return
|
||||
}
|
||||
void loadChatData(activeConversationId)
|
||||
}, [activeConversationId, conversationSource, conversationPeerKind])
|
||||
|
||||
const loadChatData = async (conversationId: number) => {
|
||||
setLoading(true)
|
||||
const loadChatData = async (conversationId: number, options: LoadChatOptions = {}) => {
|
||||
const shouldShowLoading = options.showLoading ?? !hasLoadedChatRef.current
|
||||
if (shouldShowLoading) {
|
||||
setLoading(true)
|
||||
}
|
||||
try {
|
||||
const char = await getCharacter({
|
||||
conversationId,
|
||||
@@ -352,6 +376,7 @@ export default function ChatDetail() {
|
||||
if (msgs.length > 0) {
|
||||
scrollToBottom()
|
||||
}
|
||||
hasLoadedChatRef.current = true
|
||||
} catch (error: any) {
|
||||
console.error('[chat-detail] load failed:', error)
|
||||
Taro.showToast({
|
||||
@@ -375,6 +400,8 @@ export default function ChatDetail() {
|
||||
recorderManagerRef.current = recorderManager
|
||||
|
||||
recorderManager.onStart(() => {
|
||||
recordingActiveRef.current = true
|
||||
recordStartPendingRef.current = false
|
||||
console.info('[record] started', {
|
||||
profile: recordProfileRef.current,
|
||||
runtime: runtimeInfoRef.current,
|
||||
@@ -384,6 +411,8 @@ export default function ChatDetail() {
|
||||
recorderManager.onError((error) => {
|
||||
recordingChildIdRef.current = null
|
||||
recordStartedAtRef.current = 0
|
||||
recordStartPendingRef.current = false
|
||||
recordingActiveRef.current = false
|
||||
setRecording(false)
|
||||
setRecordHint('长按开始留言')
|
||||
console.error('[record] error', {
|
||||
@@ -400,6 +429,8 @@ export default function ChatDetail() {
|
||||
const fallbackDurationMs = startedAt > 0 ? Date.now() - startedAt : 0
|
||||
recordingChildIdRef.current = null
|
||||
recordStartedAtRef.current = 0
|
||||
recordStartPendingRef.current = false
|
||||
recordingActiveRef.current = false
|
||||
setRecording(false)
|
||||
setRecordHint('长按开始留言')
|
||||
|
||||
@@ -447,9 +478,11 @@ export default function ChatDetail() {
|
||||
messageId: response.message?.id,
|
||||
})
|
||||
if (response.conversation_id && response.conversation_id !== activeConversationIdRef.current) {
|
||||
skipNextAutoLoadRef.current = true
|
||||
setActiveConversationId(response.conversation_id)
|
||||
await latestLoadChatDataRef.current(response.conversation_id, { showLoading: false })
|
||||
} else {
|
||||
await latestLoadChatDataRef.current(activeConversationIdRef.current)
|
||||
await latestLoadChatDataRef.current(activeConversationIdRef.current, { showLoading: false })
|
||||
}
|
||||
Taro.showToast({ title: '留言已发送', icon: 'success' })
|
||||
} catch (error: any) {
|
||||
@@ -502,8 +535,15 @@ export default function ChatDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleRecordStart = async () => {
|
||||
if (sending || recording) return
|
||||
const resetRecordStartTimer = () => {
|
||||
if (recordStartTimerRef.current) {
|
||||
clearTimeout(recordStartTimerRef.current)
|
||||
recordStartTimerRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
const startRecording = async () => {
|
||||
if (sending || recordingActiveRef.current || recordStartPendingRef.current) return
|
||||
if (!character.canSend || !character.childId) {
|
||||
Taro.showToast({ title: '当前会话不支持发送', icon: 'none' })
|
||||
return
|
||||
@@ -515,8 +555,17 @@ export default function ChatDetail() {
|
||||
return
|
||||
}
|
||||
|
||||
recordStartPendingRef.current = true
|
||||
const hasPermission = await ensureRecordPermission()
|
||||
if (!hasPermission) return
|
||||
if (!recordPressedRef.current) {
|
||||
recordStartPendingRef.current = false
|
||||
console.info('[record] start skipped: touch already ended')
|
||||
return
|
||||
}
|
||||
if (!hasPermission) {
|
||||
recordStartPendingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
const runtime = readVoiceRuntimeInfo()
|
||||
const profile = getPreferredRecordProfile(runtime)
|
||||
@@ -524,6 +573,7 @@ export default function ChatDetail() {
|
||||
recordProfileRef.current = profile
|
||||
recordingChildIdRef.current = character.childId
|
||||
recordStartedAtRef.current = Date.now()
|
||||
recordingActiveRef.current = true
|
||||
setRecording(true)
|
||||
setRecordHint('松开发送留言')
|
||||
console.info('[record] start request', {
|
||||
@@ -542,6 +592,8 @@ export default function ChatDetail() {
|
||||
} catch (error: any) {
|
||||
recordingChildIdRef.current = null
|
||||
recordStartedAtRef.current = 0
|
||||
recordStartPendingRef.current = false
|
||||
recordingActiveRef.current = false
|
||||
setRecording(false)
|
||||
setRecordHint('长按开始留言')
|
||||
console.error('[record] start failed:', {
|
||||
@@ -556,11 +608,49 @@ export default function ChatDetail() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleRecordTouchStart = () => {
|
||||
if (sending || recordingActiveRef.current || recordStartPendingRef.current) return
|
||||
recordPressedRef.current = true
|
||||
resetRecordStartTimer()
|
||||
setRecordHint('继续按住开始录音')
|
||||
console.info('[record] touch start', {
|
||||
delayMs: RECORD_LONG_PRESS_DELAY_MS,
|
||||
runtime: runtimeInfoRef.current,
|
||||
})
|
||||
recordStartTimerRef.current = setTimeout(() => {
|
||||
recordStartTimerRef.current = null
|
||||
if (!recordPressedRef.current) return
|
||||
console.info('[record] long press threshold reached')
|
||||
void startRecording()
|
||||
}, RECORD_LONG_PRESS_DELAY_MS)
|
||||
}
|
||||
|
||||
const handleRecordStop = () => {
|
||||
if (!recording) return
|
||||
recordPressedRef.current = false
|
||||
resetRecordStartTimer()
|
||||
console.info('[record] touch end', {
|
||||
active: recordingActiveRef.current,
|
||||
pending: recordStartPendingRef.current,
|
||||
elapsedMs: recordStartedAtRef.current > 0 ? Date.now() - recordStartedAtRef.current : 0,
|
||||
})
|
||||
|
||||
if (recordStartPendingRef.current && !recordingActiveRef.current) {
|
||||
setRecordHint('长按开始留言')
|
||||
return
|
||||
}
|
||||
if (!recordingActiveRef.current) {
|
||||
setRecordHint('长按开始留言')
|
||||
return
|
||||
}
|
||||
|
||||
recordingActiveRef.current = false
|
||||
const recorderManager = recorderManagerRef.current
|
||||
if (!recorderManager) {
|
||||
console.warn('[record] stop skipped: recorder unavailable')
|
||||
recordingChildIdRef.current = null
|
||||
recordStartedAtRef.current = 0
|
||||
setRecording(false)
|
||||
setRecordHint('长按开始留言')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -568,7 +658,21 @@ export default function ChatDetail() {
|
||||
console.info('[record] stop request', {
|
||||
elapsedMs: recordStartedAtRef.current > 0 ? Date.now() - recordStartedAtRef.current : 0,
|
||||
})
|
||||
recorderManager.stop()
|
||||
try {
|
||||
recorderManager.stop()
|
||||
} catch (error) {
|
||||
recordingChildIdRef.current = null
|
||||
recordStartedAtRef.current = 0
|
||||
recordStartPendingRef.current = false
|
||||
setRecording(false)
|
||||
setRecordHint('长按开始留言')
|
||||
console.error('[record] stop failed', {
|
||||
error,
|
||||
profile: recordProfileRef.current,
|
||||
runtime: runtimeInfoRef.current,
|
||||
})
|
||||
Taro.showToast({ title: '录音停止失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const handlePlayAudio = (message: ChatMessage) => {
|
||||
@@ -706,7 +810,7 @@ export default function ChatDetail() {
|
||||
</View>
|
||||
<View
|
||||
className={`composer-record ${recording ? 'recording' : ''} ${sending ? 'disabled' : ''}`}
|
||||
onLongPress={handleRecordStart}
|
||||
onTouchStart={handleRecordTouchStart}
|
||||
onTouchEnd={handleRecordStop}
|
||||
onTouchCancel={handleRecordStop}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user