Files
spsays-training/components/chat/chat-profile-header.tsx
T

165 lines
4.6 KiB
TypeScript

"use client";
/**
* The relationship block on a chat profile: how long you've been connected,
* which way the follow runs, mute, and blocking.
*
* One-way follows are stated plainly rather than collapsed into "friends" —
* "they follow you" and "you follow each other" are different facts, and the
* profile is where someone goes to check exactly that.
*
* Centred, to sit under the avatar and name above it — which are centred too.
*/
import { useState } from "react";
import { BellOff, Bell, ShieldBan, ShieldCheck } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { ConfirmModal } from "@/components/ui/confirm-modal";
import { relationships } from "@/lib/social";
import {
chat,
followSummary,
isMuted,
type ConversationDetail,
type MuteDuration,
} from "@/lib/chat";
function friendsSinceLabel(iso?: string | null): string | null {
if (!iso) return null;
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return null;
return `Connected since ${d.toLocaleDateString(undefined, {
month: "long",
year: "numeric",
})}`;
}
export function ChatProfileHeader({
conversationId,
detail,
onChanged,
}: {
conversationId: string;
detail: ConversationDetail;
onChanged: () => void;
}) {
const [busy, setBusy] = useState(false);
// Blocking asks first; unblocking is immediate, since it only restores what
// was there before.
const [confirmBlock, setConfirmBlock] = useState(false);
const rel = detail.relationship;
const peer = detail.peer;
const muted = isMuted(detail);
const setMute = async (duration: MuteDuration) => {
setBusy(true);
try {
await chat.setMute(conversationId, duration);
toast.success(duration === "off" ? "Chat unmuted" : "Chat muted");
onChanged();
} catch {
toast.error("Could not change mute settings");
} finally {
setBusy(false);
}
};
const toggleBlock = async () => {
if (!peer) return;
const blocking = !rel?.blocked;
setBusy(true);
try {
if (blocking) await relationships.block(peer.handle);
else await relationships.unblock(peer.handle);
toast.success(blocking ? `Blocked ${peer.name}` : `Unblocked ${peer.name}`);
onChanged();
} catch {
toast.error("Could not update block settings");
} finally {
setBusy(false);
}
};
return (
<div className="space-y-4 p-4">
{peer && rel && (
<div className="space-y-1 text-center text-sm">
<p className="text-foreground">{followSummary(rel, peer.name)}</p>
{friendsSinceLabel(rel.friends_since) && (
<p className="text-muted-foreground">
{friendsSinceLabel(rel.friends_since)}
</p>
)}
{rel.blocked_by && (
<p className="text-muted-foreground">
{peer.name} has blocked you.
</p>
)}
</div>
)}
<div className="flex flex-wrap justify-center gap-2">
{muted ? (
<Button
variant="outline"
size="sm"
disabled={busy}
onClick={() => setMute("off")}
>
<Bell className="size-4" /> Unmute
</Button>
) : (
<>
<Button
variant="outline"
size="sm"
disabled={busy}
onClick={() => setMute("8h")}
>
<BellOff className="size-4" /> Mute 8h
</Button>
<Button
variant="outline"
size="sm"
disabled={busy}
onClick={() => setMute("always")}
>
Mute always
</Button>
</>
)}
{peer && (
<Button
variant={rel?.blocked ? "outline" : "destructive"}
size="sm"
disabled={busy}
onClick={() =>
rel?.blocked ? void toggleBlock() : setConfirmBlock(true)
}
>
{rel?.blocked ? (
<>
<ShieldCheck className="size-4" /> Unblock
</>
) : (
<>
<ShieldBan className="size-4" /> Block
</>
)}
</Button>
)}
</div>
<ConfirmModal
open={confirmBlock}
onClose={() => setConfirmBlock(false)}
title={`Block ${peer?.name ?? "this person"}?`}
message="They won't be able to message you, and you won't see their messages. You can unblock them here later."
confirmLabel="Block"
onConfirm={toggleBlock}
/>
</div>
);
}