From f2f498425bbca86f83a0bef3ce6134a1dd46d578 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Fri, 18 Sep 2026 21:22:02 -0400 Subject: [PATCH] fix(threads): drop the fallback root quote on every thread reply; add Threads + Widgets to the mobile room menu (#165, #211) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inside the thread panel each reply rendered a 'Thread ↩ ' quote because the spec's fallback reply relation (is_falling_back + m.in_reply_to root) was treated like a real reply — noise on every row when the root is already pinned at the top. Genuine reply-to-a-reply quotes are kept. The Threads list and Widgets panel had desktop-only header buttons and no way to open them on a phone; both are now in the mobile ⋮ menu next to Members / Media Gallery. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/features/room/RoomViewHeader.tsx | 36 +++++++++++++++++++ .../features/room/thread/ThreadTimeline.tsx | 20 ++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index d5c6a5ac5..08f4e95d7 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -107,6 +107,10 @@ const RoomMenu = forwardRef( const [reportRoomOpen, setReportRoomOpen] = useState(false); const [bookmarksOpen, setBookmarksOpen] = useAtom(bookmarksPanelAtom); const [mobileMembers, setMobileMembers] = useAtom(mobileMembersPanelAtom); + // [Gitea #165 / #211] Threads list and Widgets had desktop-only header + // buttons and no mobile entry point at all. + const [threadsList, setThreadsList] = useAtom(threadsListAtom); + const [widgets, setWidgets] = useAtom(widgetsPanelAtom); const handleMarkAsRead = () => { markAsRead(mx, room.roomId, hideActivity); @@ -226,6 +230,38 @@ const RoomMenu = forwardRef( )} + {screenSize === ScreenSize.Mobile && ( + { + setThreadsList(!threadsList); + requestClose(); + }} + size="300" + after={} + radii="300" + aria-pressed={threadsList} + > + + Threads + + + )} + {screenSize === ScreenSize.Mobile && ( + { + setWidgets(!widgets); + requestClose(); + }} + size="300" + after={} + radii="300" + aria-pressed={widgets} + > + + Widgets + + + )} {!isServerNotice && ( 0; - const { replyEventId, threadRootId } = mEvent; + const { replyEventId } = mEvent; const forwardedMeta = getForwardedMeta(mEvent.getContent()); + // Inside the panel every reply carries the spec's *fallback* reply + // relation to the thread root (`is_falling_back: true`) so non-threaded + // clients can render it as a reply. Here the root is already at the top, + // so that quote — and the "Thread" chip — is pure noise on every row + // (Gitea #165). Only a genuine reply-to-a-reply keeps its quote. + const relation = mEvent.getWireContent()?.['m.relates_to'] as + | { is_falling_back?: boolean } + | undefined; + const genuineReplyId = + replyEventId && !(relation?.is_falling_back && replyEventId === thread.id) + ? replyEventId + : undefined; return ( navigateRoom(forwardedMeta.room_id, forwardedMeta.event_id)} /> )} - {replyEventId && ( + {genuineReplyId && (