Добавлены новые зависимости для работы с эмодзи и улучшена структура базы данных. Реализована модель сообщений и обновлены компоненты для поддержки новых функций мессенджера. Обновлены запросы и мутации для работы с сообщениями и чатом.

This commit is contained in:
Bivekich
2025-07-16 22:07:38 +03:00
parent 823ef9a28c
commit 205c9eae98
33 changed files with 3288 additions and 229 deletions

View File

@ -0,0 +1,254 @@
"use client"
import { useState, useRef, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Mic, MicOff, Square, Send, Trash2 } from 'lucide-react'
import { useAuth } from '@/hooks/useAuth'
interface VoiceRecorderProps {
onSendVoice: (audioUrl: string, duration: number) => void
}
export function VoiceRecorder({ onSendVoice }: VoiceRecorderProps) {
const { user } = useAuth()
const [isRecording, setIsRecording] = useState(false)
const [recordedAudio, setRecordedAudio] = useState<string | null>(null)
const [duration, setDuration] = useState(0)
const [isPlaying, setIsPlaying] = useState(false)
const [permission, setPermission] = useState<'granted' | 'denied' | 'prompt'>('prompt')
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const audioChunksRef = useRef<Blob[]>([])
const audioRef = useRef<HTMLAudioElement | null>(null)
const intervalRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
// Проверяем доступность микрофона
if (typeof navigator !== 'undefined' && navigator.mediaDevices) {
navigator.permissions.query({ name: 'microphone' as PermissionName }).then((result) => {
setPermission(result.state as 'granted' | 'denied' | 'prompt')
}).catch(() => {
// Если permissions API недоступен, оставляем prompt
})
}
}, [])
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
setPermission('granted')
const mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus'
})
mediaRecorderRef.current = mediaRecorder
audioChunksRef.current = []
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data)
}
}
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' })
const audioUrl = URL.createObjectURL(audioBlob)
setRecordedAudio(audioUrl)
// Останавливаем все треки для освобождения микрофона
stream.getTracks().forEach(track => track.stop())
// Останавливаем таймер
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
mediaRecorder.start()
setIsRecording(true)
setDuration(0)
// Запускаем таймер записи
intervalRef.current = setInterval(() => {
setDuration(prev => prev + 1)
}, 1000)
} catch (error) {
console.error('Error accessing microphone:', error)
setPermission('denied')
}
}
const stopRecording = () => {
if (mediaRecorderRef.current && isRecording) {
mediaRecorderRef.current.stop()
setIsRecording(false)
}
}
const cancelRecording = () => {
if (mediaRecorderRef.current && isRecording) {
mediaRecorderRef.current.stop()
setIsRecording(false)
}
setRecordedAudio(null)
setDuration(0)
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
const playRecording = () => {
if (recordedAudio) {
if (audioRef.current) {
audioRef.current.pause()
audioRef.current.currentTime = 0
}
audioRef.current = new Audio(recordedAudio)
audioRef.current.play()
setIsPlaying(true)
audioRef.current.onended = () => {
setIsPlaying(false)
}
}
}
const sendVoiceMessage = async () => {
if (!recordedAudio || !user?.id) return
try {
// Конвертируем Blob в File для загрузки
const response = await fetch(recordedAudio)
const blob = await response.blob()
const file = new File([blob], `voice-${Date.now()}.webm`, { type: 'audio/webm' })
// Загружаем в S3
const formData = new FormData()
formData.append('file', file)
formData.append('userId', user.id)
const uploadResponse = await fetch('/api/upload-voice', {
method: 'POST',
body: formData
})
if (!uploadResponse.ok) {
throw new Error('Failed to upload voice message')
}
const result = await uploadResponse.json()
// Отправляем голосовое сообщение
onSendVoice(result.url, duration)
// Очищаем состояние
setRecordedAudio(null)
setDuration(0)
} catch (error) {
console.error('Error sending voice message:', error)
}
}
const formatDuration = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}:${secs.toString().padStart(2, '0')}`
}
if (permission === 'denied') {
return (
<div className="text-white/60 text-xs text-center p-2">
Доступ к микрофону запрещен
</div>
)
}
return (
<div className="flex items-center space-x-2">
{!recordedAudio ? (
// Состояние записи
<>
{!isRecording ? (
<Button
type="button"
variant="ghost"
size="sm"
onClick={startRecording}
className="text-white/60 hover:text-white hover:bg-white/10 p-2"
>
<Mic className="h-4 w-4" />
</Button>
) : (
<>
<div className="flex items-center space-x-2 bg-red-500/20 rounded-lg px-3 py-2">
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse" />
<span className="text-white text-sm font-mono">
{formatDuration(duration)}
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={stopRecording}
className="text-white/80 hover:text-white p-1"
>
<Square className="h-3 w-3" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={cancelRecording}
className="text-red-400 hover:text-red-300 p-1"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</>
)}
</>
) : (
// Состояние воспроизведения и отправки
<div className="flex items-center space-x-2 bg-blue-500/20 rounded-lg px-3 py-2">
<Button
type="button"
variant="ghost"
size="sm"
onClick={playRecording}
className="text-blue-300 hover:text-blue-200 p-1"
>
{isPlaying ? <MicOff className="h-3 w-3" /> : <Mic className="h-3 w-3" />}
</Button>
<span className="text-white text-sm font-mono">
{formatDuration(duration)}
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={sendVoiceMessage}
className="text-green-400 hover:text-green-300 p-1"
>
<Send className="h-3 w-3" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setRecordedAudio(null)}
className="text-red-400 hover:text-red-300 p-1"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
)}
</div>
)
}