From 68ba97bb905c6b85f3d583244eb20d7edc06ab93 Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Mar 2026 11:27:16 +0300 Subject: [PATCH] fix(web): unify mic/send button and restore scroll-down - show one action button in composer: mic when empty, send when text exists - add floating scroll-to-bottom button in message list - exclude non-text/media messages from Chat Info links list to avoid duplicates --- web/src/components/ChatInfoPanel.tsx | 27 +++++++++++------ web/src/components/MessageComposer.tsx | 39 ++++++++++++++----------- web/src/components/MessageList.tsx | 40 +++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 26 deletions(-) diff --git a/web/src/components/ChatInfoPanel.tsx b/web/src/components/ChatInfoPanel.tsx index d649e0e..0a54b10 100644 --- a/web/src/components/ChatInfoPanel.tsx +++ b/web/src/components/ChatInfoPanel.tsx @@ -61,9 +61,20 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) { } return members.find((m) => m.user_id === me?.id)?.role; }, [chat?.my_role, members, me?.id]); + const myRoleNormalized = useMemo(() => { + if (!myRole) { + return null; + } + const role = String(myRole).toLowerCase(); + if (role === "owner" || role === "admin" || role === "member") { + return role; + } + return null; + }, [myRole]); const isGroupLike = chat?.type === "group" || chat?.type === "channel"; const showMembersSection = Boolean(chat && isGroupLike && !chat.is_saved); - const canManageMembers = Boolean(isGroupLike && (myRole === "owner" || myRole === "admin")); + const canManageMembers = Boolean(isGroupLike && (myRoleNormalized === "owner" || myRoleNormalized === "admin")); + const canEditTitle = Boolean(isGroupLike && (myRoleNormalized === "owner" || myRoleNormalized === "admin")); const photoAttachments = useMemo(() => attachments.filter((item) => item.file_type.startsWith("image/")).sort((a, b) => b.id - a.id), [attachments]); const videoAttachments = useMemo(() => attachments.filter((item) => item.file_type.startsWith("video/")).sort((a, b) => b.id - a.id), [attachments]); const voiceAttachments = useMemo(() => attachments.filter((item) => item.message_type === "voice").sort((a, b) => b.id - a.id), [attachments]); @@ -259,7 +270,7 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {

{muted ? "Chat notifications are muted." : "Chat notifications are enabled."}

- {isGroupLike ? ( + {isGroupLike && canEditTitle ? ( <>

Title

event.stopPropagation()} > - {myRole === "owner" && memberCtx.member.role === "member" ? ( + {myRoleNormalized === "owner" && memberCtx.member.role === "member" ? ( ) : null} - {myRole === "owner" && memberCtx.member.role === "admin" ? ( + {myRoleNormalized === "owner" && memberCtx.member.role === "admin" ? ( ) : null} - {myRole === "owner" && memberCtx.member.role !== "owner" ? ( + {myRoleNormalized === "owner" && memberCtx.member.role !== "owner" ? ( ) : null} - {(myRole === "owner" || (myRole === "admin" && memberCtx.member.role === "member")) ? ( + {(myRoleNormalized === "owner" || (myRoleNormalized === "admin" && memberCtx.member.role === "member")) ? ( - - + {hasTextToSend ? ( + + ) : ( + + )} {selectedFile ? ( diff --git a/web/src/components/MessageList.tsx b/web/src/components/MessageList.tsx index bbd5b8b..47e0b21 100644 --- a/web/src/components/MessageList.tsx +++ b/web/src/components/MessageList.tsx @@ -67,6 +67,8 @@ export function MessageList() { const [undoTick, setUndoTick] = useState(0); const [reactionsByMessage, setReactionsByMessage] = useState>({}); const [mediaViewer, setMediaViewer] = useState(null); + const [showScrollToBottom, setShowScrollToBottom] = useState(false); + const scrollContainerRef = useRef(null); const messages = useMemo(() => { if (!activeChatId) { @@ -143,11 +145,28 @@ export function MessageList() { return () => window.clearTimeout(timer); }, [activeChatId, focusedMessageId, messages.length, setFocusedMessage]); + useEffect(() => { + const container = scrollContainerRef.current; + if (!container) { + return; + } + const distance = container.scrollHeight - container.scrollTop - container.clientHeight; + setShowScrollToBottom(distance > 180); + }, [messages.length, activeChatId]); + if (!activeChatId) { return
Select a chat
; } const chatId = activeChatId; + function scrollToBottom() { + const container = scrollContainerRef.current; + if (!container) { + return; + } + container.scrollTo({ top: container.scrollHeight, behavior: "smooth" }); + } + async function ensureReactionsLoaded(messageId: number) { if (reactionsByMessage[messageId]) { return; @@ -325,7 +344,15 @@ export function MessageList() { ) : null} -
+
{ + const target = event.currentTarget; + const distance = target.scrollHeight - target.scrollTop - target.clientHeight; + setShowScrollToBottom(distance > 180); + }} + > {hasMore ? (
+
+ ) : null} {ctx ? createPortal(