feat(room-preview): join-rule + encryption chips, Request-to-join for knock rooms
Room preview (JoinBeforeNavigate -> RoomCard via getRoomSummary) was already built and, verified after the Synapse 1.156 upgrade, works via the SDK's unstable im.nheko.summary endpoint (the old 'blocked' flag tested the wrong /v1 path). Polish the preview card with the summary fields the endpoint returns: - join-rule chip (Restricted / Ask to join / Invite only / Private; public shows none) + an Encrypted badge (from im.nheko.summary.encryption). - knock-rule rooms now show a 'Request to join' button (mx.knockRoom) instead of a plain Join that would fail — mirrors the RoomIntro knock flow. Props are optional so other RoomCard usages are unaffected. LOTUS_TODO updated: Room Preview BLOCKED -> done; Synapse 1.155 -> 1.156.0. 738 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { ReactNode, useCallback, useRef, useState } from 'react';
|
||||
import { MatrixError, Room } from 'matrix-js-sdk';
|
||||
import { JoinRule, MatrixError, Room } from 'matrix-js-sdk';
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
@@ -139,11 +139,23 @@ type RoomCardProps = {
|
||||
topic?: string;
|
||||
memberCount?: number;
|
||||
roomType?: string;
|
||||
joinRule?: string;
|
||||
encrypted?: boolean;
|
||||
viaServers?: string[];
|
||||
onView?: (roomId: string) => void;
|
||||
renderTopicViewer: (name: string, topic: string, requestClose: () => void) => ReactNode;
|
||||
};
|
||||
|
||||
// Map a room's join rule to a short preview chip. Public is the common case and
|
||||
// gets no chip (avoids noise); the rest tell the user how entry works.
|
||||
const joinRuleLabel = (joinRule?: string): string | undefined => {
|
||||
if (joinRule === JoinRule.Restricted || joinRule === 'knock_restricted') return 'Restricted';
|
||||
if (joinRule === JoinRule.Knock) return 'Ask to join';
|
||||
if (joinRule === JoinRule.Invite) return 'Invite only';
|
||||
if (joinRule === JoinRule.Private) return 'Private';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const RoomCard = as<'div', RoomCardProps>(
|
||||
(
|
||||
{
|
||||
@@ -154,6 +166,8 @@ export const RoomCard = as<'div', RoomCardProps>(
|
||||
topic,
|
||||
memberCount,
|
||||
roomType,
|
||||
joinRule,
|
||||
encrypted,
|
||||
viaServers,
|
||||
onView,
|
||||
renderTopicViewer,
|
||||
@@ -203,8 +217,28 @@ export const RoomCard = as<'div', RoomCardProps>(
|
||||
[mx, roomIdOrAlias, viaServers],
|
||||
),
|
||||
);
|
||||
const joining =
|
||||
joinState.status === AsyncStatus.Loading || joinState.status === AsyncStatus.Success;
|
||||
const [knockState, knock] = useAsyncCallback<{ room_id: string }, MatrixError, []>(
|
||||
useCallback(
|
||||
() => mx.knockRoom(roomIdOrAlias, { viaServers }),
|
||||
[mx, roomIdOrAlias, viaServers],
|
||||
),
|
||||
);
|
||||
// A knock-rule room can't be joined directly — request to join instead.
|
||||
const isKnock = joinRule === JoinRule.Knock;
|
||||
const action = isKnock ? knock : join;
|
||||
const actionState = isKnock ? knockState : joinState;
|
||||
const acting =
|
||||
actionState.status === AsyncStatus.Loading || actionState.status === AsyncStatus.Success;
|
||||
let actionLabel = acting ? 'Joining' : 'Join';
|
||||
if (isKnock) {
|
||||
actionLabel =
|
||||
knockState.status === AsyncStatus.Success
|
||||
? 'Requested'
|
||||
: acting
|
||||
? 'Requesting'
|
||||
: 'Request to join';
|
||||
}
|
||||
const chip = joinRuleLabel(joinRule);
|
||||
|
||||
const [viewTopic, setViewTopic] = useState(false);
|
||||
const closeTopic = () => setViewTopic(false);
|
||||
@@ -258,6 +292,20 @@ export const RoomCard = as<'div', RoomCardProps>(
|
||||
<Text size="T200">{`${millify(joinedMemberCount)} Members`}</Text>
|
||||
</Box>
|
||||
)}
|
||||
{!joinedRoom && (chip || encrypted) && (
|
||||
<Box gap="100" wrap="Wrap">
|
||||
{chip && (
|
||||
<Badge variant="Secondary" fill="Soft" outlined>
|
||||
<Text size="L400">{chip}</Text>
|
||||
</Badge>
|
||||
)}
|
||||
{encrypted && (
|
||||
<Badge variant="Success" fill="Soft" outlined>
|
||||
<Text size="L400">Encrypted</Text>
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{typeof joinedRoomId === 'string' && (
|
||||
<Button
|
||||
onClick={onView ? () => onView(joinedRoomId) : undefined}
|
||||
@@ -270,23 +318,23 @@ export const RoomCard = as<'div', RoomCardProps>(
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
{typeof joinedRoomId !== 'string' && joinState.status !== AsyncStatus.Error && (
|
||||
{typeof joinedRoomId !== 'string' && actionState.status !== AsyncStatus.Error && (
|
||||
<Button
|
||||
onClick={join}
|
||||
onClick={action}
|
||||
variant="Secondary"
|
||||
size="300"
|
||||
disabled={joining}
|
||||
before={joining && <Spinner size="50" variant="Secondary" fill="Soft" />}
|
||||
disabled={acting}
|
||||
before={acting && <Spinner size="50" variant="Secondary" fill="Soft" />}
|
||||
>
|
||||
<Text size="B300" truncate>
|
||||
{joining ? 'Joining' : 'Join'}
|
||||
{actionLabel}
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
{typeof joinedRoomId !== 'string' && joinState.status === AsyncStatus.Error && (
|
||||
{typeof joinedRoomId !== 'string' && actionState.status === AsyncStatus.Error && (
|
||||
<Box gap="200">
|
||||
<Button
|
||||
onClick={join}
|
||||
onClick={action}
|
||||
className={css.ActionButton}
|
||||
variant="Critical"
|
||||
fill="Solid"
|
||||
@@ -297,8 +345,8 @@ export const RoomCard = as<'div', RoomCardProps>(
|
||||
</Text>
|
||||
</Button>
|
||||
<ErrorDialog
|
||||
title="Join Error"
|
||||
message={joinState.error.message || 'Failed to join. Unknown Error.'}
|
||||
title={isKnock ? 'Request Error' : 'Join Error'}
|
||||
message={actionState.error.message || 'Failed to join. Unknown Error.'}
|
||||
>
|
||||
{(openError) => (
|
||||
<Button
|
||||
|
||||
@@ -66,6 +66,8 @@ export function JoinBeforeNavigate({
|
||||
topic={summary?.topic}
|
||||
memberCount={summary?.num_joined_members}
|
||||
roomType={summary?.room_type}
|
||||
joinRule={summary?.join_rule}
|
||||
encrypted={!!summary?.['im.nheko.summary.encryption']}
|
||||
viaServers={viaServers}
|
||||
renderTopicViewer={(name, topic, requestClose) => (
|
||||
<RoomTopicViewer name={name} topic={topic} requestClose={requestClose} />
|
||||
|
||||
Reference in New Issue
Block a user