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;
|
let pilotWaveMicSession: { onDestroy: () => void; onEnd: () => void } | null = null;
|
||||||
const commCallWaveHosts = new Map<string, HTMLDivElement>();
|
const commCallWaveHosts = new Map<string, HTMLDivElement>();
|
||||||
const commCallWaveSurfers = new Map<string, any>();
|
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 callTranscriptOpen = ref<Record<string, boolean>>({});
|
||||||
const callTranscriptLoading = ref<Record<string, boolean>>({});
|
const callTranscriptLoading = ref<Record<string, boolean>>({});
|
||||||
const callTranscriptText = ref<Record<string, string>>({});
|
const callTranscriptText = ref<Record<string, string>>({});
|
||||||
@@ -1233,6 +1235,12 @@ function destroyCommCallWave(itemId: string) {
|
|||||||
if (!ws) return;
|
if (!ws) return;
|
||||||
ws.destroy();
|
ws.destroy();
|
||||||
commCallWaveSurfers.delete(itemId);
|
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() {
|
function destroyAllCommCallWaves() {
|
||||||
@@ -1242,6 +1250,31 @@ function destroyAllCommCallWaves() {
|
|||||||
commCallWaveHosts.clear();
|
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) {
|
function parseDurationToSeconds(raw?: string) {
|
||||||
if (!raw) return 0;
|
if (!raw) return 0;
|
||||||
const text = raw.trim().toLowerCase();
|
const text = raw.trim().toLowerCase();
|
||||||
@@ -1315,6 +1348,7 @@ async function ensureCommCallWave(itemId: string) {
|
|||||||
parseDurationToSeconds(callItem.duration) ||
|
parseDurationToSeconds(callItem.duration) ||
|
||||||
Math.max(8, Math.min(120, Math.round(((callItem.transcript ?? []).join(" ").length || callItem.text.length) / 10)));
|
Math.max(8, Math.min(120, Math.round(((callItem.transcript ?? []).join(" ").length || callItem.text.length) / 10)));
|
||||||
const peaks = buildCallWavePeaks(callItem, 360);
|
const peaks = buildCallWavePeaks(callItem, 360);
|
||||||
|
const audioUrl = getCallAudioUrl(callItem);
|
||||||
|
|
||||||
const ws = WaveSurfer.create({
|
const ws = WaveSurfer.create({
|
||||||
container: host,
|
container: host,
|
||||||
@@ -1322,12 +1356,32 @@ async function ensureCommCallWave(itemId: string) {
|
|||||||
waveColor: "rgba(180, 206, 255, 0.88)",
|
waveColor: "rgba(180, 206, 255, 0.88)",
|
||||||
progressColor: "rgba(118, 157, 248, 0.95)",
|
progressColor: "rgba(118, 157, 248, 0.95)",
|
||||||
cursorWidth: 0,
|
cursorWidth: 0,
|
||||||
interact: false,
|
interact: Boolean(audioUrl),
|
||||||
normalize: true,
|
normalize: true,
|
||||||
barWidth: 0,
|
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);
|
commCallWaveSurfers.set(itemId, ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4213,6 +4267,20 @@ function isCallTranscriptOpen(itemId: string) {
|
|||||||
return Boolean(callTranscriptOpen.value[itemId]);
|
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"]) {
|
function channelIcon(channel: "All" | CommItem["channel"]) {
|
||||||
if (channel === "All") return "all";
|
if (channel === "All") return "all";
|
||||||
if (channel === "Telegram") return "telegram";
|
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>
|
<span v-if="entry.item.duration"> · {{ entry.item.duration }}</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="comm-call-wave mb-2" :ref="(el) => setCommCallWaveHost(entry.item.id, el as Element | null)" />
|
<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)">
|
<button class="call-transcript-toggle" @click="toggleCallTranscript(entry.item)">
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
@@ -5712,6 +5800,28 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
background: color-mix(in oklab, var(--color-base-100) 90%, transparent);
|
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 {
|
.call-transcript-toggle:hover {
|
||||||
background: color-mix(in oklab, var(--color-base-100) 72%, var(--color-base-200));
|
background: color-mix(in oklab, var(--color-base-100) 72%, var(--color-base-200));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user