feat(communications): add play/pause for call voice messages

This commit is contained in:
Ruslan Bakiev
2026-02-23 18:42:21 +07:00
parent 2a3d18f326
commit 5fb8113ed7

View File

@@ -492,6 +492,8 @@ let pilotWaveRecordPlugin: any = null;
let pilotWaveMicSession: { onDestroy: () => void; onEnd: () => void } | null = null;
const commCallWaveHosts = new Map<string, HTMLDivElement>();
const commCallWaveSurfers = new Map<string, any>();
const commCallPlayableById = ref<Record<string, boolean>>({});
const commCallPlayingById = ref<Record<string, boolean>>({});
const callTranscriptOpen = ref<Record<string, boolean>>({});
const callTranscriptLoading = ref<Record<string, boolean>>({});
const callTranscriptText = ref<Record<string, string>>({});
@@ -1233,6 +1235,12 @@ function destroyCommCallWave(itemId: string) {
if (!ws) return;
ws.destroy();
commCallWaveSurfers.delete(itemId);
const nextPlayable = { ...commCallPlayableById.value };
delete nextPlayable[itemId];
commCallPlayableById.value = nextPlayable;
const nextPlaying = { ...commCallPlayingById.value };
delete nextPlaying[itemId];
commCallPlayingById.value = nextPlaying;
}
function destroyAllCommCallWaves() {
@@ -1242,6 +1250,31 @@ function destroyAllCommCallWaves() {
commCallWaveHosts.clear();
}
function setCommCallPlaying(itemId: string, value: boolean) {
commCallPlayingById.value = {
...commCallPlayingById.value,
[itemId]: value,
};
}
function isCommCallPlaying(itemId: string) {
return Boolean(commCallPlayingById.value[itemId]);
}
function isCommCallPlayable(item: CommItem) {
const known = commCallPlayableById.value[item.id];
if (typeof known === "boolean") return known;
return Boolean(getCallAudioUrl(item));
}
function pauseOtherCommCallWaves(currentItemId: string) {
for (const [itemId, ws] of commCallWaveSurfers.entries()) {
if (itemId === currentItemId) continue;
ws.pause?.();
setCommCallPlaying(itemId, false);
}
}
function parseDurationToSeconds(raw?: string) {
if (!raw) return 0;
const text = raw.trim().toLowerCase();
@@ -1315,6 +1348,7 @@ async function ensureCommCallWave(itemId: string) {
parseDurationToSeconds(callItem.duration) ||
Math.max(8, Math.min(120, Math.round(((callItem.transcript ?? []).join(" ").length || callItem.text.length) / 10)));
const peaks = buildCallWavePeaks(callItem, 360);
const audioUrl = getCallAudioUrl(callItem);
const ws = WaveSurfer.create({
container: host,
@@ -1322,12 +1356,32 @@ async function ensureCommCallWave(itemId: string) {
waveColor: "rgba(180, 206, 255, 0.88)",
progressColor: "rgba(118, 157, 248, 0.95)",
cursorWidth: 0,
interact: false,
interact: Boolean(audioUrl),
normalize: true,
barWidth: 0,
});
await ws.load("", [peaks], durationSeconds);
ws.on("play", () => setCommCallPlaying(itemId, true));
ws.on("pause", () => setCommCallPlaying(itemId, false));
ws.on("finish", () => setCommCallPlaying(itemId, false));
let playable = false;
if (audioUrl) {
try {
await ws.load(audioUrl, [peaks], durationSeconds);
playable = true;
} catch {
await ws.load("", [peaks], durationSeconds);
playable = false;
}
} else {
await ws.load("", [peaks], durationSeconds);
}
commCallPlayableById.value = {
...commCallPlayableById.value,
[itemId]: playable,
};
commCallWaveSurfers.set(itemId, ws);
}
@@ -4213,6 +4267,20 @@ function isCallTranscriptOpen(itemId: string) {
return Boolean(callTranscriptOpen.value[itemId]);
}
async function toggleCommCallPlayback(item: CommItem) {
if (!isCommCallPlayable(item)) return;
const itemId = item.id;
await ensureCommCallWave(itemId);
const ws = commCallWaveSurfers.get(itemId);
if (!ws) return;
if (isCommCallPlaying(itemId)) {
ws.pause?.();
return;
}
pauseOtherCommCallWaves(itemId);
await ws.play?.();
}
function channelIcon(channel: "All" | CommItem["channel"]) {
if (channel === "All") return "all";
if (channel === "Telegram") return "telegram";
@@ -5039,7 +5107,27 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
<span v-if="entry.item.duration"> · {{ entry.item.duration }}</span>
</p>
<div class="comm-call-wave mb-2" :ref="(el) => setCommCallWaveHost(entry.item.id, el as Element | null)" />
<div class="mt-2 flex" :class="entry.item.direction === 'out' ? 'justify-end' : 'justify-start'">
<div class="mt-2 flex items-center gap-2" :class="entry.item.direction === 'out' ? 'justify-end' : 'justify-start'">
<button
class="call-play-toggle"
:disabled="!isCommCallPlayable(entry.item)"
:title="isCommCallPlayable(entry.item) ? 'Play voice message' : 'Audio unavailable'"
@click="toggleCommCallPlayback(entry.item)"
>
<svg v-if="!isCommCallPlaying(entry.item.id)" viewBox="0 0 20 20" class="h-3.5 w-3.5">
<path
fill="currentColor"
d="M6.5 4.75a.75.75 0 0 1 1.12-.65l7.5 4.25a.75.75 0 0 1 0 1.3l-7.5 4.25a.75.75 0 0 1-1.12-.65v-8.5Z"
/>
</svg>
<svg v-else viewBox="0 0 20 20" class="h-3.5 w-3.5">
<path
fill="currentColor"
d="M6.75 4.5a.75.75 0 0 1 .75.75v9.5a.75.75 0 0 1-1.5 0v-9.5a.75.75 0 0 1 .75-.75Zm6.5 0a.75.75 0 0 1 .75.75v9.5a.75.75 0 0 1-1.5 0v-9.5a.75.75 0 0 1 .75-.75Z"
/>
</svg>
<span>{{ isCommCallPlaying(entry.item.id) ? "Pause" : "Play" }}</span>
</button>
<button class="call-transcript-toggle" @click="toggleCallTranscript(entry.item)">
<span>
{{
@@ -5712,6 +5800,28 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
background: color-mix(in oklab, var(--color-base-100) 90%, transparent);
}
.call-play-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid color-mix(in oklab, var(--color-base-content) 18%, transparent);
border-radius: 999px;
padding: 4px 10px;
font-size: 11px;
font-weight: 500;
color: color-mix(in oklab, var(--color-base-content) 72%, transparent);
background: color-mix(in oklab, var(--color-base-100) 90%, transparent);
}
.call-play-toggle:hover:not(:disabled) {
background: color-mix(in oklab, var(--color-base-100) 72%, var(--color-base-200));
}
.call-play-toggle:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.call-transcript-toggle:hover {
background: color-mix(in oklab, var(--color-base-100) 72%, var(--color-base-200));
}