import { $Typed } from "@atproto/api";
import type { Link as FacetLink } from "@atproto/api/dist/client/types/app/bsky/richtext/facet";
import type { LivestreamStore } from "@streamplace/core";
import { segmentize, type Facet, type FacetFeature } from "@streamplace/core";
import { EyeOff, Pin, X } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import type {
ChatMessageViewHydrated,
PinnedRecordViewHydrated,
} from "streamplace";
import { useStore } from "zustand";
import { useShallow } from "zustand/react/shallow";
import { useSession } from "../../lib/session";
function rgbColor(
color?: { red: number; green: number; blue: number } | null,
): string | undefined {
if (!color) return undefined;
return `rgb(${color.red}, ${color.green}, ${color.blue})`;
}
export function StreamNotifications({ store }: { store: LivestreamStore }) {
const state = useStore(
store,
useShallow((s) => ({
pinnedComment: s.pinnedComment,
activeTeleport: s.activeTeleport,
})),
);
return (
<>
{state.pinnedComment && (
)}
{state.activeTeleport && (
)}
>
);
}
function PinnedNotification({
store,
comment,
}: {
store: LivestreamStore;
comment: PinnedRecordViewHydrated;
}) {
const { t } = useTranslation("common");
const { state: sessionState, pdsAgent, did } = useSession();
const streamerDid = useStore(store, (s) => s.livestream?.author.did);
const canUnpin = did && streamerDid && did === streamerDid;
const message = comment.message as ChatMessageViewHydrated | undefined;
const record = comment.record;
const authorColor = rgbColor(message?.chatProfile?.color);
const authorName = message
? message.author.displayName || message.author.handle || message.author.did
: "unknown";
const messageRecord = message?.record as
| ChatMessageViewHydrated["record"]
| undefined;
const text = messageRecord?.text || "";
const facets = messageRecord?.facets;
const segments = facets ? segmentize(text, facets as Facet[]) : [];
const [dismissed, setDismissed] = useState(false);
// Handle TTL expiry
const expiresAt = record.expiresAt ? new Date(record.expiresAt) : null;
useEffect(() => {
if (!expiresAt) return;
const remaining = expiresAt.getTime() - Date.now();
if (remaining <= 0) {
setDismissed(true);
return;
}
const timeout = setTimeout(() => setDismissed(true), remaining);
return () => clearTimeout(timeout);
}, [expiresAt]);
// When dismissed, clear from store
useEffect(() => {
if (dismissed) {
store.setState({ pinnedComment: null });
}
}, [dismissed, store]);
const handleDismiss = useCallback(() => {
setDismissed(true);
}, []);
const handleUnpin = useCallback(async () => {
if (!pdsAgent || !streamerDid) return;
try {
const rkey = comment.uri.split("/").pop();
if (!rkey) return;
await pdsAgent.com.atproto.repo.deleteRecord({
repo: streamerDid,
collection: "place.stream.chat.pinnedRecord",
rkey,
});
} catch (e) {
console.error("Failed to unpin message:", e);
}
store.setState({ pinnedComment: null });
}, [pdsAgent, streamerDid, comment.uri, store]);
if (dismissed) return null;
return (
{authorName}
{segments.length > 0
? segments.map((seg, i) => (
))
: text}
{canUnpin && (
)}
);
}
function PinnedRichText({
segment,
}: {
segment: { text: string; features?: unknown[] };
}) {
const ftr = segment.features?.[0] as FacetFeature | undefined;
if (!ftr) {
return {segment.text};
}
if (ftr.$type === "app.bsky.richtext.facet#link") {
const linkFtr = ftr as $Typed;
return (
{segment.text}
);
}
if (ftr.$type === "app.bsky.richtext.facet#mention") {
return {segment.text};
}
return {segment.text};
}
function TeleportNotification({ teleport }: { teleport: any }) {
const { t } = useTranslation("common");
const targetDid = teleport.target;
const startsAt = teleport.startsAt
? new Date(teleport.startsAt).getTime()
: null;
if (!startsAt) return null;
const now = Date.now();
const diff = Math.max(0, Math.ceil((startsAt - now) / 1000));
if (diff <= 0) return null;
const mins = Math.floor(diff / 60);
const secs = diff % 60;
const display =
mins > 0
? `${mins}:${String(secs).padStart(2, "0")}`
: `0:${String(secs).padStart(2, "0")}`;
return (
{t("teleporting-in")}{" "}
{display}
);
}