Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

# Add knocking support when attempting to join a room from the directory, an address, a room mention, or space hierarchy.
139 changes: 139 additions & 0 deletions src/app/components/knock-room-prompt/KnockRoomPrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { useCallback, useEffect, FormEventHandler } from 'react';
import FocusTrap from 'focus-trap-react';
import {
Dialog,
Overlay,
OverlayCenter,
OverlayBackdrop,
Header,
config,
Box,
Text,
IconButton,
Icon,
Icons,
Input,
color,
Button,
Spinner,
} from 'folds';
import { MatrixError } from '$types/matrix-sdk';

import { useMatrixClient } from '$hooks/useMatrixClient';
import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback';
import { stopPropagation } from '$utils/keyboard';
import { createDebugLogger } from '$utils/debugLogger';

const debugLog = createDebugLogger('KnockRoomPrompt');

type KnockRoomProps = {
roomId: string;
via?: string | string[];
onDone: () => void;
onCancel: () => void;
};
export function KnockRoomPrompt({ roomId, via, onDone, onCancel }: KnockRoomProps) {
const mx = useMatrixClient();

const [knockState, knockRoom] = useAsyncCallback<undefined, MatrixError, [string?]>(
useCallback(
async (reason?: string) => {
debugLog.info('ui', 'Knock room button clicked', { roomId });
mx.knockRoom(roomId, { viaServers: via || undefined, reason });
},
[mx, roomId, via]
)
);

const handleKnock: FormEventHandler<HTMLFormElement> = (evt) => {
evt.preventDefault();
const target = evt.target as HTMLFormElement;
const reasonInput = (target?.reasonInput as HTMLInputElement) || undefined;
const reason = reasonInput?.value.trim() || undefined;
knockRoom(reason);
};

useEffect(() => {
if (knockState.status === AsyncStatus.Success) {
debugLog.info('ui', 'Successfully knocked on room', { roomId });
onDone();
}
}, [knockState, onDone, roomId]);

return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: onCancel,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Dialog variant="Surface">
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">Knock On Room</Text>
</Box>
<IconButton size="300" onClick={onCancel} radii="300">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Box
as="form"
onSubmit={handleKnock}
style={{ padding: config.space.S400 }}
direction="Column"
gap="400"
>
<Box direction="Column" gap="200">
<Text priority="400">
Request to join this room. You can optionally leave a reason for the moderators.
</Text>
<Box direction="Column" gap="100">
<Text size="L400">
Reason{' '}
<Text as="span" size="T200">
(Optional)
</Text>
</Text>
<Input name="reasonInput" variant="Background" />
{knockState.status === AsyncStatus.Error && (
<Text style={{ color: color.Critical.Main }} size="T300">
Failed to knock! {knockState.error.message}
</Text>
)}
</Box>
</Box>
<Button
type="submit"
variant="Primary"
before={
knockState.status === AsyncStatus.Loading ? (
<Spinner fill="Solid" variant="Primary" size="200" />
) : undefined
}
aria-disabled={
knockState.status === AsyncStatus.Loading ||
knockState.status === AsyncStatus.Success
}
>
<Text size="B400">
{knockState.status === AsyncStatus.Loading ? 'Knocking...' : 'Knock'}
</Text>
</Button>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
);
}
1 change: 1 addition & 0 deletions src/app/components/knock-room-prompt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './KnockRoomPrompt';
52 changes: 37 additions & 15 deletions src/app/components/room-card/RoomCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReactNode, useCallback, useRef, useState } from 'react';
import { MatrixError, Room } from '$types/matrix-sdk';
import { JoinRule, MatrixError, Room } from '$types/matrix-sdk';
import {
Avatar,
Badge,
Expand Down Expand Up @@ -31,6 +31,7 @@ import { useElementSizeObserver } from '$hooks/useElementSizeObserver';
import { getRoomAvatarUrl, getStateEvent } from '$utils/room';
import { useStateEventCallback } from '$hooks/useStateEventCallback';
import { useMediaAuthentication } from '$hooks/useMediaAuthentication';
import { KnockRoomPrompt } from '$components/knock-room-prompt';
import { RoomAvatar } from '$components/room-avatar';
import * as css from './style.css';

Expand Down Expand Up @@ -143,6 +144,7 @@ type RoomCardProps = {
topic?: string;
memberCount?: number;
roomType?: string;
joinRule?: JoinRule;
viaServers?: string[];
onView?: (roomId: string) => void;
renderTopicViewer: (name: string, topic: string, requestClose: () => void) => ReactNode;
Expand All @@ -158,6 +160,7 @@ export const RoomCard = as<'div', RoomCardProps>(
topic,
memberCount,
roomType,
joinRule,
viaServers,
onView,
renderTopicViewer,
Expand All @@ -172,7 +175,7 @@ export const RoomCard = as<'div', RoomCardProps>(
const [topicEvent, setTopicEvent] = useState(() =>
joinedRoom ? getStateEvent(joinedRoom, StateEvent.RoomTopic) : undefined
);

const [knocking, setKnocking] = useState(false);
const fallbackName = getMxIdLocalPart(roomIdOrAlias) ?? roomIdOrAlias;
const fallbackTopic = roomIdOrAlias;

Expand Down Expand Up @@ -271,19 +274,38 @@ export const RoomCard = as<'div', RoomCardProps>(
</Text>
</Button>
)}
{typeof joinedRoomId !== 'string' && joinState.status !== AsyncStatus.Error && (
<Button
onClick={join}
variant="Secondary"
size="300"
disabled={joining}
before={joining && <Spinner size="50" variant="Secondary" fill="Soft" />}
>
<Text size="B300" truncate>
{joining ? 'Joining' : 'Join'}
</Text>
</Button>
)}
{typeof joinedRoomId !== 'string' &&
joinState.status !== AsyncStatus.Error &&
(joinRule === JoinRule.Knock ? (
<>
<Button onClick={() => setKnocking(true)} variant="Secondary" size="300">
<Text size="B300" truncate>
Knock
</Text>
</Button>

{knocking && (
<KnockRoomPrompt
roomId={roomIdOrAlias}
via={viaServers}
onDone={() => setKnocking(false)}
onCancel={() => setKnocking(false)}
/>
)}
</>
) : (
<Button
onClick={join}
variant="Secondary"
size="300"
disabled={joining}
before={joining && <Spinner size="50" variant="Secondary" fill="Soft" />}
>
<Text size="B300" truncate>
{joining ? 'Joining' : 'Join'}
</Text>
</Button>
))}
{typeof joinedRoomId !== 'string' && joinState.status === AsyncStatus.Error && (
<Box gap="200">
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function JoinBeforeNavigate({
topic={summary?.topic}
memberCount={summary?.num_joined_members}
roomType={summary?.room_type}
joinRule={summary?.join_rule}
viaServers={viaServers}
renderTopicViewer={(name, topic, requestClose) => (
<RoomTopicViewer name={name} topic={topic} requestClose={requestClose} />
Expand Down
59 changes: 55 additions & 4 deletions src/app/features/lobby/RoomItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { SequenceCard } from '$components/sequence-card';
import { useMatrixClient } from '$hooks/useMatrixClient';
import { HierarchyItem } from '$hooks/useSpaceHierarchy';
import { millify } from '$plugins/millify';
import { KnockRoomPrompt } from '$components/knock-room-prompt';
import { LocalRoomSummaryLoader } from '$components/RoomSummaryLoader';
import { UseStateProvider } from '$components/UseStateProvider';
import { RoomTopicViewer } from '$components/room-topic-viewer';
Expand All @@ -44,7 +45,6 @@ type RoomJoinButtonProps = {
};
function RoomJoinButton({ roomId, via }: RoomJoinButtonProps) {
const mx = useMatrixClient();

const [joinState, join] = useAsyncCallback<Room, MatrixError, []>(
useCallback(() => mx.joinRoom(roomId, { viaServers: via }), [mx, roomId, via])
);
Expand Down Expand Up @@ -96,6 +96,47 @@ function RoomJoinButton({ roomId, via }: RoomJoinButtonProps) {
);
}

function RoomKnockButton({ roomId, via }: RoomJoinButtonProps) {
return (
<UseStateProvider initial={false}>
{(knocking, setKnocking) => (
<Box shrink="No" gap="200" alignItems="Center">
<Chip
variant="Secondary"
fill="Soft"
size="400"
radii="Pill"
before=<Icon src={Icons.MailPlus} size="50" />
onClick={() => setKnocking(true)}
>
<Text size="B300">Knock</Text>
</Chip>
{knocking && (
<KnockRoomPrompt
roomId={roomId}
via={via}
onDone={() => setKnocking(false)}
onCancel={() => setKnocking(false)}
/>
)}
</Box>
)}
</UseStateProvider>
);
}

type RoomJoinOrKnockButtonProps = {
roomId: string;
via?: string[];
joinRule?: JoinRule;
};
function RoomJoinOrKnockButton({ roomId, via, joinRule }: RoomJoinOrKnockButtonProps) {
if (joinRule === JoinRule.Knock) {
return <RoomKnockButton roomId={roomId} via={via} />;
}
return <RoomJoinButton roomId={roomId} via={via} />;
}

function RoomProfileLoading() {
return (
<Box grow="Yes" gap="300">
Expand Down Expand Up @@ -167,7 +208,7 @@ function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProf
)}
</Box>
</Box>
{!inaccessibleRoom && <RoomJoinButton roomId={roomId} via={via} />}
{!inaccessibleRoom && <RoomJoinOrKnockButton roomId={roomId} via={via} />}
</Box>
);
}
Expand Down Expand Up @@ -364,7 +405,11 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
</Chip>
</Box>
) : (
<RoomJoinButton roomId={roomId} via={content.via} />
<RoomJoinOrKnockButton
roomId={roomId}
via={content.via}
joinRule={localSummary.joinRule}
/>
)
}
/>
Expand Down Expand Up @@ -408,7 +453,13 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
memberCount={summary.num_joined_members}
suggested={content.suggested}
joinRule={summary.join_rule}
options={<RoomJoinButton roomId={roomId} via={content.via} />}
options={
<RoomJoinOrKnockButton
roomId={roomId}
via={content.via}
joinRule={summary.join_rule}
/>
}
/>
)}
</>
Expand Down
2 changes: 2 additions & 0 deletions src/app/pages/client/explore/Featured.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function FeaturedRooms() {
name={roomSummary?.name}
topic={roomSummary?.topic}
memberCount={roomSummary?.num_joined_members}
joinRule={roomSummary?.join_rule}
onView={navigateSpace}
renderTopicViewer={(name, topic, requestClose) => (
<RoomTopicViewer
Expand Down Expand Up @@ -96,6 +97,7 @@ export function FeaturedRooms() {
name={roomSummary?.name}
topic={roomSummary?.topic}
memberCount={roomSummary?.num_joined_members}
joinRule={roomSummary?.join_rule}
onView={navigateRoom}
renderTopicViewer={(name, topic, requestClose) => (
<RoomTopicViewer
Expand Down
1 change: 1 addition & 0 deletions src/app/pages/client/explore/Server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@ export function PublicRooms() {
topic={chunkRoom.topic}
memberCount={chunkRoom.num_joined_members}
roomType={chunkRoom.room_type}
joinRule={chunkRoom.join_rule}
onView={
chunkRoom.room_type === RoomType.Space
? navigateSpace
Expand Down
Loading