import { clsx, type ClassValue } from 'clsx'; export function cn(...inputs: ClassValue[]) { return clsx(inputs); } export function formatTime(date: string | Date, lang: string = 'ru'): string { const d = new Date(date); return d.toLocaleTimeString(lang === 'ru' ? 'ru-RU' : 'en-US', { hour: '2-digit', minute: '2-digit' }); } export function formatDate(date: string | Date, lang: string = 'ru'): string { const d = new Date(date); const now = new Date(); const diff = now.getTime() - d.getTime(); const days = Math.floor(diff / (1000 * 60 * 60 * 24)); if (days === 0) return lang === 'ru' ? 'Сегодня' : 'Today'; if (days === 1) return lang === 'ru' ? 'Вчера' : 'Yesterday'; if (days < 7) { const weekDaysRu = ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота']; const weekDaysEn = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; return (lang === 'ru' ? weekDaysRu : weekDaysEn)[d.getDay()]; } return d.toLocaleDateString(lang === 'ru' ? 'ru-RU' : 'en-US', { day: 'numeric', month: 'long', year: days > 365 ? 'numeric' : undefined, }); } export function formatLastSeen(date: string | Date, lang: string = 'ru'): string { const d = new Date(date); const now = new Date(); const diff = now.getTime() - d.getTime(); const minutes = Math.floor(diff / (1000 * 60)); if (minutes < 1) return lang === 'ru' ? 'только что' : 'just now'; if (minutes < 60) return lang === 'ru' ? `${minutes} мин. назад` : `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return lang === 'ru' ? `${hours} ч. назад` : `${hours}h ago`; const at = lang === 'ru' ? ' в ' : ' at '; return formatDate(date, lang) + at + formatTime(date, lang); } /** * Strips markdown syntax (**bold**, *italic*, _italic_, ~strikethrough~, `code`) * and returns plain text for use in previews. */ export function stripMarkdown(text: string): string { if (!text) return text; return text .replace(/\*\*([\s\S]*?)\*\*/g, '$1') .replace(/\*([\s\S]*?)\*/g, '$1') .replace(/_([\s\S]*?)_/g, '$1') .replace(/~([\s\S]*?)~/g, '$1') .replace(/`([\s\S]*?)`/g, '$1'); } export function getInitials(name: string): string { if (!name) return '?'; const parts = name.trim().split(/\s+/); if (parts.length >= 2) { return (parts[0][0] + parts[1][0]).toUpperCase(); } return parts[0].slice(0, 2).toUpperCase(); } export function generateAvatarColor(_name: string): string { return 'bg-gradient-to-br from-primary to-primary-container'; } // Waveform cache so we don't decode the same audio twice const waveformCache = new Map(); /** * Decodes an audio file from a URL and extracts normalized waveform peak values. * Returns an array of `bars` values in [0, 1]. */ export async function extractWaveform(url: string, bars: number = 28): Promise { const cached = waveformCache.get(url); if (cached) return cached; let audioCtx: any; try { const response = await fetch(url); const arrayBuffer = await response.arrayBuffer(); const OfflineCtx = window.OfflineAudioContext || (window as any).webkitOfflineAudioContext; if (OfflineCtx) { audioCtx = new OfflineCtx(1, 1, 44100); } else { audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)(); } // We only need the buffer, so decode it const audioBuffer = await audioCtx!.decodeAudioData(arrayBuffer); // If it was a regular AudioContext, close it. if (audioCtx.close) { await audioCtx.close(); } audioCtx = undefined; const channelData = audioBuffer.getChannelData(0); const samplesPerBar = Math.floor(channelData.length / bars); const peaks: number[] = []; for (let i = 0; i < bars; i++) { let peak = 0; const start = i * samplesPerBar; // Sample a subset for performance const step = Math.max(1, Math.floor(samplesPerBar / 200)); for (let j = 0; j < samplesPerBar; j += step) { const abs = Math.abs(channelData[start + j] || 0); if (abs > peak) peak = abs; } peaks.push(peak); } // Normalize to [0, 1] const max = Math.max(...peaks, 0.01); const normalized = peaks.map(p => p / max); waveformCache.set(url, normalized); return normalized; } catch { // Close leaked AudioContext if any if (audioCtx && audioCtx.close) audioCtx.close().catch(() => {}); // On error, return uniform bars return Array(bars).fill(0.5); } } export function getMediaUrl(url: string | null | undefined): string { if (!url) return ''; if (url.startsWith('http') || url.startsWith('blob:') || url.startsWith('data:')) return url; // Use VITE_API_URL if defined, but avoid duplicating '/api' const baseUrl = (import.meta.env.VITE_API_URL || '').replace(/\/api$/, ''); return `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}`; }