fix(web): improve context menu positioning and forward UX
Some checks failed
CI / test (push) Failing after 18s
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:
@@ -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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user