feat: add message broadcaster
This commit is contained in:
@@ -7,6 +7,7 @@ import { settings } from "@/lib/db/schema";
|
||||
import { adminCheck } from "@/lib/auth";
|
||||
import { sse } from "@/lib/db/sse-endpoints";
|
||||
import { syncAmber as syncAmberRaw } from "@/util/sync";
|
||||
import type { ToastType } from "@/components/fake-toast";
|
||||
|
||||
export async function getSettongs() {
|
||||
if (!(await adminCheck())) throw "Unauthorized";
|
||||
@@ -56,3 +57,19 @@ export async function toggleDonatePaymentMethod(
|
||||
|
||||
await actionLog("Changed a settings", { [method]: state });
|
||||
}
|
||||
|
||||
export async function broadcastMessage(
|
||||
prefix: string,
|
||||
value: string,
|
||||
type: ToastType,
|
||||
) {
|
||||
"use server";
|
||||
|
||||
sse.active.pub("announcement", {
|
||||
severity: type,
|
||||
message: value,
|
||||
prefix,
|
||||
});
|
||||
|
||||
await actionLog("Broadcasted message", { prefix, value, type });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { Send } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { FakeToast, type ToastType } from "@/components/fake-toast";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { broadcastMessage } from "./api";
|
||||
import { ActionButton } from "@/components/action-button";
|
||||
|
||||
const toastTypes: ToastType[] = ["warning", "error", "success", "info"];
|
||||
|
||||
export function Broadcaster() {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [prefix, setPrefix] = useState("/");
|
||||
const [prefixFocused, setPrefixFocused] = useState(false);
|
||||
const [value, setValue] = useState("");
|
||||
const [type, setType] = useState<ToastType>("warning");
|
||||
|
||||
function cycleType() {
|
||||
setType((currentType) => {
|
||||
const currentIndex = toastTypes.indexOf(currentType);
|
||||
return toastTypes[(currentIndex + 1) % toastTypes.length];
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-fit rounded-lg border border-zinc-800 bg-[#050505] bg-[radial-gradient(#3f3f46_1px,transparent_1px)] bg-size-[16px_16px] p-0 transition-[padding] duration-300",
|
||||
expanded && "p-12",
|
||||
)}
|
||||
>
|
||||
<FakeToast type={type} onIconClick={cycleType}>
|
||||
<span
|
||||
onClick={() => setExpanded(true)}
|
||||
onBlur={(ev) => setValue(ev.target.textContent)}
|
||||
contentEditable
|
||||
data-placeholder="Broadcast message..."
|
||||
className="w-full flex-1"
|
||||
></span>
|
||||
</FakeToast>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute right-1 bottom-1 flex w-0 gap-1 opacity-0 transition-opacity duration-300",
|
||||
expanded && "w-fit opacity-100 delay-300",
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
value={prefix}
|
||||
onChange={(event) => setPrefix(event.target.value)}
|
||||
onFocus={() => setPrefixFocused(true)}
|
||||
onBlur={() => setPrefixFocused(false)}
|
||||
className="backdrop-blur-xs transition-[width] focus:w-48"
|
||||
style={{
|
||||
width: prefixFocused
|
||||
? undefined
|
||||
: `calc(24px + ${Math.max(prefix.length, 1)}ch)`,
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="backdrop-blur-xs"
|
||||
action={async () => {
|
||||
await broadcastMessage(prefix, value, type);
|
||||
setExpanded(false);
|
||||
}}
|
||||
>
|
||||
<Send />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
import { Bomb, FolderSync } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { ActionButton } from "@/components/action-button";
|
||||
import { CdnChooser } from "@/components/chooser";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { shared } from "@/lib/comms";
|
||||
import {
|
||||
@@ -11,9 +13,8 @@ import {
|
||||
toggleDonatePaymentMethod,
|
||||
toggleEnka,
|
||||
} from "./api";
|
||||
import { Broadcaster } from "./broadcaster";
|
||||
import { Section } from "./page";
|
||||
import { ActionButton } from "@/components/action-button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export function SettingsServicesSection({
|
||||
enka: enkaInitial,
|
||||
@@ -108,6 +109,7 @@ export function SettingsServicesSection({
|
||||
{syncResult && (
|
||||
<pre className="h-full rounded border bg-muted p-2">{syncResult}</pre>
|
||||
)}
|
||||
<Broadcaster />
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
|
||||
@@ -79,6 +79,11 @@ export function VersionCheck({ headless = false }: { headless?: boolean }) {
|
||||
window.location.hash = "#update";
|
||||
queueMicrotask(() => window.location.reload());
|
||||
},
|
||||
announcement({ message, prefix, severity }) {
|
||||
if (headless) return;
|
||||
if (prefix && !window.location.pathname.startsWith(prefix)) return;
|
||||
return toast[severity](message, { duration: Infinity });
|
||||
},
|
||||
},
|
||||
{
|
||||
endpoint: "/api/active",
|
||||
|
||||
@@ -236,3 +236,12 @@ img {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
[contenteditable="true"]:empty::before {
|
||||
content: attr(data-placeholder);
|
||||
color: #a0a0a0;
|
||||
pointer-events: none;
|
||||
}
|
||||
[contenteditable="true"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user