diff --git a/.changeset/add_knocking_support_when_attempting_to_join_a_room_from_the_directory_an_address_a_room_mention_or_space_hierarchy.md b/.changeset/add_knocking_support_when_attempting_to_join_a_room_from_the_directory_an_address_a_room_mention_or_space_hierarchy.md new file mode 100644 index 000000000..d905f4f82 --- /dev/null +++ b/.changeset/add_knocking_support_when_attempting_to_join_a_room_from_the_directory_an_address_a_room_mention_or_space_hierarchy.md @@ -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, as well as text command support for knocking. Also improves rendering for knock notifications in rooms. diff --git a/src/app/components/knock-room-prompt/KnockRoomPrompt.tsx b/src/app/components/knock-room-prompt/KnockRoomPrompt.tsx new file mode 100644 index 000000000..a7948e9ab --- /dev/null +++ b/src/app/components/knock-room-prompt/KnockRoomPrompt.tsx @@ -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( + 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 = (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 ( + }> + + + +
+ + Knock On Room + + + + +
+ + + + Request to join this room. You can optionally leave a reason for the moderators. + + + + Reason{' '} + + (Optional) + + + + {knockState.status === AsyncStatus.Error && ( + + Failed to knock! {knockState.error.message} + + )} + + + + +
+
+
+
+ ); +} diff --git a/src/app/components/knock-room-prompt/index.ts b/src/app/components/knock-room-prompt/index.ts new file mode 100644 index 000000000..b53201d56 --- /dev/null +++ b/src/app/components/knock-room-prompt/index.ts @@ -0,0 +1 @@ +export * from './KnockRoomPrompt'; diff --git a/src/app/components/room-card/RoomCard.tsx b/src/app/components/room-card/RoomCard.tsx index afd66d3c8..50d1f23ab 100644 --- a/src/app/components/room-card/RoomCard.tsx +++ b/src/app/components/room-card/RoomCard.tsx @@ -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, @@ -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'; @@ -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; @@ -158,6 +160,7 @@ export const RoomCard = as<'div', RoomCardProps>( topic, memberCount, roomType, + joinRule, viaServers, onView, renderTopicViewer, @@ -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; @@ -271,19 +274,38 @@ export const RoomCard = as<'div', RoomCardProps>( )} - {typeof joinedRoomId !== 'string' && joinState.status !== AsyncStatus.Error && ( - - )} + {typeof joinedRoomId !== 'string' && + joinState.status !== AsyncStatus.Error && + (joinRule === JoinRule.Knock ? ( + <> + + + {knocking && ( + setKnocking(false)} + onCancel={() => setKnocking(false)} + /> + )} + + ) : ( + + ))} {typeof joinedRoomId !== 'string' && joinState.status === AsyncStatus.Error && (