feat: Integrate forum layout support across various components and enhance room handling logic

This commit is contained in:
2026-07-06 01:06:20 +10:00
parent d1626e3585
commit c57797ffe4
62 changed files with 6876 additions and 385 deletions

View File

@@ -0,0 +1,8 @@
import { style } from '@vanilla-extract/css';
import { color } from 'folds';
export const ForumBanner = style({
borderLeft: `3px solid ${color.Primary.Main}`,
background: color.Primary.Container,
color: color.Primary.OnContainer,
});

View File

@@ -0,0 +1,50 @@
import React, { useEffect } from 'react';
import { Room } from 'matrix-js-sdk';
import { useSearchParams } from 'react-router-dom';
import { useAtomValue } from 'jotai';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useSpaceOptionally } from '../../hooks/useSpace';
import { roomToParentsAtom } from '../../state/room/roomToParents';
import { findForumRootSpace, shouldShowForumLobby } from '../../utils/room';
import { ForumSpaceShell } from '../forum/ForumSpaceShell';
import { RoomView } from './RoomView';
type ForumRoomViewProps = {
room: Room;
eventId?: string;
};
export function ForumRoomView({ room, eventId }: ForumRoomViewProps) {
const mx = useMatrixClient();
const space = useSpaceOptionally();
const roomToParents = useAtomValue(roomToParentsAtom);
const [, setSearchParams] = useSearchParams();
const forumSpaceId =
space && shouldShowForumLobby(space)
? space.roomId
: findForumRootSpace(mx, room.roomId, roomToParents);
const forumSpace = forumSpaceId ? mx.getRoom(forumSpaceId) : null;
useEffect(() => {
if (!eventId) return;
setSearchParams(
(prev) => {
const next = new URLSearchParams(prev);
next.set('post', eventId);
if (!next.get('topic') && room.roomId !== forumSpaceId) {
next.set('topic', room.roomId);
}
return next;
},
{ replace: true }
);
}, [eventId, setSearchParams, room.roomId, forumSpaceId]);
if (forumSpace && shouldShowForumLobby(forumSpace)) {
return <ForumSpaceShell />;
}
return <RoomView room={room} eventId={eventId} />;
}

View File

@@ -15,6 +15,8 @@ import { useMarkAsRead } from '../../hooks/useMarkAsRead';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useRoomMembers } from '../../hooks/useRoomMembers';
import { activeRoomIdAtom } from '../../state/activeRoom';
import { isForum } from '../../utils/room';
import { ForumRoomView } from './ForumRoomView';
export function Room() {
const { eventId } = useParams();
@@ -28,6 +30,7 @@ export function Room() {
const powerLevels = usePowerLevels(room);
const members = useRoomMembers(mx, room.roomId);
const markAsRead = useMarkAsRead(mx);
const forumRoom = isForum(room);
// Update titlebar with current room ID
useEffect(() => {
@@ -50,7 +53,7 @@ export function Room() {
return (
<PowerLevelsContextProvider value={powerLevels}>
<Box grow="Yes">
<RoomView room={room} eventId={eventId} />
{forumRoom ? <ForumRoomView room={room} eventId={eventId} /> : <RoomView room={room} eventId={eventId} />}
{screenSize === ScreenSize.Desktop && isDrawer && (
<>
<Line variant="Background" direction="Vertical" size="300" />

View File

@@ -132,9 +132,10 @@ interface RoomInputProps {
room: Room;
/** When provided, all messages are sent as replies in this thread. */
threadRootId?: string;
onMessageSent?: () => void;
}
export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
({ editor, fileDropContainerRef, roomId, room, threadRootId }, ref) => {
({ editor, fileDropContainerRef, roomId, room, threadRootId, onMessageSent }, ref) => {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const [enterForNewline] = useSetting(settingsAtom, 'enterForNewline');
@@ -480,7 +481,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
setReplyDraft(undefined);
setCommandHint(undefined);
sendTypingStatus(false);
}, [mx, roomId, threadRootId, editor, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown, commands, autoConvertEmoticons]);
onMessageSent?.();
}, [mx, roomId, threadRootId, editor, replyDraft, sendTypingStatus, setReplyDraft, isMarkdown, commands, autoConvertEmoticons, onMessageSent]);
const handleKeyDown: KeyboardEventHandler = useCallback(
(evt) => {

View File

@@ -48,6 +48,7 @@ import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
import { useMarkAsRead } from '../../hooks/useMarkAsRead';
import { copyToClipboard } from '../../utils/dom';
import { SpeechBubble } from '../../components/speech-bubble/SpeechBubble';
import { LeaveRoomPrompt } from '../../components/leave-room-prompt';
import { useRoomAvatar, useRoomName, useRoomTopic } from '../../hooks/useRoomMeta';
import { mDirectAtom } from '../../state/mDirectList';
@@ -316,35 +317,19 @@ function CallIndicator({ roomId }: CallIndicatorProps) {
position="Bottom"
offset={8}
tooltip={
<Box
direction="Column"
gap="50"
style={{
padding: `${toRem(8)} ${toRem(12)}`,
backgroundColor: color.SurfaceVariant.Container,
color: color.SurfaceVariant.OnContainer,
borderRadius: toRem(8),
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
position: 'relative',
}}
>
{/* Speech bubble arrow */}
<div
style={{
position: 'absolute',
top: toRem(-6),
left: '50%',
transform: 'translateX(-50%)',
width: 0,
height: 0,
borderLeft: `${toRem(6)} solid transparent`,
borderRight: `${toRem(6)} solid transparent`,
borderBottom: `${toRem(6)} solid ${color.SurfaceVariant.Container}`,
}}
/>
<Text size="T300" style={{ fontWeight: 600, color: color.SurfaceVariant.OnContainer }}>{displayName}</Text>
<Text size="T200" style={{ opacity: 0.7, color: color.SurfaceVariant.OnContainer }}>{member.userId}</Text>
</Box>
<SpeechBubble tail="top" tailX="50%">
<Box direction="Column" gap="50">
<Text
size="T300"
style={{ fontWeight: 600, color: color.SurfaceVariant.OnContainer }}
>
{displayName}
</Text>
<Text size="T200" style={{ opacity: 0.7, color: color.SurfaceVariant.OnContainer }}>
{member.userId}
</Text>
</Box>
</SpeechBubble>
}
>
{(triggerRef) => (
@@ -490,7 +475,11 @@ function RoomCallButtons({ roomId }: RoomCallButtonsProps) {
);
}
export function RoomViewHeader() {
type RoomViewHeaderProps = {
forumLayout?: boolean;
};
export function RoomViewHeader({ forumLayout = false }: RoomViewHeaderProps) {
const navigate = useNavigate();
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
@@ -564,9 +553,16 @@ export function RoomViewHeader() {
</Avatar>
)}
<Box direction="Column">
<Text size={topic ? 'H5' : 'H3'} truncate>
{name}
</Text>
<Box alignItems="Center" gap="100">
<Text size={topic ? 'H5' : 'H3'} truncate>
{name}
</Text>
{forumLayout && (
<Badge variant="Secondary" fill="Soft" outlined>
<Text size="T200">Forum</Text>
</Badge>
)}
</Box>
{topic && (
<UseStateProvider initial={false}>
{(viewTopic, setViewTopic) => (