feat(communications): add play/pause for call voice messages
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user