Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
49 lines
4.2 KiB
TypeScript
49 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import type { FeedItem } from "../../lib/fixtures";
|
|
import { PLATFORMS, type Platform } from "../../lib/platforms";
|
|
import type { ConnectionSummary } from "../../lib/api-types";
|
|
import { useApiResource } from "../../lib/use-api";
|
|
import { Sheet } from "./sheet";
|
|
import { nativeAmplifyActions } from "../../lib/amplify";
|
|
|
|
const identity = (value: ConnectionSummary[]): ConnectionSummary[] => value;
|
|
const empty: ConnectionSummary[] = [];
|
|
|
|
export function AmplifySheet({ post, platform, open, onClose }: { post: FeedItem | null; platform: Platform | null; open: boolean; onClose: () => void }): React.ReactElement {
|
|
const router = useRouter();
|
|
const connections = useApiResource<ConnectionSummary[], ConnectionSummary[]>("/connections", identity, empty);
|
|
const [picked, setPicked] = useState<string[]>([]);
|
|
const [notice, setNotice] = useState("");
|
|
const native = platform === null ? [] : nativeAmplifyActions(platform);
|
|
function compose(ids: string[]): void {
|
|
if (post === null) return;
|
|
const params = new URLSearchParams({ text: `${post.text}\n\nVia ${post.handle}`, targets: ids.join(",") });
|
|
onClose();
|
|
router.push(`/compose?${params.toString()}`);
|
|
}
|
|
async function share(): Promise<void> {
|
|
if (post === null) return;
|
|
const url = window.location.href;
|
|
if (navigator.share) await navigator.share({ title: `Post by ${post.handle}`, text: post.text, url }).catch(() => undefined);
|
|
else if (navigator.clipboard) { await navigator.clipboard.writeText(`${post.text}\n${url}`); setNotice("Post text copied"); }
|
|
}
|
|
return <Sheet open={open} onClose={onClose} title={post === null ? "Amplify" : `Amplify ${post.handle}`} footer={<div className="flex gap-2"><button type="button" className="min-h-11 min-w-11 rounded-md border border-border px-3" onClick={() => void share()}>Share</button><button type="button" className="min-h-11 flex-1 rounded-md bg-accent px-3 text-sm font-semibold text-white disabled:opacity-40" disabled={picked.length === 0} onClick={() => compose(picked)}>Crosspost to {picked.length}</button></div>}>
|
|
{platform !== null && <><h3 className="mt-2 text-xs font-semibold uppercase text-text-faint">Native on {platform.displayName}</h3>
|
|
{native.length === 0 ? <p className="mt-3 text-xs text-text-faint">This platform exposes no native amplify action.</p> : native.map((action) => <button key={action.kind} type="button" className="mt-2 flex min-h-12 w-full items-center rounded-md border border-border px-4 text-left text-sm" onClick={() => action.kind === "quote" ? compose([]) : setNotice(`${action.label} is available in the platform, but its connector action is not wired yet.`)}>{action.label}{action.kind === "quote" && <span className="ml-auto text-xs text-text-faint">Opens compose</span>}</button>)}
|
|
</>}
|
|
{notice && <p className="mt-2 text-xs text-warn" role="status">{notice}</p>}
|
|
<h3 className="mt-6 text-xs font-semibold uppercase text-text-faint">Crosspost to your other accounts</h3>
|
|
{connections.source !== "live" && <p className="mt-2 text-xs text-text-faint">Connect to the API to choose publishing accounts.</p>}
|
|
{connections.data.filter((connection) => connection.provider !== platform?.id).map((connection) => {
|
|
const target = PLATFORMS[connection.provider];
|
|
const reason = !target?.capabilities.publishText ? "Text publishing unavailable" : target.capabilities.requiresMediaForPublish && !post?.hasMedia ? `${target.displayName} needs media` : connection.health !== "healthy" ? "Reconnect account first" : "";
|
|
return <label key={connection.id} className={`mt-2 flex min-h-14 items-center gap-3 rounded-md border border-border px-3 ${reason ? "opacity-60" : ""}`}>
|
|
<input type="checkbox" className="h-5 w-5 accent-accent" disabled={reason !== ""} checked={picked.includes(connection.id)} onChange={(event) => setPicked((current) => event.target.checked ? [...current, connection.id] : current.filter((id) => id !== connection.id))} />
|
|
<span className="min-w-0 flex-1"><span className="block text-sm">{target?.displayName ?? connection.provider}</span><span className="block text-xs text-text-faint">{reason || "Creates a new attributed post in compose"}</span></span>
|
|
</label>;
|
|
})}
|
|
</Sheet>;
|
|
}
|