Add Jitsi participant copy actions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -533,6 +533,7 @@ Shown above the meeting list when data is loaded. Stats reflect the **filtered +
|
|||||||
- **Total Duration** — sum of all session durations (HH:MM:SS)
|
- **Total Duration** — sum of all session durations (HH:MM:SS)
|
||||||
|
|
||||||
In grouped view, each room header also shows its own subtotals (meeting count, unique participants by Novi UUID when available).
|
In grouped view, each room header also shows its own subtotals (meeting count, unique participants by Novi UUID when available).
|
||||||
|
Each meeting instance now includes a **Copy names** button so staff can grab the full participant list for pasting into follow-up reports.
|
||||||
|
|
||||||
### Caching / Load Behavior
|
### Caching / Load Behavior
|
||||||
|
|
||||||
|
|||||||
@@ -358,6 +358,41 @@ function is_room_open(room_name: string): boolean {
|
|||||||
|
|
||||||
// --- URL Builder ---
|
// --- URL Builder ---
|
||||||
let show_url_builder = $state(false);
|
let show_url_builder = $state(false);
|
||||||
|
let copied_participants_meeting_id = $state<string | null>(null);
|
||||||
|
let copied_participants_timeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
function build_participant_copy_text(participants: MeetingParticipant[]): string {
|
||||||
|
return participants
|
||||||
|
.map((participant) =>
|
||||||
|
participant.role === 'moderator'
|
||||||
|
? `Mod: ${participant.displayName}`
|
||||||
|
: participant.displayName
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copy_participants(
|
||||||
|
meeting_id: string,
|
||||||
|
participants: MeetingParticipant[]
|
||||||
|
) {
|
||||||
|
const text = build_participant_copy_text(participants);
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
copied_participants_meeting_id = meeting_id;
|
||||||
|
if (copied_participants_timeout) {
|
||||||
|
clearTimeout(copied_participants_timeout);
|
||||||
|
}
|
||||||
|
copied_participants_timeout = setTimeout(() => {
|
||||||
|
if (copied_participants_meeting_id === meeting_id) {
|
||||||
|
copied_participants_meeting_id = null;
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to copy participants to clipboard.', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Export ---
|
// --- Export ---
|
||||||
function download_file(content: string, filename: string, mime: string) {
|
function download_file(content: string, filename: string, mime: string) {
|
||||||
@@ -776,6 +811,7 @@ function export_json() {
|
|||||||
{@const mods = m.real_participants.filter((p) => p.role === 'moderator')}
|
{@const mods = m.real_participants.filter((p) => p.role === 'moderator')}
|
||||||
{@const others = m.real_participants.filter((p) => p.role !== 'moderator')}
|
{@const others = m.real_participants.filter((p) => p.role !== 'moderator')}
|
||||||
{@const all_names = m.real_participants.map((p) => `${p.displayName} (${p.role})`).join('\n')}
|
{@const all_names = m.real_participants.map((p) => `${p.displayName} (${p.role})`).join('\n')}
|
||||||
|
{@const participant_copy_text = build_participant_copy_text(m.real_participants)}
|
||||||
<tr
|
<tr
|
||||||
class="border-surface-200-800 hover:bg-surface-100-900 border-b transition-colors duration-200">
|
class="border-surface-200-800 hover:bg-surface-100-900 border-b transition-colors duration-200">
|
||||||
<td
|
<td
|
||||||
@@ -813,18 +849,34 @@ function export_json() {
|
|||||||
{#if m.real_participant_count === 0}
|
{#if m.real_participant_count === 0}
|
||||||
<span class="opacity-40">—</span>
|
<span class="opacity-40">—</span>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-0.5 text-xs">
|
<div class="space-y-1 text-xs">
|
||||||
{#if mods.length > 0}
|
{#if mods.length > 0}
|
||||||
<div>
|
<div class="whitespace-normal break-words">
|
||||||
<span class="mr-1 opacity-40">Mod:</span>
|
<span class="mr-1 opacity-40">Mod:</span>
|
||||||
<span class="font-semibold">{mods.map((p) => p.displayName).join(', ')}</span>
|
<span class="font-semibold">{mods.map((p) => p.displayName).join(', ')}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if others.length > 0}
|
{#if others.length > 0}
|
||||||
<div class="opacity-80">
|
<div class="whitespace-normal break-words opacity-80">
|
||||||
{others.slice(0, 5).map((p) => p.displayName).join(', ')}{others.length > 5 ? ` +${others.length - 5} more` : ''}
|
{others.map((p) => p.displayName).join(', ')}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() =>
|
||||||
|
copy_participants(
|
||||||
|
m.meeting_id,
|
||||||
|
m.real_participants
|
||||||
|
)}
|
||||||
|
class="inline-flex items-center gap-1 rounded border border-surface-200-800 bg-surface-100-900 px-2 py-1 text-xs font-medium transition-colors hover:bg-surface-200-800"
|
||||||
|
title="Copy participants to clipboard">
|
||||||
|
<span
|
||||||
|
class="fas fa-copy"
|
||||||
|
aria-hidden="true"></span>
|
||||||
|
{copied_participants_meeting_id === m.meeting_id
|
||||||
|
? 'Copied'
|
||||||
|
: 'Copy names'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
@@ -993,9 +1045,27 @@ function export_json() {
|
|||||||
|
|
||||||
<!-- Final Participants (exclusion-applied) -->
|
<!-- Final Participants (exclusion-applied) -->
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div class="mb-2 flex items-center justify-between gap-2">
|
||||||
class="mb-2 text-xs tracking-wide uppercase opacity-40">
|
<div
|
||||||
Final Participants ({meeting.real_participant_count})
|
class="text-xs tracking-wide uppercase opacity-40">
|
||||||
|
Final Participants ({meeting.real_participant_count})
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() =>
|
||||||
|
copy_participants(
|
||||||
|
meeting.meeting_id,
|
||||||
|
meeting.real_participants
|
||||||
|
)}
|
||||||
|
class="inline-flex items-center gap-1 rounded border border-surface-200-800 bg-surface-100-900 px-2 py-1 text-xs font-medium transition-colors hover:bg-surface-200-800"
|
||||||
|
title="Copy participants to clipboard">
|
||||||
|
<span
|
||||||
|
class="fas fa-copy"
|
||||||
|
aria-hidden="true"></span>
|
||||||
|
{copied_participants_meeting_id === meeting.meeting_id
|
||||||
|
? 'Copied'
|
||||||
|
: 'Copy names'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if meeting.real_participants && meeting.real_participants.length > 0}
|
{#if meeting.real_participants && meeting.real_participants.length > 0}
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
|
|||||||
Reference in New Issue
Block a user