import { Badge, Button, Field, InlineNotice, Panel, Select, TextInput } from "@ekklesia/ui";
import { type ReactElement, useState } from "react";
import type { SignageApi } from "../api.js";
import { useAction, useResource, useTick } from "../hooks.js";
import type { ChannelGroupView, ViewerHealthView } from "../types.js";
import { ConfirmButton, FailureNote, formatSince, OnlineBadge, renderLoadable } from "./shared.js";
const AUDIENCE_KEY = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
/**
* The key content providers use to pick an audience's version of a slide, derived from the name an
* operator typed. It is an implementation detail: staff name an audience ("Online"), and only an
* operator who knows a provider expects a particular key needs the Advanced override.
*/
export function audienceKeyFor(name: string, taken: readonly string[]): string {
const base =
name
.normalize("NFKD")
.toLowerCase()
.replace(/[^a-z0-9]+/gu, "-")
.replace(/^-+|-+$/gu, "")
.slice(0, 48)
.replace(/-+$/u, "") || "audience";
const start = /^[a-z]/u.test(base) ? base : `audience-${base}`;
const reserved = new Set([...taken, "default"]);
let candidate = start;
for (let suffix = 2; reserved.has(candidate); suffix += 1) {
candidate = `${start}-${suffix}`;
}
return candidate;
}
function DisplayRow({
canManage,
groups,
nowMs,
onChannelChange,
onNameChange,
onRemove,
viewer,
}: {
readonly canManage: boolean;
readonly groups: readonly ChannelGroupView[];
readonly nowMs: number;
readonly onChannelChange: (channelId: string) => void;
readonly onNameChange: (name: string) => void;
readonly onRemove: () => void;
readonly viewer: ViewerHealthView;
}): ReactElement {
return (
{canManage ? (
event.target.value !== viewer.name && onNameChange(event.target.value)
}
/>
) : (
{viewer.name}
)}
{viewer.deviceId === null ? (
// Opened by someone signed in — a test tab on a laptop, not a paired screen.
Signed-in browser
) : undefined}
{canManage ? (
{(control) => (
)}
) : undefined}
{`Seen ${formatSince(viewer.lastSeenAtMs, nowMs)}`}
{viewer.offlineReady ? `${viewer.cachedAssetCount} cached` : "Not offline ready"}
{viewer.lastError === null ? undefined : (
{viewer.lastError.code}
)}
{canManage && !viewer.online ? (
// Only an offline entry: a screen that is still connected would simply reappear.
) : undefined}
)
}
description="Connect a screen in three short steps."
title="Pair a display"
>
1
On the screen that will show signage, open the display setup address. Copy it from
here; opening it on this computer only previews the display.
2Enter its six-digit code under Display devices in the Dashboard sidebar.
3Name the display and choose its group below.
{displaySetupUrl === undefined ? (
This host did not provide a display setup address.
) : undefined}
{copyStatus === "copied"
? "Display setup address copied."
: copyStatus === "failed"
? "Could not copy the address. Use Preview here and copy it from the address bar."
: ""}
{viewers.state.status === "ready" && viewers.state.value.viewers.length === 0 ? (
No displays paired yetA newly approved screen appears below in its assigned channel.
) : undefined}
{(control) => (
setGroupName(event.target.value)}
placeholder="Nursery hallway"
value={groupName}
/>
)}
) : undefined
}
description="Displays in one group play together, on the same slide at the same moment. Use another group for screens that should run their own loop."
title="Channel groups and displays"
>
{renderLoadable(
groups.state,
(value) => {
const knownChannelIds = new Set(
value.groups.flatMap((group) => group.channels.map((channel) => channel.id)),
);
const unassigned = readyViewers.filter(
(viewer) => viewer.channelId === null || !knownChannelIds.has(viewer.channelId),
);
return (
Delete {group.name}? Its playback history will be removed. Displays
assigned here will move to the default channel of another group.
) : undefined}
Audiences
Who is watching these displays
Every display in this group shows the same slide at the same moment. Add
an audience only when some screens should get a different version of it,
such as wording for people watching online. Images look the same for every
audience.