fix(web): improve context menu positioning and forward UX
Some checks failed
CI / test (push) Failing after 18s

- render chat/message context menus via portal to document.body

- clamp menu coordinates to viewport while keeping near-cursor placement

- remove visible chat id fallbacks from chat/discover UI

- hide 'delete for everyone' checkbox for channels; show channel-specific hint

- replace forward-by-chat-id prompt with searchable chat picker modal
This commit is contained in:
2026-03-08 01:03:04 +03:00
parent 456595a576
commit 997598188d
3 changed files with 139 additions and 49 deletions

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { deleteChat } from "../api/chats";
import { updateMyProfile } from "../api/users";
import { useAuthStore } from "../store/authStore";
@@ -26,7 +27,7 @@ export function ChatList() {
const [profileError, setProfileError] = useState<string | null>(null);
const [profileSaving, setProfileSaving] = useState(false);
const deleteModalChat = chats.find((chat) => chat.id === deleteModalChatId) ?? null;
const canDeleteForEveryone = Boolean(deleteModalChat && !deleteModalChat.is_saved && deleteModalChat.type !== "private");
const canDeleteForEveryone = Boolean(deleteModalChat && !deleteModalChat.is_saved && deleteModalChat.type === "group");
useEffect(() => {
const timer = setTimeout(() => {
@@ -115,7 +116,7 @@ export function ChatList() {
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<p className="truncate text-sm font-semibold">{chat.display_title || chat.title || `${chat.type} #${chat.id}`}</p>
<p className="truncate text-sm font-semibold">{chatLabel(chat)}</p>
<span className="shrink-0 text-[11px] text-slate-400">
{messagesByChat[chat.id]?.length ? "now" : ""}
</span>
@@ -128,26 +129,32 @@ export function ChatList() {
</div>
<NewChatPanel />
{ctxChatId && ctxPos ? (
<div className="fixed z-50 w-44 rounded-lg border border-slate-700/80 bg-slate-900/95 p-1 shadow-2xl" style={{ left: ctxPos.x, top: ctxPos.y }} onClick={(e) => e.stopPropagation()}>
<button
className="block w-full rounded px-2 py-1.5 text-left text-sm text-red-300 hover:bg-slate-800"
onClick={() => {
setDeleteModalChatId(ctxChatId);
setCtxChatId(null);
setCtxPos(null);
setDeleteForAll(false);
}}
>
Delete chat
</button>
</div>
) : null}
{ctxChatId && ctxPos
? createPortal(
<div className="fixed z-[100] w-44 rounded-lg border border-slate-700/80 bg-slate-900/95 p-1 shadow-2xl" style={{ left: ctxPos.x, top: ctxPos.y }} onClick={(e) => e.stopPropagation()}>
<button
className="block w-full rounded px-2 py-1.5 text-left text-sm text-red-300 hover:bg-slate-800"
onClick={() => {
setDeleteModalChatId(ctxChatId);
setCtxChatId(null);
setCtxPos(null);
setDeleteForAll(false);
}}
>
Delete chat
</button>
</div>,
document.body
)
: null}
{deleteModalChatId ? (
<div className="absolute inset-0 z-50 flex items-end justify-center bg-slate-950/55 p-3">
<div className="w-full rounded-xl border border-slate-700/80 bg-slate-900 p-3">
<p className="mb-2 text-sm font-semibold">Delete chat #{deleteModalChatId}</p>
<p className="mb-2 text-sm font-semibold">Delete chat: {deleteModalChat ? chatLabel(deleteModalChat) : "selected chat"}</p>
{deleteModalChat?.type === "channel" ? (
<p className="mb-3 text-xs text-slate-400">Channels are removed for all subscribers.</p>
) : null}
{canDeleteForEveryone ? (
<label className="mb-3 flex items-center gap-2 text-sm">
<input checked={deleteForAll} onChange={(e) => setDeleteForAll(e.target.checked)} type="checkbox" />
@@ -232,3 +239,12 @@ export function ChatList() {
const safeY = Math.min(Math.max(pad, wantedY), window.innerHeight - menuHeight - pad);
return { x: safeX, y: safeY };
}
function chatLabel(chat: { display_title?: string | null; title: string | null; type: "private" | "group" | "channel"; is_saved?: boolean }): string {
if (chat.display_title?.trim()) return chat.display_title;
if (chat.title?.trim()) return chat.title;
if (chat.is_saved) return "Saved Messages";
if (chat.type === "private") return "Direct chat";
if (chat.type === "group") return "Group";
return "Channel";
}