Split chat component into smaller components

This commit is contained in:
2026-03-07 18:06:45 +01:00
parent 901df84d13
commit 66246e4e16
15 changed files with 2831 additions and 2289 deletions

View File

@@ -120,6 +120,16 @@ export class ChatAudioPlayerComponent implements OnDestroy {
this.isMuted.set(false);
this.volumePercent.set(storedVolume);
this.lastNonZeroVolume.set(storedVolume);
requestAnimationFrame(() => {
const audio = this.audioRef?.nativeElement;
if (!audio)
return;
audio.load();
this.applyAudioVolume(storedVolume);
});
});
}
@@ -150,6 +160,7 @@ export class ChatAudioPlayerComponent implements OnDestroy {
this.waveformExpanded.set(nextExpanded);
if (nextExpanded) {
this.waveformUnavailable.set(false);
requestAnimationFrame(() => {
void this.ensureWaveformLoaded();
});
@@ -165,6 +176,12 @@ export class ChatAudioPlayerComponent implements OnDestroy {
this.applyAudioVolume(this.volumePercent());
this.durationSeconds.set(Number.isFinite(audio.duration) ? audio.duration : 0);
this.currentTimeSeconds.set(audio.currentTime || 0);
if (this.waveformExpanded() && !this.waveSurfer && !this.waveformLoading()) {
requestAnimationFrame(() => {
void this.ensureWaveformLoaded();
});
}
}
onTimeUpdate(): void {
@@ -266,7 +283,7 @@ export class ChatAudioPlayerComponent implements OnDestroy {
}
private async ensureWaveformLoaded(): Promise<void> {
if (this.waveformLoading() || this.waveSurfer || this.waveformUnavailable())
if (this.waveformLoading() || this.waveSurfer)
return;
const source = this.src();
@@ -277,8 +294,18 @@ export class ChatAudioPlayerComponent implements OnDestroy {
return;
this.waveformLoading.set(true);
this.waveformUnavailable.set(false);
try {
await this.ensureAudioMetadata(audio);
await this.waitForNextPaint();
if (!this.waveformExpanded()) {
this.waveformLoading.set(false);
return;
}
const { default: WaveSurfer } = await import('wavesurfer.js');
this.waveSurfer = WaveSurfer.create({
@@ -309,14 +336,41 @@ export class ChatAudioPlayerComponent implements OnDestroy {
});
} catch {
this.destroyWaveSurfer();
this.waveformLoading.set(false);
this.waveformUnavailable.set(true);
} finally {
if (this.waveformUnavailable()) {
this.waveformLoading.set(false);
}
}
}
private ensureAudioMetadata(audio: HTMLAudioElement): Promise<void> {
if (audio.readyState >= HTMLMediaElement.HAVE_METADATA)
return Promise.resolve();
return new Promise((resolve, reject) => {
const handleLoadedMetadata = (): void => {
cleanup();
resolve();
};
const handleCanPlay = (): void => {
cleanup();
resolve();
};
const handleError = (): void => {
cleanup();
reject(new Error('Failed to load audio metadata'));
};
const cleanup = (): void => {
audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
audio.removeEventListener('canplay', handleCanPlay);
audio.removeEventListener('error', handleError);
};
audio.addEventListener('loadedmetadata', handleLoadedMetadata, { once: true });
audio.addEventListener('canplay', handleCanPlay, { once: true });
audio.addEventListener('error', handleError, { once: true });
audio.load();
});
}
private destroyWaveSurfer(): void {
if (!this.waveSurfer)
return;
@@ -363,6 +417,14 @@ export class ChatAudioPlayerComponent implements OnDestroy {
].join('');
}
private waitForNextPaint(): Promise<void> {
return new Promise((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(() => resolve());
});
});
}
formatTime(seconds: number): string {
if (!Number.isFinite(seconds) || seconds < 0)
return '0:00';