Skip to content
Open
5 changes: 5 additions & 0 deletions .changeset/add-forum-room-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Add the `forum` room type with a dedicated forum view that presents threads as topics.
19 changes: 16 additions & 3 deletions src/app/components/RenderMessageContent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CSSProperties } from 'react';
import { memo, useMemo, useCallback } from 'react';
import type { IPreviewUrlResponse, MatrixClient, MatrixEvent, Room } from '$types/matrix-sdk';
import { MsgType } from '$types/matrix-sdk';
import { EventType, MsgType } from '$types/matrix-sdk';
import { parseSettingsLink } from '$features/settings/settingsLink';
import { useSettingsLinkBaseUrl } from '$features/settings/useSettingsLinkBaseUrl';
import { testMatrixTo } from '$plugins/matrix-to';
Expand All @@ -23,6 +23,7 @@ import {
MImage,
MLocation,
MNotice,
MStrickerWrappper,
MText,
MVideo,
ReadPdfFile,
Expand All @@ -48,7 +49,7 @@ import { TextViewer } from './text-viewer';
import { ClientSideHoverFreeze } from './ClientSideHoverFreeze';
import { CuteEventType, MCuteEvent } from './message/MCuteEvent';
import { PollEvent } from './message/PollEvent';
import { M_TEXT } from 'matrix-js-sdk';
import { M_POLL_START, M_TEXT } from 'matrix-js-sdk';
import type { IImageInfo } from '$types/matrix/common';

type RenderMessageContentProps = {
Expand All @@ -70,6 +71,7 @@ type RenderMessageContentProps = {
mEvent?: MatrixEvent;
mx?: MatrixClient;
room?: Room;
autoplayStickers?: boolean;
};

const getMediaType = (url: string) => {
Expand Down Expand Up @@ -106,6 +108,7 @@ function RenderMessageContentInternal({
mEvent,
mx,
room,
autoplayStickers,
}: RenderMessageContentProps) {
const content = useMemo(() => getContent() as Record<string, unknown>, [getContent]);

Expand Down Expand Up @@ -460,11 +463,21 @@ function RenderMessageContentInternal({
}
/>
);
if (content['org.matrix.msc3381.poll.start']) {
if (content[M_POLL_START.name]) {
if (mEvent && mx && room)
return <PollEvent content={content} mEvent={mEvent} mx={mx} room={room} />;
else return <UnsupportedContent />;
}
if (mEvent?.getType() === EventType.Sticker) {
return (
<MStrickerWrappper
mEvent={mEvent}
autoplayStickers={autoplayStickers}
mediaAutoLoad={mediaAutoLoad}
/>
);
}

return (
<UnsupportedContent
body={
Expand Down
26 changes: 26 additions & 0 deletions src/app/components/create-room/CreateRoomTypeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,32 @@ export function CreateRoomTypeSelector({
</Box>
</SettingTile>
</SequenceCard>
<SequenceCard
style={{ padding: config.space.S300 }}
variant={value === CreateRoomType.ForumRoom ? 'Primary' : 'SurfaceVariant'}
direction="Column"
gap="100"
as="button"
type="button"
aria-pressed={value === CreateRoomType.ForumRoom}
onClick={() => onSelect(CreateRoomType.ForumRoom)}
disabled={disabled}
>
<SettingTile
before={getIcon(CreateRoomType.ForumRoom)}
after={value === CreateRoomType.ForumRoom && sizedIcon(Check)}
>
<Box gap="200" alignItems="Baseline">
<Text size="H6" style={{ flexShrink: 0 }}>
Forum Room
</Text>
<Text size="T300" priority="300" truncate>
- Conversations split in topics.
</Text>
<BetaNoticeBadge />
</Box>
</SettingTile>
</SequenceCard>
</Box>
);
}
1 change: 1 addition & 0 deletions src/app/components/create-room/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum CreateRoomType {
TextRoom = 'text',
VoiceRoom = 'voice',
ForumRoom = 'forum',
}

export enum CreateRoomAccess {
Expand Down
5 changes: 3 additions & 2 deletions src/app/components/create-room/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import type {
import { JoinRule, RestrictedAllowType, EventType, RoomType } from '$types/matrix-sdk';

import type { StateEvents } from '$types/matrix-sdk';
import type { CustomRoomType } from '$types/matrix/room';
import { getViaServers } from '$plugins/via-servers';
import { getMxIdServer } from '$utils/mxIdHelper';
import { CreateRoomAccess } from './types';
import * as prefix from '$unstable/prefixes';

export const createRoomCreationContent = (
type: RoomType | undefined,
type: RoomType | CustomRoomType | undefined,
allowFederation: boolean,
additionalCreators: string[] | undefined
): object => {
Expand Down Expand Up @@ -101,7 +102,7 @@ export const createVoiceRoomPowerLevelsOverride = () => ({

export type CreateRoomData = {
version: string;
type?: RoomType;
type?: RoomType | CustomRoomType;
parent?: Room;
access: CreateRoomAccess;
name: string;
Expand Down
28 changes: 26 additions & 2 deletions src/app/components/icons/roomIcons.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { JoinRule, RoomType } from '$types/matrix-sdk';
import type { ComponentType } from 'react';
import type { IconProps } from '@phosphor-icons/react';
import { Globe, HashStraight, Lock, SpeakerHigh, SquaresFour } from './phosphor';
import { CustomRoomType } from '$types/matrix/room';
import { Chats, Globe, HashStraight, Lock, SpeakerHigh, SquaresFour } from './phosphor';

export type RoomPhosphorIcon = ComponentType<IconProps>;

export type RoomIconOverlay = 'globe' | 'lock';

const isRegularRoom = (roomType?: string): boolean =>
roomType !== RoomType.Space && roomType !== RoomType.UnstableCall;
roomType !== RoomType.Space &&
roomType !== RoomType.UnstableCall;

export function getRoomIconOverlay(
roomType?: string,
Expand Down Expand Up @@ -59,6 +61,17 @@ export function getRoomStandaloneIconComponent(
return SpeakerHigh;
}

if (roomType === CustomRoomType.Forum) {
if (
joinRule === JoinRule.Invite ||
joinRule === JoinRule.Knock ||
joinRule === JoinRule.Private
) {
return Lock;
}
return Chats;
}

if (joinRule === JoinRule.Public) return Globe;
if (
joinRule === JoinRule.Invite ||
Expand Down Expand Up @@ -95,5 +108,16 @@ export function getRoomIconComponent(roomType?: string, joinRule?: JoinRule): Ro
return SpeakerHigh;
}

if (roomType === CustomRoomType.Forum) {
if (
joinRule === JoinRule.Invite ||
joinRule === JoinRule.Knock ||
joinRule === JoinRule.Private
) {
return Lock;
}
return Chats;
}

return HashStraight;
}
39 changes: 39 additions & 0 deletions src/app/components/message/MsgTypeRenderers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type CSSProperties, type ReactNode, useMemo } from 'react';
import { ArrowSquareOut, sizedIcon, Link } from '$components/icons/phosphor';
import { Box, Chip, Text, toRem } from 'folds';
import type { MatrixEvent } from '$types/matrix-sdk';
import { type IContent, type IPreviewUrlResponse, type MatrixClient } from '$types/matrix-sdk';
import { isJumboEmojiText } from '$utils/emojiDetection';
import { trimReplyFromBody } from '$utils/room';
Expand All @@ -25,6 +26,7 @@ import type { PerMessageProfileBeeperFormat } from '$hooks/usePerMessageProfile'
import { Attachment, AttachmentBox, AttachmentContent, AttachmentHeader } from './attachment';
import { FileHeader, FileDownloadButton } from './FileHeader';
import {
ImageContent,
MessageBadEncryptedContent,
MessageBrokenContent,
MessageDeletedContent,
Expand All @@ -40,9 +42,12 @@ import { copyToClipboard } from '$utils/dom';
import { MapContainer, Marker, TileLayer } from 'react-leaflet';
import type { LatLngExpression } from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { Image } from '$components/media';
import { ImageViewer } from '$components/image-viewer';

import * as css from './MsgTypeRenderers.css';
import { markerIcon } from '$features/room/location-modal/LocationDialog';
import { ClientSideHoverFreeze } from '$components/ClientSideHoverFreeze';

export interface BundleContent extends IPreviewUrlResponse {
matched_url: string;
Expand Down Expand Up @@ -777,3 +782,37 @@ export function MSticker({ content, renderImageContent }: MStickerProps) {
</AttachmentBox>
);
}
type MStrickerWrappperProps = {
mEvent: MatrixEvent;
autoplayStickers?: boolean;
mediaAutoLoad?: boolean;
};

export function MStrickerWrappper({
mEvent,
autoplayStickers,
mediaAutoLoad,
}: MStrickerWrappperProps) {
return (
<MSticker
content={mEvent.getContent() as unknown as IImageContent}
renderImageContent={(props) => (
<ImageContent
{...props}
autoPlay={mediaAutoLoad}
renderImage={(p) => {
if (!autoplayStickers && p.src) {
return (
<ClientSideHoverFreeze src={p.src}>
<Image {...p} loading="lazy" />
</ClientSideHoverFreeze>
);
}
return <Image {...p} loading="lazy" />;
}}
renderViewer={(p) => <ImageViewer {...p} />}
/>
)}
/>
);
}
2 changes: 1 addition & 1 deletion src/app/components/message/PollEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export function PollEvent({ content, mEvent, mx, room }: PollEventProps) {
rel_type: 'm.reference',
event_id: eventId,
},
'org.matrix.msc3381.poll.end': {},
[M_POLL_END.name]: {},
[M_TEXT.name]: endText,
body: endText,
msgtype: 'm.text',
Expand Down
61 changes: 35 additions & 26 deletions src/app/components/message/modals/Options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ export function OptionQuickMenu({
onReplyClick,
onEditId,
hideReadReceipts,
hideReplyButton,
showDeveloperTools,
canPinEvent,
cleanedDisplayName,
Expand Down Expand Up @@ -330,18 +331,20 @@ export function OptionQuickMenu({
</IconButton>
</>
)}
<IconButton
onClick={(ev) => {
onReplyClick(ev);
closeMenu();
}}
data-event-id={mEvent.getId()}
variant="SurfaceVariant"
size="300"
radii="300"
>
{menuIcon(ArrowBendUpLeftIcon)}
</IconButton>
{!hideReplyButton && (
<IconButton
onClick={(ev) => {
onReplyClick(ev);
closeMenu();
}}
data-event-id={mEvent.getId()}
variant="SurfaceVariant"
size="300"
radii="300"
>
{menuIcon(ArrowBendUpLeftIcon)}
</IconButton>
)}
{!isThreadedMessage && (
<IconButton
onClick={(ev) => {
Expand Down Expand Up @@ -384,6 +387,7 @@ export function OptionQuickMenu({
onReplyClick={onReplyClick}
onEditId={onEditId}
hideReadReceipts={hideReadReceipts}
hideReplyButton={hideReplyButton}
showDeveloperTools={showDeveloperTools}
canPinEvent={canPinEvent}
cleanedDisplayName={cleanedDisplayName}
Expand Down Expand Up @@ -431,6 +435,7 @@ export type OptionMenuProps = {
) => void;
onEditId?: (eventId?: string) => void;
hideReadReceipts?: boolean;
hideReplyButton?: boolean;
showDeveloperTools?: boolean;
canPinEvent?: boolean;
cleanedDisplayName?: string;
Expand All @@ -456,6 +461,7 @@ export function OptionMenu({
onReplyClick,
onEditId,
hideReadReceipts,
hideReplyButton,
showDeveloperTools,
canPinEvent,
cleanedDisplayName,
Expand Down Expand Up @@ -606,20 +612,22 @@ export function OptionMenu({
</MenuItem>
)}
{relations && <MessageAllReactionItem room={room} relations={relations} />}
<MenuItem
size="300"
after={menuIcon(ArrowBendUpLeftIcon)}
radii="300"
data-event-id={mEvent.getId()}
onClick={(evt) => {
onReplyClick(evt);
onTotalClose();
}}
>
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
Reply
</Text>
</MenuItem>
{!hideReplyButton && (
<MenuItem
size="300"
after={menuIcon(ArrowBendUpLeftIcon)}
radii="300"
data-event-id={mEvent.getId()}
onClick={(evt) => {
onReplyClick(evt);
onTotalClose();
}}
>
<Text className={css.MessageMenuItemText} as="span" size="T300" truncate>
Reply
</Text>
</MenuItem>
)}
{!isThreadedMessage && (
<MenuItem
size="300"
Expand Down Expand Up @@ -861,6 +869,7 @@ export function MobileOptionsInternal({ options }: { options: OptionMenuProps })
canSendReaction={options.canSendReaction}
isModal
dragOpts={dragOpts}
hideReplyButton={options.hideReplyButton}
/>
</Box>
</Box>
Expand Down
Loading
Loading