Compare commits

..
Author SHA1 Message Date
Lotus CIandClaude Opus 5.5 e16c923bea fix(share): room share and invite links open Lotus Chat, not matrix.to (#285)
CI / Build & Quality Checks (pull_request) Successful in 2m49s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 12s
CI / Playwright smoke (e2e) (pull_request) Successful in 17m6s
matrix.to can't send anyone to this deployment (its Cinny adapter is
app.cinny.in, web-instance[] is Element-only), so a friend scanning a
room QR or clicking an invite link was offered Element or app.cinny.in.
Share Room (link + QR) and the Invite dialog (link + QR) now use the
Lotus permalink (correct on desktop via webAppUrl). The space menus
keep both "Copy Link" (matrix.to, for other clients) and "Copy Lotus Link".

Fixes #285

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
2026-10-17 19:59:57 -04:00
3 changed files with 141 additions and 187 deletions
@@ -51,9 +51,10 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
import { BreakWord } from '../../styles/Text.css';
import { useAlive } from '../../hooks/useAlive';
import { copyToClipboard } from '../../utils/dom';
import { getMatrixToRoom } from '../../plugins/matrix-to';
import { getViaServers } from '../../plugins/via-servers';
import { useModalStyle } from '../../hooks/useModalStyle';
import { useLotusShareBase } from '../../hooks/useLotusLinkBase';
import { getLotusRoomPermalink, getLotusSpacePermalink } from '../../plugins/lotus-permalink';
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
limit: 1000,
@@ -75,10 +76,14 @@ export function InviteUserPrompt({ room, requestClose }: InviteUserProps) {
const [linkCopied, setLinkCopied] = useState(false);
const [showQr, setShowQr] = useState(false);
// [Gitea #285] A link into this deployment; matrix.to can't open Lotus Chat.
const lotusBase = useLotusShareBase();
const inviteUrl = (() => {
const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId);
const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room);
return getMatrixToRoom(roomIdOrAlias, viaServers);
return room.isSpaceRoom()
? getLotusSpacePermalink(lotusBase, roomIdOrAlias, viaServers)
: getLotusRoomPermalink(lotusBase, roomIdOrAlias, undefined, viaServers);
})();
const handleCopyLink = () => {
+134 -185
View File
@@ -255,8 +255,6 @@ function HomeCustomSection({
return (localName ?? matrixName).toLowerCase().includes(query);
});
}, [mx, section.rooms, closed, roomsWithUnreadSet, selectedRoomId, filterQuery]);
// [Gitea #289] While filtering, a section with no matches is hidden.
if (filterQuery.trim() && items.length === 0) return null;
// Sections are user-curated and small, so they render plainly — no
// virtualizer per section (a late-mounted one never measured its scroller).
return (
@@ -445,26 +443,6 @@ export function Home() {
});
}, [mx, sortedRooms, filterQuery]);
// [Gitea #289] While filtering, hide empty sections; with no match at all,
// say so instead of leaving bare headers.
const filtering = filterQuery.trim().length > 0;
const customSectionMatch = useMemo(() => {
if (!filtering) return true;
const query = filterQuery.toLowerCase();
const localNames = getLocalRoomNamesContent(mx);
return customSections.some((section) =>
section.rooms.some((rId) =>
(localNames.rooms[rId] ?? mx.getRoom(rId)?.name ?? '').toLowerCase().includes(query),
),
);
}, [mx, customSections, filtering, filterQuery]);
const noFilterMatch =
filtering &&
!customSectionMatch &&
filteredFavoriteRooms.length === 0 &&
filteredRooms.length === 0 &&
filteredLowPriorityRooms.length === 0;
const favVirtualizer = useVirtualizer({
count: filteredFavoriteRooms.length,
getScrollElement: () => scrollRef.current,
@@ -602,7 +580,7 @@ export function Home() {
/>
</Box>
</NavCategory>
{favoriteRooms.length > 0 && (!filtering || filteredFavoriteRooms.length > 0) && (
{favoriteRooms.length > 0 && (
<NavCategory>
<NavCategoryHeader>
<RoomNavCategoryButton
@@ -651,171 +629,142 @@ export function Home() {
notificationPreferences={notificationPreferences}
/>
))}
{noFilterMatch && (
<NavCategory>
<Box
direction="Column"
alignItems="Center"
gap="200"
style={{ padding: config.space.S400 }}
<NavCategory>
<NavCategoryHeader>
<RoomNavCategoryButton
closed={closedCategories.has(DEFAULT_CATEGORY_ID)}
data-category-id={DEFAULT_CATEGORY_ID}
onClick={handleCategoryClick}
>
<Text size="T300" priority="300" align="Center">
No rooms match &ldquo;{filterQuery.trim()}&rdquo;
</Text>
<Button
size="300"
variant="Secondary"
fill="Soft"
radii="300"
onClick={() => setFilterQuery('')}
>
<Text size="B300">Clear filter</Text>
</Button>
</Box>
</NavCategory>
)}
{(!filtering || filteredRooms.length > 0) && (
<NavCategory>
<NavCategoryHeader>
<RoomNavCategoryButton
closed={closedCategories.has(DEFAULT_CATEGORY_ID)}
data-category-id={DEFAULT_CATEGORY_ID}
onClick={handleCategoryClick}
>
Rooms
</RoomNavCategoryButton>
{!closedCategories.has(DEFAULT_CATEGORY_ID) && (
<>
<IconButton
aria-expanded={!!sortMenuAnchor}
aria-haspopup="menu"
variant="Background"
fill="None"
size="300"
radii="300"
onClick={(evt: React.MouseEvent<HTMLButtonElement>) => {
const cords = evt.currentTarget.getBoundingClientRect();
setSortMenuAnchor((current) => (current ? undefined : cords));
}}
aria-label="Sort rooms"
>
<Icon src={Icons.Sort} size="100" />
</IconButton>
<PopOut
anchor={sortMenuAnchor}
position="Bottom"
align="End"
offset={6}
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setSortMenuAnchor(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
>
<Menu style={{ maxWidth: toRem(180), width: '100vw' }}>
<Box
direction="Column"
gap="100"
style={{ padding: config.space.S100 }}
Rooms
</RoomNavCategoryButton>
{!closedCategories.has(DEFAULT_CATEGORY_ID) && (
<>
<IconButton
aria-expanded={!!sortMenuAnchor}
aria-haspopup="menu"
variant="Background"
fill="None"
size="300"
radii="300"
onClick={(evt: React.MouseEvent<HTMLButtonElement>) => {
const cords = evt.currentTarget.getBoundingClientRect();
setSortMenuAnchor((current) => (current ? undefined : cords));
}}
aria-label="Sort rooms"
>
<Icon src={Icons.Sort} size="100" />
</IconButton>
<PopOut
anchor={sortMenuAnchor}
position="Bottom"
align="End"
offset={6}
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setSortMenuAnchor(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
>
<Menu style={{ maxWidth: toRem(180), width: '100vw' }}>
<Box
direction="Column"
gap="100"
style={{ padding: config.space.S100 }}
>
<MenuItem
onClick={() => {
setHomeRoomSort('recent');
setSortMenuAnchor(undefined);
}}
size="300"
after={
homeRoomSort === 'recent' ? (
<Icon size="100" src={Icons.Check} />
) : undefined
}
radii="300"
>
<MenuItem
onClick={() => {
setHomeRoomSort('recent');
setSortMenuAnchor(undefined);
}}
size="300"
after={
homeRoomSort === 'recent' ? (
<Icon size="100" src={Icons.Check} />
) : undefined
}
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Recent Activity
</Text>
</MenuItem>
<MenuItem
onClick={() => {
setHomeRoomSort('alpha');
setSortMenuAnchor(undefined);
}}
size="300"
after={
homeRoomSort === 'alpha' ? (
<Icon size="100" src={Icons.Check} />
) : undefined
}
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
A → Z
</Text>
</MenuItem>
<MenuItem
onClick={() => {
setHomeRoomSort('unread');
setSortMenuAnchor(undefined);
}}
size="300"
after={
homeRoomSort === 'unread' ? (
<Icon size="100" src={Icons.Check} />
) : undefined
}
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Unread First
</Text>
</MenuItem>
</Box>
</Menu>
</FocusTrap>
}
/>
</>
)}
</NavCategoryHeader>
<div
style={{
position: 'relative',
height: virtualizer.getTotalSize(),
}}
>
{virtualizer.getVirtualItems().map((vItem) => {
const roomId = filteredRooms[vItem.index];
const room = mx.getRoom(roomId);
if (!room) return null;
const selected = selectedRoomId === roomId;
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Recent Activity
</Text>
</MenuItem>
<MenuItem
onClick={() => {
setHomeRoomSort('alpha');
setSortMenuAnchor(undefined);
}}
size="300"
after={
homeRoomSort === 'alpha' ? (
<Icon size="100" src={Icons.Check} />
) : undefined
}
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
A → Z
</Text>
</MenuItem>
<MenuItem
onClick={() => {
setHomeRoomSort('unread');
setSortMenuAnchor(undefined);
}}
size="300"
after={
homeRoomSort === 'unread' ? (
<Icon size="100" src={Icons.Check} />
) : undefined
}
radii="300"
>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
Unread First
</Text>
</MenuItem>
</Box>
</Menu>
</FocusTrap>
}
/>
</>
)}
</NavCategoryHeader>
<div
style={{
position: 'relative',
height: virtualizer.getTotalSize(),
}}
>
{virtualizer.getVirtualItems().map((vItem) => {
const roomId = filteredRooms[vItem.index];
const room = mx.getRoom(roomId);
if (!room) return null;
const selected = selectedRoomId === roomId;
return (
<VirtualTile
virtualItem={vItem}
key={roomId}
ref={virtualizer.measureElement}
>
<RoomNavItem
room={room}
selected={selected}
linkPath={getHomeRoomPath(getCanonicalAliasOrRoomId(mx, roomId))}
notificationMode={getRoomNotificationMode(
notificationPreferences,
room.roomId,
)}
/>
</VirtualTile>
);
})}
</div>
</NavCategory>
)}
{lowPriorityRooms.length > 0 && (!filtering || filteredLowPriorityRooms.length > 0) && (
return (
<VirtualTile virtualItem={vItem} key={roomId} ref={virtualizer.measureElement}>
<RoomNavItem
room={room}
selected={selected}
linkPath={getHomeRoomPath(getCanonicalAliasOrRoomId(mx, roomId))}
notificationMode={getRoomNotificationMode(
notificationPreferences,
room.roomId,
)}
/>
</VirtualTile>
);
})}
</div>
</NavCategory>
{lowPriorityRooms.length > 0 && (
<NavCategory>
<NavCategoryHeader>
<RoomNavCategoryButton