feat(web): refresh audio card UI and enforce outside-click menu close
All checks were successful
CI / test (push) Successful in 24s
All checks were successful
CI / test (push) Successful in 24s
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { archiveChat, clearChat, createPrivateChat, deleteChat, getChats, getSavedMessagesChat, joinChat, leaveChat, pinChat, unarchiveChat, unpinChat } from "../api/chats";
|
||||
import { globalSearch } from "../api/search";
|
||||
@@ -46,6 +46,9 @@ export function ChatList() {
|
||||
const [profileAllowPrivateMessages, setProfileAllowPrivateMessages] = useState(true);
|
||||
const [profileError, setProfileError] = useState<string | null>(null);
|
||||
const [profileSaving, setProfileSaving] = useState(false);
|
||||
const sidebarRef = useRef<HTMLElement | null>(null);
|
||||
const burgerMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const burgerButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const deleteModalChat = chats.find((chat) => chat.id === deleteModalChatId) ?? null;
|
||||
const canDeleteForEveryone = Boolean(
|
||||
deleteModalChat &&
|
||||
@@ -168,6 +171,25 @@ export function ChatList() {
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const onPointerDown = (event: MouseEvent) => {
|
||||
const target = event.target as Node | null;
|
||||
if (ctxChatId) {
|
||||
setCtxChatId(null);
|
||||
setCtxPos(null);
|
||||
}
|
||||
if (menuOpen) {
|
||||
const inMenu = burgerMenuRef.current?.contains(target ?? null) ?? false;
|
||||
const inButton = burgerButtonRef.current?.contains(target ?? null) ?? false;
|
||||
if (!inMenu && !inButton) {
|
||||
setMenuOpen(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", onPointerDown);
|
||||
return () => document.removeEventListener("mousedown", onPointerDown);
|
||||
}, [ctxChatId, menuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
applyAppearancePreferences(getAppPreferences());
|
||||
}, []);
|
||||
@@ -271,10 +293,11 @@ export function ChatList() {
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="relative flex h-full w-full flex-col bg-slate-900/60" onClick={() => { setCtxChatId(null); setCtxPos(null); setMenuOpen(false); }}>
|
||||
<aside ref={sidebarRef} className="relative flex h-full w-full flex-col bg-slate-900/60" onClick={() => { setCtxChatId(null); setCtxPos(null); setMenuOpen(false); }}>
|
||||
<div className="border-b border-slate-700/50 px-3 py-3">
|
||||
<div className="relative mb-2 flex items-center gap-2">
|
||||
<button
|
||||
ref={burgerButtonRef}
|
||||
className="rounded-lg bg-slate-700/70 px-2 py-2 text-xs"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -285,6 +308,7 @@ export function ChatList() {
|
||||
</button>
|
||||
{menuOpen ? (
|
||||
<div
|
||||
ref={burgerMenuRef}
|
||||
className="absolute left-0 top-11 z-40 w-56 rounded-xl border border-slate-700/80 bg-slate-900/95 p-1 shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
@@ -597,68 +621,70 @@ export function ChatList() {
|
||||
|
||||
{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);
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const chat = chats.find((c) => c.id === ctxChatId);
|
||||
if (!chat) return "Delete chat";
|
||||
if (chat.is_saved) return "Clear chat";
|
||||
if (chat.type === "channel" && chat.my_role === "member") return "Leave channel";
|
||||
return "Delete chat";
|
||||
})()}
|
||||
</button>
|
||||
<button
|
||||
className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-slate-800"
|
||||
onClick={async () => {
|
||||
const target = chats.find((c) => c.id === ctxChatId) ?? archivedChats.find((c) => c.id === ctxChatId);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
if (target.archived) {
|
||||
await unarchiveChat(target.id);
|
||||
} else {
|
||||
await archiveChat(target.id);
|
||||
}
|
||||
await loadChats();
|
||||
const nextArchived = await getChats(undefined, true);
|
||||
setArchivedChats(nextArchived);
|
||||
setCtxChatId(null);
|
||||
setCtxPos(null);
|
||||
}}
|
||||
>
|
||||
{(chats.find((c) => c.id === ctxChatId) ?? archivedChats.find((c) => c.id === ctxChatId))?.archived ? "Unarchive" : "Archive"}
|
||||
</button>
|
||||
<button
|
||||
className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-slate-800"
|
||||
onClick={async () => {
|
||||
const target = chats.find((c) => c.id === ctxChatId) ?? archivedChats.find((c) => c.id === ctxChatId);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
if (target.pinned) {
|
||||
await unpinChat(target.id);
|
||||
} else {
|
||||
await pinChat(target.id);
|
||||
}
|
||||
await loadChats();
|
||||
if (tab === "archived") {
|
||||
<div className="fixed inset-0 z-[99]" onClick={() => { setCtxChatId(null); setCtxPos(null); }}>
|
||||
<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);
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const chat = chats.find((c) => c.id === ctxChatId);
|
||||
if (!chat) return "Delete chat";
|
||||
if (chat.is_saved) return "Clear chat";
|
||||
if (chat.type === "channel" && chat.my_role === "member") return "Leave channel";
|
||||
return "Delete chat";
|
||||
})()}
|
||||
</button>
|
||||
<button
|
||||
className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-slate-800"
|
||||
onClick={async () => {
|
||||
const target = chats.find((c) => c.id === ctxChatId) ?? archivedChats.find((c) => c.id === ctxChatId);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
if (target.archived) {
|
||||
await unarchiveChat(target.id);
|
||||
} else {
|
||||
await archiveChat(target.id);
|
||||
}
|
||||
await loadChats();
|
||||
const nextArchived = await getChats(undefined, true);
|
||||
setArchivedChats(nextArchived);
|
||||
}
|
||||
setCtxChatId(null);
|
||||
setCtxPos(null);
|
||||
}}
|
||||
>
|
||||
{(chats.find((c) => c.id === ctxChatId) ?? archivedChats.find((c) => c.id === ctxChatId))?.pinned ? "Unpin chat" : "Pin chat"}
|
||||
</button>
|
||||
setCtxChatId(null);
|
||||
setCtxPos(null);
|
||||
}}
|
||||
>
|
||||
{(chats.find((c) => c.id === ctxChatId) ?? archivedChats.find((c) => c.id === ctxChatId))?.archived ? "Unarchive" : "Archive"}
|
||||
</button>
|
||||
<button
|
||||
className="block w-full rounded px-2 py-1.5 text-left text-sm hover:bg-slate-800"
|
||||
onClick={async () => {
|
||||
const target = chats.find((c) => c.id === ctxChatId) ?? archivedChats.find((c) => c.id === ctxChatId);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
if (target.pinned) {
|
||||
await unpinChat(target.id);
|
||||
} else {
|
||||
await pinChat(target.id);
|
||||
}
|
||||
await loadChats();
|
||||
if (tab === "archived") {
|
||||
const nextArchived = await getChats(undefined, true);
|
||||
setArchivedChats(nextArchived);
|
||||
}
|
||||
setCtxChatId(null);
|
||||
setCtxPos(null);
|
||||
}}
|
||||
>
|
||||
{(chats.find((c) => c.id === ctxChatId) ?? archivedChats.find((c) => c.id === ctxChatId))?.pinned ? "Unpin chat" : "Pin chat"}
|
||||
</button>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user