Files
forkmessager/client-web/src/core/utils/utils.ts
Халимов Рустам a42007df2d Дизайн
2026-04-02 14:57:32 +03:00

145 lines
5.0 KiB
TypeScript

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-[#3096e5] to-[#1e6fb3]';
}
// Waveform cache so we don't decode the same audio twice
const waveformCache = new Map<string, number[]>();
/**
* 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<number[]> {
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, otherwise let it be a relative path which the browser
// will resolve against the current origin (port).
const baseUrl = import.meta.env.VITE_API_URL || '';
return `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}`;
}