feat: add user display profiles and fix web context menu UX
Some checks failed
CI / test (push) Failing after 17s
Some checks failed
CI / test (push) Failing after 17s
backend: - add required user name and optional bio fields - extend auth/register and user schemas/services with name/bio - add alembic migration 0006 with safe backfill name=username - compute per-user chat display_title for private chats - keep Saved Messages delete-for-all protections web: - registration now includes name - add profile edit modal (name/username/bio/avatar url) - show private chat names via display_title - fix context menus to open near cursor with viewport clamping - stabilize +/close floating button to remove visual jump
This commit is contained in:
@@ -13,6 +13,7 @@ export function NewChatPanel() {
|
||||
const [title, setTitle] = useState("");
|
||||
const [handle, setHandle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [isPublic, setIsPublic] = useState(false);
|
||||
const [results, setResults] = useState<UserSearchItem[]>([]);
|
||||
const [discoverResults, setDiscoverResults] = useState<DiscoverChat[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -87,8 +88,14 @@ export function NewChatPanel() {
|
||||
setError(null);
|
||||
try {
|
||||
let chat;
|
||||
if (handle.trim()) {
|
||||
chat = await createPublicChat(mode, title.trim(), handle.trim().replace("@", "").toLowerCase(), description.trim() || undefined);
|
||||
if (isPublic) {
|
||||
const normalizedHandle = handle.trim().replace("@", "").toLowerCase();
|
||||
if (!normalizedHandle) {
|
||||
setError("Public chat requires @handle");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
chat = await createPublicChat(mode, title.trim(), normalizedHandle, description.trim() || undefined);
|
||||
} else {
|
||||
chat = await createChat(mode as ChatType, title.trim(), []);
|
||||
}
|
||||
@@ -97,6 +104,7 @@ export function NewChatPanel() {
|
||||
setTitle("");
|
||||
setHandle("");
|
||||
setDescription("");
|
||||
setIsPublic(false);
|
||||
} catch {
|
||||
setError("Failed to create chat");
|
||||
} finally {
|
||||
@@ -124,6 +132,7 @@ export function NewChatPanel() {
|
||||
setQuery("");
|
||||
setResults([]);
|
||||
setDiscoverResults([]);
|
||||
setIsPublic(false);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -148,8 +157,8 @@ export function NewChatPanel() {
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<button className="h-12 w-12 rounded-full bg-sky-500 text-xl font-semibold text-slate-950 shadow-lg hover:bg-sky-400" onClick={() => setMenuOpen((v) => !v)}>
|
||||
{menuOpen ? "×" : "+"}
|
||||
<button className="flex h-12 w-12 items-center justify-center rounded-full bg-sky-500 text-slate-950 shadow-lg hover:bg-sky-400" onClick={() => setMenuOpen((v) => !v)}>
|
||||
<span className="block w-5 text-center text-2xl leading-none">{menuOpen ? "✕" : "+"}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -169,7 +178,8 @@ export function NewChatPanel() {
|
||||
<div className="tg-scrollbar max-h-44 overflow-auto">
|
||||
{results.map((user) => (
|
||||
<button className="mb-1 block w-full rounded-lg bg-slate-800 px-3 py-2 text-left text-sm hover:bg-slate-700" key={user.id} onClick={() => void createPrivate(user.id)}>
|
||||
@{user.username}
|
||||
<p className="truncate font-semibold">{user.name}</p>
|
||||
<p className="truncate text-xs text-slate-400">@{user.username}</p>
|
||||
</button>
|
||||
))}
|
||||
{normalizedQuery && results.length === 0 ? <p className="text-xs text-slate-400">No users</p> : null}
|
||||
@@ -180,6 +190,7 @@ export function NewChatPanel() {
|
||||
{dialog === "discover" ? (
|
||||
<div className="space-y-2">
|
||||
<input className="w-full rounded-xl border border-slate-700/80 bg-slate-800/80 px-3 py-2 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500" placeholder="Search groups/channels by title or @handle" value={query} onChange={(e) => void handleDiscover(e.target.value)} />
|
||||
<p className="text-xs text-slate-400">Search works only for public groups/channels.</p>
|
||||
<div className="tg-scrollbar max-h-52 overflow-auto">
|
||||
{discoverResults.map((chat) => (
|
||||
<div className="mb-1 flex items-center justify-between rounded-lg bg-slate-800 px-3 py-2" key={chat.id}>
|
||||
@@ -204,7 +215,13 @@ export function NewChatPanel() {
|
||||
{dialog === "group" || dialog === "channel" ? (
|
||||
<form className="space-y-2" onSubmit={(e) => void createByType(e, dialog)}>
|
||||
<input className="w-full rounded-xl border border-slate-700/80 bg-slate-800/80 px-3 py-2 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500" placeholder={dialog === "group" ? "Group title" : "Channel title"} value={title} onChange={(e) => setTitle(e.target.value)} />
|
||||
<input className="w-full rounded-xl border border-slate-700/80 bg-slate-800/80 px-3 py-2 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500" placeholder="@handle (optional, enables public join/search)" value={handle} onChange={(e) => setHandle(e.target.value)} />
|
||||
<label className="flex items-center gap-2 rounded-xl border border-slate-700/80 bg-slate-800/60 px-3 py-2 text-sm">
|
||||
<input checked={isPublic} onChange={(e) => setIsPublic(e.target.checked)} type="checkbox" />
|
||||
Public {dialog} (discover + join by others)
|
||||
</label>
|
||||
{isPublic ? (
|
||||
<input className="w-full rounded-xl border border-slate-700/80 bg-slate-800/80 px-3 py-2 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500" placeholder="@handle (required for public)" value={handle} onChange={(e) => setHandle(e.target.value)} />
|
||||
) : null}
|
||||
<input className="w-full rounded-xl border border-slate-700/80 bg-slate-800/80 px-3 py-2 text-sm outline-none placeholder:text-slate-400 focus:border-sky-500" placeholder="Description (optional)" value={description} onChange={(e) => setDescription(e.target.value)} />
|
||||
<button className="w-full rounded-lg bg-sky-500 px-3 py-2 text-sm font-semibold text-slate-950 hover:bg-sky-400 disabled:opacity-60" disabled={loading} type="submit">
|
||||
Create {dialog}
|
||||
|
||||
Reference in New Issue
Block a user