= {
- b: MarkType.Bold,
- strong: MarkType.Bold,
- i: MarkType.Italic,
- em: MarkType.Italic,
- u: MarkType.Underline,
- s: MarkType.StrikeThrough,
- del: MarkType.StrikeThrough,
- code: MarkType.Code,
- span: MarkType.Spoiler,
-};
+type ProcessTextCallback = (text: string) => string;
-const elementToTextMark = (node: Element): MarkType | undefined => {
- const markType = markNodeToType[node.name];
- if (!markType) return undefined;
-
- if (markType === MarkType.Spoiler && node.attribs['data-mx-spoiler'] === undefined) {
- return undefined;
- }
- if (
- markType === MarkType.Code &&
- node.parent &&
- 'name' in node.parent &&
- node.parent.name === 'pre'
- ) {
- return undefined;
- }
- return markType;
-};
-
-const parseNodeText = (node: ChildNode): string => {
+const getText = (node: ChildNode): string => {
if (isText(node)) {
return node.data;
}
if (isTag(node)) {
- return node.children.map((child) => parseNodeText(child)).join('');
+ return node.children.map((child) => getText(child)).join('');
}
return '';
};
-const elementToInlineNode = (node: Element): MentionElement | EmoticonElement | undefined => {
+const getInlineNodeMarkType = (node: Element): MarkType | undefined => {
+ if (node.name === 'b' || node.name === 'strong') {
+ return MarkType.Bold;
+ }
+
+ if (node.name === 'i' || node.name === 'em') {
+ return MarkType.Italic;
+ }
+
+ if (node.name === 'u') {
+ return MarkType.Underline;
+ }
+
+ if (node.name === 's' || node.name === 'del') {
+ return MarkType.StrikeThrough;
+ }
+
+ if (node.name === 'code') {
+ if (node.parent && 'name' in node.parent && node.parent.name === 'pre') {
+ return undefined; // Don't apply `Code` mark inside a tag
+ }
+ return MarkType.Code;
+ }
+
+ if (node.name === 'span' && node.attribs['data-mx-spoiler'] !== undefined) {
+ return MarkType.Spoiler;
+ }
+
+ return undefined;
+};
+
+const getInlineMarkElement = (
+ markType: MarkType,
+ node: Element,
+ getChild: (child: ChildNode) => InlineElement[]
+): InlineElement[] => {
+ const children = node.children.flatMap(getChild);
+ const mdSequence = node.attribs['data-md'];
+ if (mdSequence !== undefined) {
+ children.unshift({ text: mdSequence });
+ children.push({ text: mdSequence });
+ return children;
+ }
+ children.forEach((child) => {
+ if (Text.isText(child)) {
+ child[markType] = true;
+ }
+ });
+ return children;
+};
+
+const getInlineNonMarkElement = (node: Element): MentionElement | EmoticonElement | undefined => {
if (node.name === 'img' && node.attribs['data-mx-emoticon'] !== undefined) {
const { src, alt } = node.attribs;
if (!src) return undefined;
@@ -79,13 +106,13 @@ const elementToInlineNode = (node: Element): MentionElement | EmoticonElement |
if (testMatrixTo(href)) {
const userMention = parseMatrixToUser(href);
if (userMention) {
- return createMentionElement(userMention, parseNodeText(node) || userMention, false);
+ return createMentionElement(userMention, getText(node) || userMention, false);
}
const roomMention = parseMatrixToRoom(href);
if (roomMention) {
return createMentionElement(
roomMention.roomIdOrAlias,
- parseNodeText(node) || roomMention.roomIdOrAlias,
+ getText(node) || roomMention.roomIdOrAlias,
false,
undefined,
roomMention.viaServers
@@ -95,7 +122,7 @@ const elementToInlineNode = (node: Element): MentionElement | EmoticonElement |
if (eventMention) {
return createMentionElement(
eventMention.roomIdOrAlias,
- parseNodeText(node) || eventMention.roomIdOrAlias,
+ getText(node) || eventMention.roomIdOrAlias,
false,
eventMention.eventId,
eventMention.viaServers
@@ -106,44 +133,40 @@ const elementToInlineNode = (node: Element): MentionElement | EmoticonElement |
return undefined;
};
-const parseInlineNodes = (node: ChildNode): InlineElement[] => {
+const getInlineElement = (node: ChildNode, processText: ProcessTextCallback): InlineElement[] => {
if (isText(node)) {
- return [{ text: node.data }];
+ return [{ text: processText(node.data) }];
}
+
if (isTag(node)) {
- const markType = elementToTextMark(node);
+ const markType = getInlineNodeMarkType(node);
if (markType) {
- const children = node.children.flatMap(parseInlineNodes);
- if (node.attribs['data-md'] !== undefined) {
- children.unshift({ text: node.attribs['data-md'] });
- children.push({ text: node.attribs['data-md'] });
- } else {
- children.forEach((child) => {
- if (Text.isText(child)) {
- child[markType] = true;
- }
- });
- }
- return children;
+ return getInlineMarkElement(markType, node, (child) => {
+ if (markType === MarkType.Code) return [{ text: getText(child) }];
+ return getInlineElement(child, processText);
+ });
}
- const inlineNode = elementToInlineNode(node);
+ const inlineNode = getInlineNonMarkElement(node);
if (inlineNode) return [inlineNode];
if (node.name === 'a') {
- const children = node.childNodes.flatMap(parseInlineNodes);
+ const children = node.childNodes.flatMap((child) => getInlineElement(child, processText));
children.unshift({ text: '[' });
children.push({ text: `](${node.attribs.href})` });
return children;
}
- return node.childNodes.flatMap(parseInlineNodes);
+ return node.childNodes.flatMap((child) => getInlineElement(child, processText));
}
return [];
};
-const parseBlockquoteNode = (node: Element): BlockQuoteElement[] | ParagraphElement[] => {
+const parseBlockquoteNode = (
+ node: Element,
+ processText: ProcessTextCallback
+): BlockQuoteElement[] | ParagraphElement[] => {
const quoteLines: Array = [];
let lineHolder: InlineElement[] = [];
@@ -156,7 +179,7 @@ const parseBlockquoteNode = (node: Element): BlockQuoteElement[] | ParagraphElem
node.children.forEach((child) => {
if (isText(child)) {
- lineHolder.push({ text: child.data });
+ lineHolder.push({ text: processText(child.data) });
return;
}
if (isTag(child)) {
@@ -168,19 +191,20 @@ const parseBlockquoteNode = (node: Element): BlockQuoteElement[] | ParagraphElem
if (child.name === 'p') {
appendLine();
- quoteLines.push(child.children.flatMap((c) => parseInlineNodes(c)));
+ quoteLines.push(child.children.flatMap((c) => getInlineElement(c, processText)));
return;
}
- parseInlineNodes(child).forEach((inlineNode) => lineHolder.push(inlineNode));
+ lineHolder.push(...getInlineElement(child, processText));
}
});
appendLine();
- if (node.attribs['data-md'] !== undefined) {
+ const mdSequence = node.attribs['data-md'];
+ if (mdSequence !== undefined) {
return quoteLines.map((lineChildren) => ({
type: BlockType.Paragraph,
- children: [{ text: `${node.attribs['data-md']} ` }, ...lineChildren],
+ children: [{ text: `${mdSequence} ` }, ...lineChildren],
}));
}
@@ -195,22 +219,19 @@ const parseBlockquoteNode = (node: Element): BlockQuoteElement[] | ParagraphElem
];
};
const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElement[] => {
- const codeLines = parseNodeText(node).trim().split('\n');
+ const codeLines = getText(node).trim().split('\n');
- if (node.attribs['data-md'] !== undefined) {
- const pLines = codeLines.map((lineText) => ({
+ const mdSequence = node.attribs['data-md'];
+ if (mdSequence !== undefined) {
+ const pLines = codeLines.map((text) => ({
type: BlockType.Paragraph,
- children: [
- {
- text: lineText,
- },
- ],
+ children: [{ text }],
}));
const childCode = node.children[0];
const className =
isTag(childCode) && childCode.tagName === 'code' ? childCode.attribs.class ?? '' : '';
- const prefix = { text: `${node.attribs['data-md']}${className.replace('language-', '')}` };
- const suffix = { text: node.attribs['data-md'] };
+ const prefix = { text: `${mdSequence}${className.replace('language-', '')}` };
+ const suffix = { text: mdSequence };
return [
{ type: BlockType.Paragraph, children: [prefix] },
...pLines,
@@ -221,19 +242,16 @@ const parseCodeBlockNode = (node: Element): CodeBlockElement[] | ParagraphElemen
return [
{
type: BlockType.CodeBlock,
- children: codeLines.map((lineTxt) => ({
+ children: codeLines.map((text) => ({
type: BlockType.CodeLine,
- children: [
- {
- text: lineTxt,
- },
- ],
+ children: [{ text }],
})),
},
];
};
const parseListNode = (
- node: Element
+ node: Element,
+ processText: ProcessTextCallback
): OrderedListElement[] | UnorderedListElement[] | ParagraphElement[] => {
const listLines: Array = [];
let lineHolder: InlineElement[] = [];
@@ -247,7 +265,7 @@ const parseListNode = (
node.children.forEach((child) => {
if (isText(child)) {
- lineHolder.push({ text: child.data });
+ lineHolder.push({ text: processText(child.data) });
return;
}
if (isTag(child)) {
@@ -259,17 +277,18 @@ const parseListNode = (
if (child.name === 'li') {
appendLine();
- listLines.push(child.children.flatMap((c) => parseInlineNodes(c)));
+ listLines.push(child.children.flatMap((c) => getInlineElement(c, processText)));
return;
}
- parseInlineNodes(child).forEach((inlineNode) => lineHolder.push(inlineNode));
+ lineHolder.push(...getInlineElement(child, processText));
}
});
appendLine();
- if (node.attribs['data-md'] !== undefined) {
- const prefix = node.attribs['data-md'] || '-';
+ const mdSequence = node.attribs['data-md'];
+ if (mdSequence !== undefined) {
+ const prefix = mdSequence || '-';
const [starOrHyphen] = prefix.match(/^\*|-$/) ?? [];
return listLines.map((lineChildren) => ({
type: BlockType.Paragraph,
@@ -302,17 +321,21 @@ const parseListNode = (
},
];
};
-const parseHeadingNode = (node: Element): HeadingElement | ParagraphElement => {
- const children = node.children.flatMap((child) => parseInlineNodes(child));
+const parseHeadingNode = (
+ node: Element,
+ processText: ProcessTextCallback
+): HeadingElement | ParagraphElement => {
+ const children = node.children.flatMap((child) => getInlineElement(child, processText));
const headingMatch = node.name.match(/^h([123456])$/);
const [, g1AsLevel] = headingMatch ?? ['h3', '3'];
const level = parseInt(g1AsLevel, 10);
- if (node.attribs['data-md'] !== undefined) {
+ const mdSequence = node.attribs['data-md'];
+ if (mdSequence !== undefined) {
return {
type: BlockType.Paragraph,
- children: [{ text: `${node.attribs['data-md']} ` }, ...children],
+ children: [{ text: `${mdSequence} ` }, ...children],
};
}
@@ -323,7 +346,11 @@ const parseHeadingNode = (node: Element): HeadingElement | ParagraphElement => {
};
};
-export const domToEditorInput = (domNodes: ChildNode[]): Descendant[] => {
+export const domToEditorInput = (
+ domNodes: ChildNode[],
+ processText: ProcessTextCallback,
+ processLineStartText: ProcessTextCallback
+): Descendant[] => {
const children: Descendant[] = [];
let lineHolder: InlineElement[] = [];
@@ -340,7 +367,14 @@ export const domToEditorInput = (domNodes: ChildNode[]): Descendant[] => {
domNodes.forEach((node) => {
if (isText(node)) {
- lineHolder.push({ text: node.data });
+ if (lineHolder.length === 0) {
+ // we are inserting first part of line
+ // it may contain block markdown starting data
+ // that we may need to escape.
+ lineHolder.push({ text: processLineStartText(node.data) });
+ return;
+ }
+ lineHolder.push({ text: processText(node.data) });
return;
}
if (isTag(node)) {
@@ -354,14 +388,14 @@ export const domToEditorInput = (domNodes: ChildNode[]): Descendant[] => {
appendLine();
children.push({
type: BlockType.Paragraph,
- children: node.children.flatMap((child) => parseInlineNodes(child)),
+ children: node.children.flatMap((child) => getInlineElement(child, processText)),
});
return;
}
if (node.name === 'blockquote') {
appendLine();
- children.push(...parseBlockquoteNode(node));
+ children.push(...parseBlockquoteNode(node, processText));
return;
}
if (node.name === 'pre') {
@@ -371,17 +405,17 @@ export const domToEditorInput = (domNodes: ChildNode[]): Descendant[] => {
}
if (node.name === 'ol' || node.name === 'ul') {
appendLine();
- children.push(...parseListNode(node));
+ children.push(...parseListNode(node, processText));
return;
}
if (node.name.match(/^h[123456]$/)) {
appendLine();
- children.push(parseHeadingNode(node));
+ children.push(parseHeadingNode(node, processText));
return;
}
- parseInlineNodes(node).forEach((inlineNode) => lineHolder.push(inlineNode));
+ lineHolder.push(...getInlineElement(node, processText));
}
});
appendLine();
@@ -389,21 +423,31 @@ export const domToEditorInput = (domNodes: ChildNode[]): Descendant[] => {
return children;
};
-export const htmlToEditorInput = (unsafeHtml: string): Descendant[] => {
+export const htmlToEditorInput = (unsafeHtml: string, markdown?: boolean): Descendant[] => {
const sanitizedHtml = sanitizeCustomHtml(unsafeHtml);
+ const processText = (partText: string) => {
+ if (!markdown) return partText;
+ return escapeMarkdownInlineSequences(partText);
+ };
+
const domNodes = parse(sanitizedHtml);
- const editorNodes = domToEditorInput(domNodes);
+ const editorNodes = domToEditorInput(domNodes, processText, (lineStartText: string) => {
+ if (!markdown) return lineStartText;
+ return escapeMarkdownBlockSequences(lineStartText, processText);
+ });
return editorNodes;
};
-export const plainToEditorInput = (text: string): Descendant[] => {
+export const plainToEditorInput = (text: string, markdown?: boolean): Descendant[] => {
const editorNodes: Descendant[] = text.split('\n').map((lineText) => {
const paragraphNode: ParagraphElement = {
type: BlockType.Paragraph,
children: [
{
- text: lineText,
+ text: markdown
+ ? escapeMarkdownBlockSequences(lineText, escapeMarkdownInlineSequences)
+ : lineText,
},
],
};
diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts
index d6136d99..dbdd51f3 100644
--- a/src/app/components/editor/output.ts
+++ b/src/app/components/editor/output.ts
@@ -1,10 +1,17 @@
-import { Descendant, Text } from 'slate';
-
+import { Descendant, Editor, Text } from 'slate';
+import { MatrixClient } from 'matrix-js-sdk';
import { sanitizeText } from '../../utils/sanitize';
import { BlockType } from './types';
import { CustomElement } from './slate';
-import { parseBlockMD, parseInlineMD } from '../../plugins/markdown';
+import {
+ parseBlockMD,
+ parseInlineMD,
+ unescapeMarkdownBlockSequences,
+ unescapeMarkdownInlineSequences,
+} from '../../plugins/markdown';
import { findAndReplace } from '../../utils/findAndReplace';
+import { sanitizeForRegex } from '../../utils/regex';
+import { getCanonicalAliasOrRoomId, isUserId } from '../../utils/matrix';
export type OutputOptions = {
allowTextFormatting?: boolean;
@@ -18,7 +25,7 @@ const textToCustomHtml = (node: Text, opts: OutputOptions): string => {
if (node.bold) string = `${string}`;
if (node.italic) string = `${string}`;
if (node.underline) string = `${string}`;
- if (node.strikeThrough) string = `${string}`;
+ if (node.strikeThrough) string = `${string}`;
if (node.code) string = `${string}`;
if (node.spoiler) string = `${string}`;
}
@@ -101,7 +108,8 @@ export const toMatrixCustomHTML = (
allowBlockMarkdown: false,
})
.replace(/
$/, '\n')
- .replace(/^>/, '>');
+ .replace(/^(\\*)>/, '$1>');
+
markdownLines += line;
if (index === targetNodes.length - 1) {
return parseBlockMD(markdownLines, ignoreHTMLParseInlineMD);
@@ -156,11 +164,14 @@ const elementToPlainText = (node: CustomElement, children: string): string => {
}
};
-export const toPlainText = (node: Descendant | Descendant[]): string => {
- if (Array.isArray(node)) return node.map((n) => toPlainText(n)).join('');
- if (Text.isText(node)) return node.text;
+export const toPlainText = (node: Descendant | Descendant[], isMarkdown: boolean): string => {
+ if (Array.isArray(node)) return node.map((n) => toPlainText(n, isMarkdown)).join('');
+ if (Text.isText(node))
+ return isMarkdown
+ ? unescapeMarkdownBlockSequences(node.text, unescapeMarkdownInlineSequences)
+ : node.text;
- const children = node.children.map((n) => toPlainText(n)).join('');
+ const children = node.children.map((n) => toPlainText(n, isMarkdown)).join('');
return elementToPlainText(node, children);
};
@@ -179,9 +190,42 @@ export const customHtmlEqualsPlainText = (customHtml: string, plain: string): bo
export const trimCustomHtml = (customHtml: string) => customHtml.replace(/
$/g, '').trim();
export const trimCommand = (cmdName: string, str: string) => {
- const cmdRegX = new RegExp(`^(\\s+)?(\\/${cmdName})([^\\S\n]+)?`);
+ const cmdRegX = new RegExp(`^(\\s+)?(\\/${sanitizeForRegex(cmdName)})([^\\S\n]+)?`);
const match = str.match(cmdRegX);
if (!match) return str;
return str.slice(match[0].length);
};
+
+export type MentionsData = {
+ room: boolean;
+ users: Set;
+};
+export const getMentions = (mx: MatrixClient, roomId: string, editor: Editor): MentionsData => {
+ const mentionData: MentionsData = {
+ room: false,
+ users: new Set(),
+ };
+
+ const parseMentions = (node: Descendant): void => {
+ if (Text.isText(node)) return;
+ if (node.type === BlockType.CodeBlock) return;
+
+ if (node.type === BlockType.Mention) {
+ if (node.id === getCanonicalAliasOrRoomId(mx, roomId)) {
+ mentionData.room = true;
+ }
+ if (isUserId(node.id) && node.id !== mx.getUserId()) {
+ mentionData.users.add(node.id);
+ }
+
+ return;
+ }
+
+ node.children.forEach(parseMentions);
+ };
+
+ editor.children.forEach(parseMentions);
+
+ return mentionData;
+};
diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx
index 77e56a91..28735081 100644
--- a/src/app/components/emoji-board/EmojiBoard.tsx
+++ b/src/app/components/emoji-board/EmojiBoard.tsx
@@ -50,6 +50,7 @@ import { addRecentEmoji } from '../../plugins/recent-emoji';
import { mobileOrTablet } from '../../utils/user-agent';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { ImagePack, ImageUsage, PackImageReader } from '../../plugins/custom-emoji';
+import { getEmoticonSearchStr } from '../../plugins/utils';
const RECENT_GROUP_ID = 'recent_group';
const SEARCH_GROUP_ID = 'search_group';
@@ -636,15 +637,8 @@ export const NativeEmojiGroups = memo(
)
);
-const getSearchListItemStr = (item: PackImageReader | IEmoji) => {
- const shortcode = `:${item.shortcode}:`;
- if ('body' in item) {
- return [shortcode, item.body ?? ''];
- }
- return shortcode;
-};
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
- limit: 26,
+ limit: 1000,
matchOptions: {
contain: true,
},
@@ -696,10 +690,12 @@ export function EmojiBoard({
const [result, search, resetSearch] = useAsyncSearch(
searchList,
- getSearchListItemStr,
+ getEmoticonSearchStr,
SEARCH_OPTIONS
);
+ const searchedItems = result?.items.slice(0, 100);
+
const handleOnChange: ChangeEventHandler = useDebounce(
useCallback(
(evt) => {
@@ -920,13 +916,13 @@ export function EmojiBoard({
direction="Column"
gap="200"
>
- {result && (
+ {searchedItems && (
)}
diff --git a/src/app/components/message/FileHeader.tsx b/src/app/components/message/FileHeader.tsx
index 947be90e..0248862d 100644
--- a/src/app/components/message/FileHeader.tsx
+++ b/src/app/components/message/FileHeader.tsx
@@ -1,22 +1,81 @@
-import { Badge, Box, Text, as, toRem } from 'folds';
-import React from 'react';
+import { Badge, Box, Icon, IconButton, Icons, Spinner, Text, as, toRem } from 'folds';
+import React, { ReactNode, useCallback } from 'react';
+import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
+import FileSaver from 'file-saver';
import { mimeTypeToExt } from '../../utils/mimeTypes';
+import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
+import {
+ decryptFile,
+ downloadEncryptedMedia,
+ downloadMedia,
+ mxcUrlToHttp,
+} from '../../utils/matrix';
const badgeStyles = { maxWidth: toRem(100) };
+type FileDownloadButtonProps = {
+ filename: string;
+ url: string;
+ mimeType: string;
+ encInfo?: EncryptedAttachmentInfo;
+};
+export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDownloadButtonProps) {
+ const mx = useMatrixClient();
+ const useAuthentication = useMediaAuthentication();
+
+ const [downloadState, download] = useAsyncCallback(
+ useCallback(async () => {
+ const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
+ const fileContent = encInfo
+ ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
+ : await downloadMedia(mediaUrl);
+
+ const fileURL = URL.createObjectURL(fileContent);
+ FileSaver.saveAs(fileURL, filename);
+ return fileURL;
+ }, [mx, url, useAuthentication, mimeType, encInfo, filename])
+ );
+
+ const downloading = downloadState.status === AsyncStatus.Loading;
+ const hasError = downloadState.status === AsyncStatus.Error;
+ return (
+
+ {downloading ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
export type FileHeaderProps = {
body: string;
mimeType: string;
+ after?: ReactNode;
};
-export const FileHeader = as<'div', FileHeaderProps>(({ body, mimeType, ...props }, ref) => (
+export const FileHeader = as<'div', FileHeaderProps>(({ body, mimeType, after, ...props }, ref) => (
-
-
- {mimeTypeToExt(mimeType)}
+
+
+
+ {mimeTypeToExt(mimeType)}
+
+
+
+
+
+ {body}
-
-
- {body}
-
+
+ {after}
));
diff --git a/src/app/components/message/MsgTypeRenderers.tsx b/src/app/components/message/MsgTypeRenderers.tsx
index 07ad3a74..446cc513 100644
--- a/src/app/components/message/MsgTypeRenderers.tsx
+++ b/src/app/components/message/MsgTypeRenderers.tsx
@@ -28,7 +28,7 @@ import {
import { FALLBACK_MIMETYPE, getBlobSafeMimeType } from '../../utils/mimeTypes';
import { parseGeoUri, scaleYDimension } from '../../utils/common';
import { Attachment, AttachmentBox, AttachmentContent, AttachmentHeader } from './attachment';
-import { FileHeader } from './FileHeader';
+import { FileHeader, FileDownloadButton } from './FileHeader';
export function MBadEncrypted() {
return (
@@ -245,8 +245,24 @@ export function MVideo({ content, renderAsFile, renderVideoContent, outlined }:
const height = scaleYDimension(videoInfo.w || 400, 400, videoInfo.h || 400);
+ const filename = content.filename ?? content.body ?? 'Video';
+
return (
+
+
+ }
+ />
+
;
}
+ const filename = content.filename ?? content.body ?? 'Audio';
return (
-
+
+ }
+ />
diff --git a/src/app/components/text-viewer/TextViewer.css.ts b/src/app/components/text-viewer/TextViewer.css.ts
index 2b79fa64..83ee6058 100644
--- a/src/app/components/text-viewer/TextViewer.css.ts
+++ b/src/app/components/text-viewer/TextViewer.css.ts
@@ -31,8 +31,11 @@ export const TextViewerContent = style([
export const TextViewerPre = style([
DefaultReset,
{
- padding: config.space.S600,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
},
]);
+
+export const TextViewerPrePadding = style({
+ padding: config.space.S600,
+});
diff --git a/src/app/components/text-viewer/TextViewer.tsx b/src/app/components/text-viewer/TextViewer.tsx
index 7829fb35..f39ef953 100644
--- a/src/app/components/text-viewer/TextViewer.tsx
+++ b/src/app/components/text-viewer/TextViewer.tsx
@@ -1,5 +1,5 @@
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
-import React, { Suspense, lazy } from 'react';
+import React, { ComponentProps, HTMLAttributes, Suspense, forwardRef, lazy } from 'react';
import classNames from 'classnames';
import { Box, Chip, Header, Icon, IconButton, Icons, Scroll, Text, as } from 'folds';
import { ErrorBoundary } from 'react-error-boundary';
@@ -8,6 +8,29 @@ import { copyToClipboard } from '../../utils/dom';
const ReactPrism = lazy(() => import('../../plugins/react-prism/ReactPrism'));
+type TextViewerContentProps = {
+ text: string;
+ langName: string;
+ size?: ComponentProps['size'];
+} & HTMLAttributes;
+export const TextViewerContent = forwardRef(
+ ({ text, langName, size, className, ...props }, ref) => (
+
+ {text}}>
+ {text}}>
+ {(codeRef) => {text}}
+
+
+
+ )
+);
+
export type TextViewerProps = {
name: string;
text: string;
@@ -43,6 +66,7 @@ export const TextViewer = as<'div', TextViewerProps>(
+
(
alignItems="Center"
>
-
- {text}}>
- {text}}>
- {(codeRef) => {text}}
-
-
-
+
diff --git a/src/app/components/upload-card/UploadCardRenderer.tsx b/src/app/components/upload-card/UploadCardRenderer.tsx
index bad13561..4383e204 100644
--- a/src/app/components/upload-card/UploadCardRenderer.tsx
+++ b/src/app/components/upload-card/UploadCardRenderer.tsx
@@ -1,5 +1,5 @@
-import React, { useCallback, useEffect } from 'react';
-import { Chip, Icon, IconButton, Icons, Text, Tooltip, TooltipProvider, color } from 'folds';
+import React, { useEffect } from 'react';
+import { Box, Chip, Icon, IconButton, Icons, Text, color, config, toRem } from 'folds';
import { UploadCard, UploadCardError, UploadCardProgress } from './UploadCard';
import { UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
import { useMatrixClient } from '../../hooks/useMatrixClient';
@@ -10,11 +10,60 @@ import {
TUploadItem,
TUploadMetadata,
} from '../../state/room/roomInputDrafts';
+import { useObjectURL } from '../../hooks/useObjectURL';
+
+type ImagePreviewProps = { fileItem: TUploadItem; onSpoiler: (marked: boolean) => void };
+function ImagePreview({ fileItem, onSpoiler }: ImagePreviewProps) {
+ const { originalFile, metadata } = fileItem;
+ const fileUrl = useObjectURL(originalFile);
+
+ return fileUrl ? (
+
+
+
+ }
+ onClick={() => onSpoiler(!metadata.markedAsSpoiler)}
+ >
+ Spoiler
+
+
+
+ ) : null;
+}
type UploadCardRendererProps = {
isEncrypted?: boolean;
fileItem: TUploadItem;
- setMetadata: (metadata: TUploadMetadata) => void;
+ setMetadata: (fileItem: TUploadItem, metadata: TUploadMetadata) => void;
onRemove: (file: TUploadContent) => void;
onComplete?: (upload: UploadSuccess) => void;
};
@@ -33,9 +82,9 @@ export function UploadCardRenderer({
if (upload.status === UploadStatus.Idle) startUpload();
- const toggleSpoiler = useCallback(() => {
- setMetadata({ ...metadata, markedAsSpoiler: !metadata.markedAsSpoiler });
- }, [setMetadata, metadata]);
+ const handleSpoiler = (marked: boolean) => {
+ setMetadata(fileItem, { ...metadata, markedAsSpoiler: marked });
+ };
const removeUpload = () => {
cancelUpload();
@@ -66,31 +115,6 @@ export function UploadCardRenderer({
Retry
)}
- {(file.type.startsWith('image') || file.type.startsWith('video')) && (
-
- Mark as Spoiler
-
- }
- position="Top"
- align="Center"
- >
- {(triggerRef) => (
-
-
-
- )}
-
- )}
+ {fileItem.originalFile.type.startsWith('image') && (
+
+ )}
{upload.status === UploadStatus.Idle && (
)}
diff --git a/src/app/features/lobby/HierarchyItemMenu.tsx b/src/app/features/lobby/HierarchyItemMenu.tsx
index 30a4f632..d1a7ec6b 100644
--- a/src/app/features/lobby/HierarchyItemMenu.tsx
+++ b/src/app/features/lobby/HierarchyItemMenu.tsx
@@ -155,7 +155,7 @@ function SettingsMenuItem({
disabled?: boolean;
}) {
const handleSettings = () => {
- if (item.space) {
+ if ('space' in item) {
openSpaceSettings(item.roomId);
} else {
toggleRoomSettings(item.roomId);
@@ -271,7 +271,7 @@ export function HierarchyItemMenu({
{promptLeave &&
- (item.space ? (
+ ('space' in item ? (
>(() => new Map());
+
useElementSizeObserver(
useCallback(() => heroSectionRef.current, []),
useCallback((w, height) => setHeroSectionHeight(height), [])
@@ -107,19 +113,20 @@ export function Lobby() {
);
const [draggingItem, setDraggingItem] = useState();
- const flattenHierarchy = useSpaceHierarchy(
+ const hierarchy = useSpaceHierarchy(
space.roomId,
spaceRooms,
getRoom,
useCallback(
(childId) =>
- closedCategories.has(makeLobbyCategoryId(space.roomId, childId)) || !!draggingItem?.space,
+ closedCategories.has(makeLobbyCategoryId(space.roomId, childId)) ||
+ (draggingItem ? 'space' in draggingItem : false),
[closedCategories, space.roomId, draggingItem]
)
);
const virtualizer = useVirtualizer({
- count: flattenHierarchy.length,
+ count: hierarchy.length,
getScrollElement: () => scrollRef.current,
estimateSize: () => 1,
overscan: 2,
@@ -129,8 +136,17 @@ export function Lobby() {
const roomsPowerLevels = useRoomsPowerLevels(
useMemo(
- () => flattenHierarchy.map((i) => mx.getRoom(i.roomId)).filter((r) => !!r) as Room[],
- [mx, flattenHierarchy]
+ () =>
+ hierarchy
+ .flatMap((i) => {
+ const childRooms = Array.isArray(i.rooms)
+ ? i.rooms.map((r) => mx.getRoom(r.roomId))
+ : [];
+
+ return [mx.getRoom(i.space.roomId), ...childRooms];
+ })
+ .filter((r) => !!r) as Room[],
+ [mx, hierarchy]
)
);
@@ -142,8 +158,8 @@ export function Lobby() {
return false;
}
- if (item.space) {
- if (!container.item.space) return false;
+ if ('space' in item) {
+ if (!('space' in container.item)) return false;
const containerSpaceId = space.roomId;
if (
@@ -156,9 +172,8 @@ export function Lobby() {
return true;
}
- const containerSpaceId = container.item.space
- ? container.item.roomId
- : container.item.parentId;
+ const containerSpaceId =
+ 'space' in container.item ? container.item.roomId : container.item.parentId;
const dropOutsideSpace = item.parentId !== containerSpaceId;
@@ -192,22 +207,22 @@ export function Lobby() {
);
const reorderSpace = useCallback(
- (item: HierarchyItem, containerItem: HierarchyItem) => {
+ (item: HierarchyItemSpace, containerItem: HierarchyItem) => {
if (!item.parentId) return;
- const childItems = flattenHierarchy
- .filter((i) => i.parentId && i.space)
+ const itemSpaces: HierarchyItemSpace[] = hierarchy
+ .map((i) => i.space)
.filter((i) => i.roomId !== item.roomId);
- const beforeIndex = childItems.findIndex((i) => i.roomId === containerItem.roomId);
+ const beforeIndex = itemSpaces.findIndex((i) => i.roomId === containerItem.roomId);
const insertIndex = beforeIndex + 1;
- childItems.splice(insertIndex, 0, {
+ itemSpaces.splice(insertIndex, 0, {
...item,
content: { ...item.content, order: undefined },
});
- const currentOrders = childItems.map((i) => {
+ const currentOrders = itemSpaces.map((i) => {
if (typeof i.content.order === 'string' && lex.has(i.content.order)) {
return i.content.order;
}
@@ -217,21 +232,21 @@ export function Lobby() {
const newOrders = orderKeys(lex, currentOrders);
newOrders?.forEach((orderKey, index) => {
- const itm = childItems[index];
+ const itm = itemSpaces[index];
if (!itm || !itm.parentId) return;
const parentPL = roomsPowerLevels.get(itm.parentId);
const canEdit = parentPL && canEditSpaceChild(parentPL);
if (canEdit && orderKey !== currentOrders[index]) {
mx.sendStateEvent(
itm.parentId,
- StateEvent.SpaceChild,
+ StateEvent.SpaceChild as any,
{ ...itm.content, order: orderKey },
itm.roomId
);
}
});
},
- [mx, flattenHierarchy, lex, roomsPowerLevels, canEditSpaceChild]
+ [mx, hierarchy, lex, roomsPowerLevels, canEditSpaceChild]
);
const reorderRoom = useCallback(
@@ -240,13 +255,12 @@ export function Lobby() {
if (!item.parentId) {
return;
}
- const containerParentId: string = containerItem.space
- ? containerItem.roomId
- : containerItem.parentId;
+ const containerParentId: string =
+ 'space' in containerItem ? containerItem.roomId : containerItem.parentId;
const itemContent = item.content;
if (item.parentId !== containerParentId) {
- mx.sendStateEvent(item.parentId, StateEvent.SpaceChild, {}, item.roomId);
+ mx.sendStateEvent(item.parentId, StateEvent.SpaceChild as any, {}, item.roomId);
}
if (
@@ -265,28 +279,29 @@ export function Lobby() {
const allow =
joinRuleContent.allow?.filter((allowRule) => allowRule.room_id !== item.parentId) ?? [];
allow.push({ type: RestrictedAllowType.RoomMembership, room_id: containerParentId });
- mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules, {
+ mx.sendStateEvent(itemRoom.roomId, StateEvent.RoomJoinRules as any, {
...joinRuleContent,
allow,
});
}
}
- const childItems = flattenHierarchy
- .filter((i) => i.parentId === containerParentId && !i.space)
- .filter((i) => i.roomId !== item.roomId);
+ const itemSpaces = Array.from(
+ hierarchy?.find((i) => i.space.roomId === containerParentId)?.rooms ?? []
+ );
- const beforeItem: HierarchyItem | undefined = containerItem.space ? undefined : containerItem;
- const beforeIndex = childItems.findIndex((i) => i.roomId === beforeItem?.roomId);
+ const beforeItem: HierarchyItem | undefined =
+ 'space' in containerItem ? undefined : containerItem;
+ const beforeIndex = itemSpaces.findIndex((i) => i.roomId === beforeItem?.roomId);
const insertIndex = beforeIndex + 1;
- childItems.splice(insertIndex, 0, {
+ itemSpaces.splice(insertIndex, 0, {
...item,
parentId: containerParentId,
content: { ...itemContent, order: undefined },
});
- const currentOrders = childItems.map((i) => {
+ const currentOrders = itemSpaces.map((i) => {
if (typeof i.content.order === 'string' && lex.has(i.content.order)) {
return i.content.order;
}
@@ -296,18 +311,18 @@ export function Lobby() {
const newOrders = orderKeys(lex, currentOrders);
newOrders?.forEach((orderKey, index) => {
- const itm = childItems[index];
+ const itm = itemSpaces[index];
if (itm && orderKey !== currentOrders[index]) {
mx.sendStateEvent(
containerParentId,
- StateEvent.SpaceChild,
+ StateEvent.SpaceChild as any,
{ ...itm.content, order: orderKey },
itm.roomId
);
}
});
},
- [mx, flattenHierarchy, lex]
+ [mx, hierarchy, lex]
);
useDnDMonitor(
@@ -318,7 +333,7 @@ export function Lobby() {
if (!canDrop(item, container)) {
return;
}
- if (item.space) {
+ if ('space' in item) {
reorderSpace(item, container.item);
} else {
reorderRoom(item, container.item);
@@ -328,8 +343,16 @@ export function Lobby() {
)
);
- const addSpaceRoom = useCallback(
- (roomId: string) => setSpaceRooms({ type: 'PUT', roomId }),
+ const handleSpacesFound = useCallback(
+ (sItems: IHierarchyRoom[]) => {
+ setSpaceRooms({ type: 'PUT', roomIds: sItems.map((i) => i.room_id) });
+ setSpacesItem((current) => {
+ const newItems = produce(current, (draft) => {
+ sItems.forEach((item) => draft.set(item.room_id, item));
+ });
+ return current.size === newItems.size ? current : newItems;
+ });
+ },
[setSpaceRooms]
);
@@ -394,121 +417,44 @@ export function Lobby() {
{vItems.map((vItem) => {
- const item = flattenHierarchy[vItem.index];
+ const item = hierarchy[vItem.index];
if (!item) return null;
- const itemPowerLevel = roomsPowerLevels.get(item.roomId) ?? {};
- const userPLInItem = powerLevelAPI.getPowerLevel(
- itemPowerLevel,
- mx.getUserId() ?? undefined
- );
- const canInvite = powerLevelAPI.canDoAction(
- itemPowerLevel,
- 'invite',
- userPLInItem
- );
- const isJoined = allJoinedRooms.has(item.roomId);
+ const nextSpaceId = hierarchy[vItem.index + 1]?.space.roomId;
- const nextRoomId: string | undefined =
- flattenHierarchy[vItem.index + 1]?.roomId;
+ const categoryId = makeLobbyCategoryId(space.roomId, item.space.roomId);
- const dragging =
- draggingItem?.roomId === item.roomId &&
- draggingItem.parentId === item.parentId;
-
- if (item.space) {
- const categoryId = makeLobbyCategoryId(space.roomId, item.roomId);
- const { parentId } = item;
- const parentPowerLevels = parentId
- ? roomsPowerLevels.get(parentId) ?? {}
- : undefined;
-
- return (
-
-
- )
- }
- before={item.parentId ? undefined : undefined}
- after={
-
- }
- onDragging={setDraggingItem}
- data-dragging={dragging}
- />
-
- );
- }
-
- const parentPowerLevels = roomsPowerLevels.get(item.parentId) ?? {};
- const prevItem: HierarchyItem | undefined = flattenHierarchy[vItem.index - 1];
- const nextItem: HierarchyItem | undefined = flattenHierarchy[vItem.index + 1];
return (
-
+
- }
- data-dragging={dragging}
+ handleClose={handleCategoryClick}
+ draggingItem={draggingItem}
onDragging={setDraggingItem}
+ canDrop={canDrop}
+ nextSpaceId={nextSpaceId}
+ getRoom={getRoom}
+ pinned={sidebarSpaces.has(item.space.roomId)}
+ togglePinToSidebar={togglePinToSidebar}
+ onSpacesFound={handleSpacesFound}
+ onOpenRoom={handleOpenRoom}
/>
);
diff --git a/src/app/features/lobby/RoomItem.tsx b/src/app/features/lobby/RoomItem.tsx
index f8db3991..994cda05 100644
--- a/src/app/features/lobby/RoomItem.tsx
+++ b/src/app/features/lobby/RoomItem.tsx
@@ -1,4 +1,4 @@
-import React, { MouseEventHandler, ReactNode, useCallback, useEffect, useRef } from 'react';
+import React, { MouseEventHandler, ReactNode, useCallback, useRef } from 'react';
import {
Avatar,
Badge,
@@ -20,23 +20,20 @@ import {
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { JoinRule, MatrixError, Room } from 'matrix-js-sdk';
+import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
import { SequenceCard } from '../../components/sequence-card';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
import { millify } from '../../plugins/millify';
-import {
- HierarchyRoomSummaryLoader,
- LocalRoomSummaryLoader,
-} from '../../components/RoomSummaryLoader';
+import { LocalRoomSummaryLoader } from '../../components/RoomSummaryLoader';
import { UseStateProvider } from '../../components/UseStateProvider';
import { RoomTopicViewer } from '../../components/room-topic-viewer';
import { onEnterOrSpace, stopPropagation } from '../../utils/keyboard';
-import { Membership, RoomType } from '../../../types/matrix/room';
+import { Membership } from '../../../types/matrix/room';
import * as css from './RoomItem.css';
import * as styleCss from './style.css';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
-import { ErrorCode } from '../../cs-errorcode';
import { getDirectRoomAvatarUrl, getRoomAvatarUrl } from '../../utils/room';
import { ItemDraggableTarget, useDraggableItem } from './DnD';
import { mxcUrlToHttp } from '../../utils/matrix';
@@ -125,13 +122,11 @@ function RoomProfileLoading() {
type RoomProfileErrorProps = {
roomId: string;
- error: Error;
+ inaccessibleRoom: boolean;
suggested?: boolean;
via?: string[];
};
-function RoomProfileError({ roomId, suggested, error, via }: RoomProfileErrorProps) {
- const privateRoom = error.name === ErrorCode.M_FORBIDDEN;
-
+function RoomProfileError({ roomId, suggested, inaccessibleRoom, via }: RoomProfileErrorProps) {
return (
@@ -142,7 +137,7 @@ function RoomProfileError({ roomId, suggested, error, via }: RoomProfileErrorPro
renderFallback={() => (
)}
@@ -162,25 +157,18 @@ function RoomProfileError({ roomId, suggested, error, via }: RoomProfileErrorPro
)}
- {privateRoom && (
- <>
-
- Private Room
-
-
- >
+ {inaccessibleRoom ? (
+
+ Inaccessible
+
+ ) : (
+
+ {roomId}
+
)}
-
- {roomId}
-
- {!privateRoom && }
+ {!inaccessibleRoom && }
);
}
@@ -288,23 +276,11 @@ function RoomProfile({
);
}
-function CallbackOnFoundSpace({
- roomId,
- onSpaceFound,
-}: {
- roomId: string;
- onSpaceFound: (roomId: string) => void;
-}) {
- useEffect(() => {
- onSpaceFound(roomId);
- }, [roomId, onSpaceFound]);
-
- return null;
-}
-
type RoomItemCardProps = {
item: HierarchyItem;
- onSpaceFound: (roomId: string) => void;
+ loading: boolean;
+ error: Error | null;
+ summary: IHierarchyRoom | undefined;
dm?: boolean;
firstChild?: boolean;
lastChild?: boolean;
@@ -320,10 +296,10 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
(
{
item,
- onSpaceFound,
+ loading,
+ error,
+ summary,
dm,
- firstChild,
- lastChild,
onOpen,
options,
before,
@@ -348,8 +324,6 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
return (
(
name={localSummary.name}
topic={localSummary.topic}
avatarUrl={
- dm ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication) : getRoomAvatarUrl(mx, room, 96, useAuthentication)
+ dm
+ ? getDirectRoomAvatarUrl(mx, room, 96, useAuthentication)
+ : getRoomAvatarUrl(mx, room, 96, useAuthentication)
}
memberCount={localSummary.memberCount}
suggested={content.suggested}
@@ -395,46 +371,46 @@ export const RoomItemCard = as<'div', RoomItemCardProps>(
)}
) : (
-
- {(summaryState) => (
- <>
- {summaryState.status === AsyncStatus.Loading && }
- {summaryState.status === AsyncStatus.Error && (
-
- )}
- {summaryState.status === AsyncStatus.Success && (
- <>
- {summaryState.data.room_type === RoomType.Space && (
-
- )}
-
+ {!summary &&
+ (error ? (
+
+ ) : (
+ <>
+ {loading && }
+ {!loading && (
+ }
+ via={content.via}
/>
- >
- )}
- >
+ )}
+ >
+ ))}
+ {summary && (
+ }
+ />
)}
-
+ >
)}
{options}
diff --git a/src/app/features/lobby/SpaceHierarchy.tsx b/src/app/features/lobby/SpaceHierarchy.tsx
new file mode 100644
index 00000000..2c43282f
--- /dev/null
+++ b/src/app/features/lobby/SpaceHierarchy.tsx
@@ -0,0 +1,225 @@
+import React, { forwardRef, MouseEventHandler, useEffect, useMemo } from 'react';
+import { MatrixError, Room } from 'matrix-js-sdk';
+import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
+import { Box, config, Text } from 'folds';
+import {
+ HierarchyItem,
+ HierarchyItemRoom,
+ HierarchyItemSpace,
+ useFetchSpaceHierarchyLevel,
+} from '../../hooks/useSpaceHierarchy';
+import { IPowerLevels, powerLevelAPI } from '../../hooks/usePowerLevels';
+import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { SpaceItemCard } from './SpaceItem';
+import { AfterItemDropTarget, CanDropCallback } from './DnD';
+import { HierarchyItemMenu } from './HierarchyItemMenu';
+import { RoomItemCard } from './RoomItem';
+import { RoomType } from '../../../types/matrix/room';
+import { SequenceCard } from '../../components/sequence-card';
+
+type SpaceHierarchyProps = {
+ summary: IHierarchyRoom | undefined;
+ spaceItem: HierarchyItemSpace;
+ roomItems?: HierarchyItemRoom[];
+ allJoinedRooms: Set;
+ mDirects: Set;
+ roomsPowerLevels: Map;
+ canEditSpaceChild: (powerLevels: IPowerLevels) => boolean;
+ categoryId: string;
+ closed: boolean;
+ handleClose: MouseEventHandler;
+ draggingItem?: HierarchyItem;
+ onDragging: (item?: HierarchyItem) => void;
+ canDrop: CanDropCallback;
+ nextSpaceId?: string;
+ getRoom: (roomId: string) => Room | undefined;
+ pinned: boolean;
+ togglePinToSidebar: (roomId: string) => void;
+ onSpacesFound: (spaceItems: IHierarchyRoom[]) => void;
+ onOpenRoom: MouseEventHandler;
+};
+export const SpaceHierarchy = forwardRef(
+ (
+ {
+ summary,
+ spaceItem,
+ roomItems,
+ allJoinedRooms,
+ mDirects,
+ roomsPowerLevels,
+ canEditSpaceChild,
+ categoryId,
+ closed,
+ handleClose,
+ draggingItem,
+ onDragging,
+ canDrop,
+ nextSpaceId,
+ getRoom,
+ pinned,
+ togglePinToSidebar,
+ onOpenRoom,
+ onSpacesFound,
+ },
+ ref
+ ) => {
+ const mx = useMatrixClient();
+
+ const { fetching, error, rooms } = useFetchSpaceHierarchyLevel(spaceItem.roomId, true);
+
+ const subspaces = useMemo(() => {
+ const s: Map = new Map();
+ rooms.forEach((r) => {
+ if (r.room_type === RoomType.Space) {
+ s.set(r.room_id, r);
+ }
+ });
+ return s;
+ }, [rooms]);
+
+ const spacePowerLevels = roomsPowerLevels.get(spaceItem.roomId) ?? {};
+ const userPLInSpace = powerLevelAPI.getPowerLevel(
+ spacePowerLevels,
+ mx.getUserId() ?? undefined
+ );
+ const canInviteInSpace = powerLevelAPI.canDoAction(spacePowerLevels, 'invite', userPLInSpace);
+
+ const draggingSpace =
+ draggingItem?.roomId === spaceItem.roomId && draggingItem.parentId === spaceItem.parentId;
+
+ const { parentId } = spaceItem;
+ const parentPowerLevels = parentId ? roomsPowerLevels.get(parentId) ?? {} : undefined;
+
+ useEffect(() => {
+ onSpacesFound(Array.from(subspaces.values()));
+ }, [subspaces, onSpacesFound]);
+
+ let childItems = roomItems?.filter((i) => !subspaces.has(i.roomId));
+ if (!canEditSpaceChild(spacePowerLevels)) {
+ // hide unknown rooms for normal user
+ childItems = childItems?.filter((i) => {
+ const forbidden = error instanceof MatrixError ? error.errcode === 'M_FORBIDDEN' : false;
+ const inaccessibleRoom = !rooms.get(i.roomId) && !fetching && (error ? forbidden : true);
+ return !inaccessibleRoom;
+ });
+ }
+
+ return (
+
+
+ )
+ }
+ after={
+
+ }
+ onDragging={onDragging}
+ data-dragging={draggingSpace}
+ />
+ {childItems && childItems.length > 0 ? (
+
+ {childItems.map((roomItem, index) => {
+ const roomSummary = rooms.get(roomItem.roomId);
+
+ const roomPowerLevels = roomsPowerLevels.get(roomItem.roomId) ?? {};
+ const userPLInRoom = powerLevelAPI.getPowerLevel(
+ roomPowerLevels,
+ mx.getUserId() ?? undefined
+ );
+ const canInviteInRoom = powerLevelAPI.canDoAction(
+ roomPowerLevels,
+ 'invite',
+ userPLInRoom
+ );
+
+ const lastItem = index === childItems.length;
+ const nextRoomId = lastItem ? nextSpaceId : childItems[index + 1]?.roomId;
+
+ const roomDragging =
+ draggingItem?.roomId === roomItem.roomId &&
+ draggingItem.parentId === roomItem.parentId;
+
+ return (
+
+ }
+ after={
+
+ }
+ data-dragging={roomDragging}
+ onDragging={onDragging}
+ />
+ );
+ })}
+
+ ) : (
+ childItems && (
+
+
+
+ No Rooms
+
+
+ This space does not contains rooms yet.
+
+
+
+ )
+ )}
+
+ );
+ }
+);
diff --git a/src/app/features/lobby/SpaceItem.tsx b/src/app/features/lobby/SpaceItem.tsx
index deaf9ba5..0a4d9de5 100644
--- a/src/app/features/lobby/SpaceItem.tsx
+++ b/src/app/features/lobby/SpaceItem.tsx
@@ -19,19 +19,16 @@ import {
import FocusTrap from 'focus-trap-react';
import classNames from 'classnames';
import { MatrixError, Room } from 'matrix-js-sdk';
+import { IHierarchyRoom } from 'matrix-js-sdk/lib/@types/spaces';
import { HierarchyItem } from '../../hooks/useSpaceHierarchy';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { RoomAvatar } from '../../components/room-avatar';
import { nameInitials } from '../../utils/common';
-import {
- HierarchyRoomSummaryLoader,
- LocalRoomSummaryLoader,
-} from '../../components/RoomSummaryLoader';
+import { LocalRoomSummaryLoader } from '../../components/RoomSummaryLoader';
import { getRoomAvatarUrl } from '../../utils/room';
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
import * as css from './SpaceItem.css';
import * as styleCss from './style.css';
-import { ErrorCode } from '../../cs-errorcode';
import { useDraggableItem } from './DnD';
import { openCreateRoom, openSpaceAddExisting } from '../../../client/action/navigation';
import { stopPropagation } from '../../utils/keyboard';
@@ -53,18 +50,11 @@ function SpaceProfileLoading() {
);
}
-type UnknownPrivateSpaceProfileProps = {
+type InaccessibleSpaceProfileProps = {
roomId: string;
- name?: string;
- avatarUrl?: string;
suggested?: boolean;
};
-function UnknownPrivateSpaceProfile({
- roomId,
- name,
- avatarUrl,
- suggested,
-}: UnknownPrivateSpaceProfileProps) {
+function InaccessibleSpaceProfile({ roomId, suggested }: InaccessibleSpaceProfileProps) {
return (
(
- {nameInitials(name)}
+ U
)}
/>
@@ -88,11 +76,11 @@ function UnknownPrivateSpaceProfile({
>
- {name || 'Unknown'}
+ Unknown
- Private Space
+ Inaccessible
{suggested && (
@@ -104,20 +92,20 @@ function UnknownPrivateSpaceProfile({
);
}
-type UnknownSpaceProfileProps = {
+type UnjoinedSpaceProfileProps = {
roomId: string;
via?: string[];
name?: string;
avatarUrl?: string;
suggested?: boolean;
};
-function UnknownSpaceProfile({
+function UnjoinedSpaceProfile({
roomId,
via,
name,
avatarUrl,
suggested,
-}: UnknownSpaceProfileProps) {
+}: UnjoinedSpaceProfileProps) {
const mx = useMatrixClient();
const [joinState, join] = useAsyncCallback(
@@ -376,6 +364,8 @@ function AddSpaceButton({ item }: { item: HierarchyItem }) {
}
type SpaceItemCardProps = {
+ summary: IHierarchyRoom | undefined;
+ loading?: boolean;
item: HierarchyItem;
joined?: boolean;
categoryId: string;
@@ -393,6 +383,8 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
(
{
className,
+ summary,
+ loading,
joined,
closed,
categoryId,
@@ -451,37 +443,31 @@ export const SpaceItemCard = as<'div', SpaceItemCardProps>(
}
) : (
-
- {(summaryState) => (
- <>
- {summaryState.status === AsyncStatus.Loading && }
- {summaryState.status === AsyncStatus.Error &&
- (summaryState.error.name === ErrorCode.M_FORBIDDEN ? (
-
- ) : (
-
- ))}
- {summaryState.status === AsyncStatus.Success && (
-
- )}
- >
+ <>
+ {!summary &&
+ (loading ? (
+
+ ) : (
+
+ ))}
+ {summary && (
+
)}
-
+ >
)}
{canEditChild && (
diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx
index ef59bf98..ffff0f45 100644
--- a/src/app/features/room-nav/RoomNavItem.tsx
+++ b/src/app/features/room-nav/RoomNavItem.tsx
@@ -39,6 +39,8 @@ import { getMatrixToRoom } from '../../plugins/matrix-to';
import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix';
import { getViaServers } from '../../plugins/via-servers';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { useSetting } from '../../state/hooks/settings';
+import { settingsAtom } from '../../state/settings';
type RoomNavItemMenuProps = {
room: Room;
@@ -47,13 +49,14 @@ type RoomNavItemMenuProps = {
const RoomNavItemMenu = forwardRef(
({ room, requestClose }, ref) => {
const mx = useMatrixClient();
+ const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevels(room);
const { getPowerLevel, canDoAction } = usePowerLevelsAPI(powerLevels);
const canInvite = canDoAction('invite', getPowerLevel(mx.getUserId() ?? ''));
const handleMarkAsRead = () => {
- markAsRead(mx, room.roomId);
+ markAsRead(mx, room.roomId, hideActivity);
requestClose();
};
diff --git a/src/app/features/room/MembersDrawer.tsx b/src/app/features/room/MembersDrawer.tsx
index a4305e45..df8008ca 100644
--- a/src/app/features/room/MembersDrawer.tsx
+++ b/src/app/features/room/MembersDrawer.tsx
@@ -156,7 +156,7 @@ export type MembersFilterOptions = {
};
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
- limit: 100,
+ limit: 1000,
matchOptions: {
contain: true,
},
@@ -428,8 +428,9 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
}}
after={}
>
- {`${result.items.length || 'No'} ${result.items.length === 1 ? 'Result' : 'Results'
- }`}
+ {`${result.items.length || 'No'} ${
+ result.items.length === 1 ? 'Result' : 'Results'
+ }`}
)
}
@@ -485,15 +486,17 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
const member = tagOrMember;
const name = getName(member);
const avatarMxcUrl = member.getMxcAvatarUrl();
- const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp(
- avatarMxcUrl,
- 100,
- 100,
- 'crop',
- undefined,
- false,
- useAuthentication
- ) : undefined;
+ const avatarUrl = avatarMxcUrl
+ ? mx.mxcUrlToHttp(
+ avatarMxcUrl,
+ 100,
+ 100,
+ 'crop',
+ undefined,
+ false,
+ useAuthentication
+ )
+ : undefined;
return (