fix(threads): drop the fallback root quote on every thread reply; add Threads + Widgets to the mobile room menu (#165, #211)
Inside the thread panel each reply rendered a 'Thread ↩ <root author> <root text…>' 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -107,6 +107,10 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(
|
|||||||
const [reportRoomOpen, setReportRoomOpen] = useState(false);
|
const [reportRoomOpen, setReportRoomOpen] = useState(false);
|
||||||
const [bookmarksOpen, setBookmarksOpen] = useAtom(bookmarksPanelAtom);
|
const [bookmarksOpen, setBookmarksOpen] = useAtom(bookmarksPanelAtom);
|
||||||
const [mobileMembers, setMobileMembers] = useAtom(mobileMembersPanelAtom);
|
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 = () => {
|
const handleMarkAsRead = () => {
|
||||||
markAsRead(mx, room.roomId, hideActivity);
|
markAsRead(mx, room.roomId, hideActivity);
|
||||||
@@ -226,6 +230,38 @@ const RoomMenu = forwardRef<HTMLDivElement, RoomMenuProps>(
|
|||||||
</Text>
|
</Text>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
{screenSize === ScreenSize.Mobile && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setThreadsList(!threadsList);
|
||||||
|
requestClose();
|
||||||
|
}}
|
||||||
|
size="300"
|
||||||
|
after={<Icon size="100" src={Icons.Thread} filled={threadsList} />}
|
||||||
|
radii="300"
|
||||||
|
aria-pressed={threadsList}
|
||||||
|
>
|
||||||
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
|
Threads
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{screenSize === ScreenSize.Mobile && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setWidgets(!widgets);
|
||||||
|
requestClose();
|
||||||
|
}}
|
||||||
|
size="300"
|
||||||
|
after={<Icon size="100" src={Icons.Category} filled={widgets} />}
|
||||||
|
radii="300"
|
||||||
|
aria-pressed={widgets}
|
||||||
|
>
|
||||||
|
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
|
||||||
|
Widgets
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{!isServerNotice && (
|
{!isServerNotice && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={handleInvite}
|
onClick={handleInvite}
|
||||||
|
|||||||
@@ -734,8 +734,20 @@ export function ThreadTimeline({ room, thread, editor }: ThreadTimelineProps) {
|
|||||||
const reactionRelations = getEventReactions(timelineSet, mEventId);
|
const reactionRelations = getEventReactions(timelineSet, mEventId);
|
||||||
const reactions = reactionRelations?.getSortedAnnotationsByKey();
|
const reactions = reactionRelations?.getSortedAnnotationsByKey();
|
||||||
const hasReactions = !!reactions && reactions.length > 0;
|
const hasReactions = !!reactions && reactions.length > 0;
|
||||||
const { replyEventId, threadRootId } = mEvent;
|
const { replyEventId } = mEvent;
|
||||||
const forwardedMeta = getForwardedMeta(mEvent.getContent());
|
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 (
|
return (
|
||||||
<Message
|
<Message
|
||||||
@@ -770,12 +782,11 @@ export function ThreadTimeline({ room, thread, editor }: ThreadTimelineProps) {
|
|||||||
onJump={() => navigateRoom(forwardedMeta.room_id, forwardedMeta.event_id)}
|
onJump={() => navigateRoom(forwardedMeta.room_id, forwardedMeta.event_id)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{replyEventId && (
|
{genuineReplyId && (
|
||||||
<Reply
|
<Reply
|
||||||
room={room}
|
room={room}
|
||||||
timelineSet={timelineSet}
|
timelineSet={timelineSet}
|
||||||
replyEventId={replyEventId}
|
replyEventId={genuineReplyId}
|
||||||
threadRootId={threadRootId}
|
|
||||||
onClick={handleOpenReply}
|
onClick={handleOpenReply}
|
||||||
getMemberPowerTag={getMemberPowerTag}
|
getMemberPowerTag={getMemberPowerTag}
|
||||||
accessibleTagColors={accessiblePowerTagColors}
|
accessibleTagColors={accessiblePowerTagColors}
|
||||||
@@ -811,6 +822,7 @@ export function ThreadTimeline({ room, thread, editor }: ThreadTimelineProps) {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
navigateRoom,
|
navigateRoom,
|
||||||
|
thread.id,
|
||||||
room,
|
room,
|
||||||
messageSpacing,
|
messageSpacing,
|
||||||
messageLayout,
|
messageLayout,
|
||||||
|
|||||||
Reference in New Issue
Block a user