feat: chat
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* The group profile's Settings tab: rename, choose who may post, and leave.
|
||||
*
|
||||
* Editing is admin-only; everyone else sees the same facts read-only, so the
|
||||
* tab still answers "why can't I post here?" for a plain member.
|
||||
*/
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { Camera, Loader2, LogOut } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { UserAvatar } from "@/components/ui/user-avatar";
|
||||
import { compressImage } from "@/lib/compress-image";
|
||||
import {
|
||||
chat,
|
||||
groupAvatarUrl,
|
||||
isGroupAdmin,
|
||||
uploadGroupAvatar,
|
||||
type ConversationDetail,
|
||||
type SendPolicy,
|
||||
} from "@/lib/chat";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function GroupSettingsTab({
|
||||
conversationId,
|
||||
detail,
|
||||
selfHandle,
|
||||
onChanged,
|
||||
}: {
|
||||
conversationId: string;
|
||||
detail: ConversationDetail;
|
||||
selfHandle: string;
|
||||
onChanged: () => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [title, setTitle] = useState(detail.title ?? "");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const iAmAdmin = isGroupAdmin(detail.my_role);
|
||||
const renamed = title.trim() !== (detail.title ?? "").trim();
|
||||
|
||||
/**
|
||||
* Upload first, then point the group at the new key. Two steps because the
|
||||
* bytes go straight to storage and never through the BFF — the same path
|
||||
* message attachments take.
|
||||
*/
|
||||
const changePhoto = async (file: File) => {
|
||||
setUploading(true);
|
||||
try {
|
||||
const compressed = await compressImage(file).catch(() => file);
|
||||
const key = await uploadGroupAvatar(conversationId, compressed);
|
||||
await chat.updateGroup(conversationId, { avatar_key: key });
|
||||
toast.success("Group photo updated");
|
||||
onChanged();
|
||||
} catch {
|
||||
toast.error("Could not update the group photo");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const rename = async () => {
|
||||
if (!renamed || title.trim() === "") return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await chat.updateGroup(conversationId, { title: title.trim() });
|
||||
toast.success("Group renamed");
|
||||
onChanged();
|
||||
} catch {
|
||||
toast.error("Could not rename the group");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const setPolicy = async (policy: SendPolicy) => {
|
||||
if (policy === detail.send_policy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await chat.updateGroup(conversationId, { send_policy: policy });
|
||||
toast.success(
|
||||
policy === "admins"
|
||||
? "Only admins can send messages now"
|
||||
: "Everyone can send messages now",
|
||||
);
|
||||
onChanged();
|
||||
} catch {
|
||||
toast.error("Could not change the setting");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const leave = async () => {
|
||||
if (
|
||||
!window.confirm(
|
||||
detail.my_role === "owner"
|
||||
? "Leave this group? Another admin will become the owner."
|
||||
: "Leave this group?",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
await chat.removeMember(conversationId, selfHandle);
|
||||
toast.success("You left the group");
|
||||
router.push("/chats");
|
||||
} catch {
|
||||
toast.error("Could not leave the group");
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-foreground">Group photo</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<UserAvatar
|
||||
src={groupAvatarUrl(conversationId, detail.avatar_key)}
|
||||
name={detail.title ?? "Group"}
|
||||
className="size-16"
|
||||
textClassName="text-lg"
|
||||
/>
|
||||
{uploading && (
|
||||
<span className="absolute inset-0 flex items-center justify-center rounded-full bg-black/40">
|
||||
<Loader2 className="size-5 animate-spin text-white" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{iAmAdmin ? (
|
||||
<>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) void changePhoto(file);
|
||||
// Reset so re-picking the same file still fires a change.
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={uploading || busy}
|
||||
onClick={() => fileRef.current?.click()}
|
||||
>
|
||||
<Camera className="size-4" />
|
||||
{detail.avatar_key ? "Change photo" : "Add photo"}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Only group admins can change the photo.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-foreground">Group name</h3>
|
||||
{iAmAdmin ? (
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
maxLength={120}
|
||||
className="flex-1 rounded-lg border border-border bg-muted/40 px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-primary/40"
|
||||
/>
|
||||
<Button size="sm" disabled={!renamed || busy} onClick={rename}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">{detail.title}</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-foreground">
|
||||
Who can send messages
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<PolicyOption
|
||||
active={detail.send_policy === "everyone"}
|
||||
disabled={!iAmAdmin || busy}
|
||||
onClick={() => setPolicy("everyone")}
|
||||
title="Everyone"
|
||||
description="Any member can send messages."
|
||||
/>
|
||||
<PolicyOption
|
||||
active={detail.send_policy === "admins"}
|
||||
disabled={!iAmAdmin || busy}
|
||||
onClick={() => setPolicy("admins")}
|
||||
title="Only admins"
|
||||
description="Members can read but not post — an announcement group."
|
||||
/>
|
||||
</div>
|
||||
{!iAmAdmin && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Only group admins can change these settings.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-medium text-foreground">Leave</h3>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onClick={leave}
|
||||
>
|
||||
<LogOut className="size-4" /> Leave group
|
||||
</Button>
|
||||
{detail.my_role === "owner" && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
You own this group. If you leave, the longest-serving admin becomes
|
||||
the owner.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PolicyOption({
|
||||
active,
|
||||
disabled,
|
||||
onClick,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
active: boolean;
|
||||
disabled: boolean;
|
||||
onClick: () => void;
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
aria-pressed={active}
|
||||
className={cn(
|
||||
"flex w-full flex-col items-start gap-0.5 rounded-lg border p-3 text-left transition-colors",
|
||||
active
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:bg-muted/60",
|
||||
disabled && "cursor-default opacity-70 hover:bg-transparent",
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-medium text-foreground">{title}</span>
|
||||
<span className="text-xs text-muted-foreground">{description}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user