Refactor timeline (#1346)
* fix intersection & resize observer * add binary search util * add scroll info util * add virtual paginator hook - WIP * render timeline using paginator hook * add continuous pagination to fill timeline * add doc comments in virtual paginator hook * add scroll to element func in virtual paginator * extract timeline pagination login into hook * add sliding name for timeline messages - testing * scroll with live event * change message rending style * make message timestamp smaller * remove unused imports * add random number between util * add compact message component * add sanitize html types * fix sending alias in room mention * get room member display name util * add get room with canonical alias util * add sanitize html util * render custom html with new styles * fix linkifying link text * add reaction component * display message reactions in timeline * Change mention color * show edited message * add event sent by function factory * add functions to get emoji shortcode * add component for reaction msg * add tooltip for who has reacted * add message layouts & placeholder * fix reaction size * fix dark theme colors * add code highlight with prismjs * add options to configure spacing in msgs * render message reply * fix trim reply from body regex * fix crash when loading reply * fix reply hover style * decrypt event on timeline paginate * update custom html code style * remove console logs * fix virtual paginator scroll to func * fix virtual paginator scroll to types * add stop scroll for in view item options * fix virtual paginator out of range scroll to index * scroll to and highlight reply on click * fix reply hover style * make message avatar clickable * fix scrollTo issue in virtual paginator * load reply from fetch * import virtual paginator restore scroll * load timeline for specific event * Fix back pagination recalibration * fix reply min height * revert code block colors to secondary * stop sanitizing text in code block * add decrypt file util * add image media component * update folds * fix code block font style * add msg event type * add scale dimension util * strict msg layout type * add image renderer component * add message content fallback components * add message matrix event renderer components * render matrix event using hooks * add attachment component * add attachment content types * handle error when rendering image in timeline * add video component * render video * include blurhash in thumbnails * generate thumbnails for image message * fix reactToDom spoiler opts * add hooks for HTMLMediaElement * render audio file in timeline * add msg image content component * fix image content props * add video content component * render new image/video component in timeline * remove console.log * convert seconds to milliseconds in video info * add load thumbnail prop to video content component * add file saver types * add file header component * add file content component * render file in timeline * add media control component * render audio message in room timeline * remove moved components * safely load message reply * add media loading hook * update media control layout * add loading indication in audio component * fill audio play icon when playing audio * fix media expanding * add image viewer - WIP * add pan and zoom control to image viewer * add text based file viewer * add pdf viewer * add error handling in pdf viewer * add download btn to pdf viewer * fix file button spinner fill * fix file opens on re-render * add range slider in audio content player * render location in timeline * update folds * display membership event in timeline * make reactions toggle * render sticker messages in timeline * render room name, topic, avatar change and event * fix typos * update render state event type style * add room intro in start of timeline * add power levels context * fix wrong param passing in RoomView * fix sending typing notification in wrong room Slate onChange callback was not updating with react re-renders. * send typing status on key up * add typing indicator component * add typing member atom * display typing status in member drawer * add room view typing member component * display typing members in room view * remove old roomTimeline uses * add event readers hook * add latest event hook * display following members in room view * fetch event instead of event context for reply * fix typo in virtual paginator hook * add scroll to latest btn in timeline * change scroll to latest chip variant * destructure paginator object to improve perf * restore forward dir scroll in virtual paginator * run scroll to bottom in layout effect * display unread message indicator in timeline * make component for room timeline float * add timeline divider component * add day divider and format message time * apply message spacing to dividers * format date in room intro * send read receipt on message arrive * add event readers component * add reply, read receipt, source delete opt * bug fixes * update timeline on delete & show reason * fix empty reaction container style * show msg selection effect on msg option open * add report message options * add options to send quick reactions * add emoji board in message options * add reaction viewer * fix styles * show view reaction in msg options menu * fix spacing between two msg by same person * add option menu in other rendered event * handle m.room.encrypted messages * fix italic reply text overflow cut * handle encrypted sticker messages * remove console log * prevent message context menu with alt key pressed * make mentions clickable in messages * add options to show and hidden events in timeline * add option to disable media autoload * remove old emojiboard opener * add options to use system emoji * refresh timeline on reset * fix stuck typing member in member drawer
This commit is contained in:
parent
fcd7723f73
commit
3a95d0da01
124 changed files with 9438 additions and 258 deletions
|
|
@ -20,6 +20,9 @@ module.exports = {
|
||||||
ecmaVersion: 'latest',
|
ecmaVersion: 'latest',
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
|
"globals": {
|
||||||
|
JSX: "readonly"
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
'react',
|
'react',
|
||||||
'@typescript-eslint'
|
'@typescript-eslint'
|
||||||
|
|
|
||||||
648
package-lock.json
generated
648
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13
package.json
13
package.json
|
|
@ -33,22 +33,26 @@
|
||||||
"browser-encrypt-attachment": "0.3.0",
|
"browser-encrypt-attachment": "0.3.0",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.3.2",
|
||||||
"dateformat": "5.0.3",
|
"dateformat": "5.0.3",
|
||||||
|
"dayjs": "1.11.10",
|
||||||
"emojibase": "6.1.0",
|
"emojibase": "6.1.0",
|
||||||
"emojibase-data": "7.0.1",
|
"emojibase-data": "7.0.1",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"flux": "4.0.3",
|
"flux": "4.0.3",
|
||||||
"focus-trap-react": "10.0.2",
|
"focus-trap-react": "10.0.2",
|
||||||
"folds": "1.3.0",
|
"folds": "1.5.0",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
"html-react-parser": "3.0.4",
|
"html-react-parser": "4.2.0",
|
||||||
"immer": "9.0.16",
|
"immer": "9.0.16",
|
||||||
"is-hotkey": "0.2.0",
|
"is-hotkey": "0.2.0",
|
||||||
"jotai": "1.12.0",
|
"jotai": "1.12.0",
|
||||||
"katex": "0.16.4",
|
"katex": "0.16.4",
|
||||||
"linkify-html": "4.0.2",
|
"linkify-html": "4.0.2",
|
||||||
|
"linkify-react": "4.1.1",
|
||||||
"linkifyjs": "4.0.2",
|
"linkifyjs": "4.0.2",
|
||||||
"matrix-js-sdk": "24.1.0",
|
"matrix-js-sdk": "24.1.0",
|
||||||
"millify": "6.1.0",
|
"millify": "6.1.0",
|
||||||
|
"pdfjs-dist": "3.10.111",
|
||||||
|
"prismjs": "1.29.0",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-autosize-textarea": "7.1.0",
|
"react-autosize-textarea": "7.1.0",
|
||||||
|
|
@ -56,8 +60,10 @@
|
||||||
"react-dnd": "15.1.2",
|
"react-dnd": "15.1.2",
|
||||||
"react-dnd-html5-backend": "15.1.3",
|
"react-dnd-html5-backend": "15.1.3",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
|
"react-error-boundary": "4.0.10",
|
||||||
"react-google-recaptcha": "2.1.0",
|
"react-google-recaptcha": "2.1.0",
|
||||||
"react-modal": "3.16.1",
|
"react-modal": "3.16.1",
|
||||||
|
"react-range": "1.8.14",
|
||||||
"sanitize-html": "2.8.0",
|
"sanitize-html": "2.8.0",
|
||||||
"slate": "0.90.0",
|
"slate": "0.90.0",
|
||||||
"slate-history": "0.93.0",
|
"slate-history": "0.93.0",
|
||||||
|
|
@ -70,9 +76,12 @@
|
||||||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||||
"@rollup/plugin-inject": "5.0.3",
|
"@rollup/plugin-inject": "5.0.3",
|
||||||
"@rollup/plugin-wasm": "6.1.1",
|
"@rollup/plugin-wasm": "6.1.1",
|
||||||
|
"@types/file-saver": "2.0.5",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.11.18",
|
||||||
|
"@types/prismjs": "1.26.0",
|
||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "18.0.9",
|
"@types/react-dom": "18.0.9",
|
||||||
|
"@types/sanitize-html": "2.9.0",
|
||||||
"@types/ua-parser-js": "0.7.36",
|
"@types/ua-parser-js": "0.7.36",
|
||||||
"@typescript-eslint/eslint-plugin": "5.46.1",
|
"@typescript-eslint/eslint-plugin": "5.46.1",
|
||||||
"@typescript-eslint/parser": "5.46.1",
|
"@typescript-eslint/parser": "5.46.1",
|
||||||
|
|
|
||||||
37
src/app/components/Pdf-viewer/PdfViewer.css.ts
Normal file
37
src/app/components/Pdf-viewer/PdfViewer.css.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
import { DefaultReset, color, config } from 'folds';
|
||||||
|
|
||||||
|
export const PdfViewer = style([
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const PdfViewerHeader = style([
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
paddingLeft: config.space.S200,
|
||||||
|
paddingRight: config.space.S200,
|
||||||
|
borderBottomWidth: config.borderWidth.B300,
|
||||||
|
flexShrink: 0,
|
||||||
|
gap: config.space.S200,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
export const PdfViewerFooter = style([
|
||||||
|
PdfViewerHeader,
|
||||||
|
{
|
||||||
|
borderTopWidth: config.borderWidth.B300,
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const PdfViewerContent = style([
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
margin: 'auto',
|
||||||
|
display: 'inline-block',
|
||||||
|
backgroundColor: color.Surface.Container,
|
||||||
|
color: color.Surface.OnContainer,
|
||||||
|
},
|
||||||
|
]);
|
||||||
257
src/app/components/Pdf-viewer/PdfViewer.tsx
Normal file
257
src/app/components/Pdf-viewer/PdfViewer.tsx
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||||
|
import React, { FormEventHandler, useEffect, useRef, useState } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
Header,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Icons,
|
||||||
|
Input,
|
||||||
|
Menu,
|
||||||
|
PopOut,
|
||||||
|
Scroll,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
|
as,
|
||||||
|
config,
|
||||||
|
} from 'folds';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import FileSaver from 'file-saver';
|
||||||
|
import * as css from './PdfViewer.css';
|
||||||
|
import { AsyncStatus } from '../../hooks/useAsyncCallback';
|
||||||
|
import { useZoom } from '../../hooks/useZoom';
|
||||||
|
import { createPage, usePdfDocumentLoader, usePdfJSLoader } from '../../plugins/pdfjs-dist';
|
||||||
|
|
||||||
|
export type PdfViewerProps = {
|
||||||
|
name: string;
|
||||||
|
src: string;
|
||||||
|
requestClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PdfViewer = as<'div', PdfViewerProps>(
|
||||||
|
({ className, name, src, requestClose, ...props }, ref) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { zoom, zoomIn, zoomOut, setZoom } = useZoom(0.2);
|
||||||
|
|
||||||
|
const [pdfJSState, loadPdfJS] = usePdfJSLoader();
|
||||||
|
const [docState, loadPdfDocument] = usePdfDocumentLoader(
|
||||||
|
pdfJSState.status === AsyncStatus.Success ? pdfJSState.data : undefined,
|
||||||
|
src
|
||||||
|
);
|
||||||
|
const isLoading =
|
||||||
|
pdfJSState.status === AsyncStatus.Loading || docState.status === AsyncStatus.Loading;
|
||||||
|
const isError =
|
||||||
|
pdfJSState.status === AsyncStatus.Error || docState.status === AsyncStatus.Error;
|
||||||
|
const [pageNo, setPageNo] = useState(1);
|
||||||
|
const [openJump, setOpenJump] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadPdfJS();
|
||||||
|
}, [loadPdfJS]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (pdfJSState.status === AsyncStatus.Success) {
|
||||||
|
loadPdfDocument();
|
||||||
|
}
|
||||||
|
}, [pdfJSState, loadPdfDocument]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (docState.status === AsyncStatus.Success) {
|
||||||
|
const doc = docState.data;
|
||||||
|
if (pageNo < 0 || pageNo > doc.numPages) return;
|
||||||
|
createPage(doc, pageNo, { scale: zoom }).then((canvas) => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
container.textContent = '';
|
||||||
|
container.append(canvas);
|
||||||
|
scrollRef.current?.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [docState, pageNo, zoom]);
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
FileSaver.saveAs(src, name);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJumpSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
if (docState.status !== AsyncStatus.Success) return;
|
||||||
|
const jumpInput = evt.currentTarget.jumpInput as HTMLInputElement;
|
||||||
|
if (!jumpInput) return;
|
||||||
|
const jumpTo = parseInt(jumpInput.value, 10);
|
||||||
|
setPageNo(Math.max(1, Math.min(docState.data.numPages, jumpTo)));
|
||||||
|
setOpenJump(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevPage = () => {
|
||||||
|
setPageNo((n) => Math.max(n - 1, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextPage = () => {
|
||||||
|
if (docState.status !== AsyncStatus.Success) return;
|
||||||
|
setPageNo((n) => Math.min(n + 1, docState.data.numPages));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className={classNames(css.PdfViewer, className)} direction="Column" {...props} ref={ref}>
|
||||||
|
<Header className={css.PdfViewerHeader} size="400">
|
||||||
|
<Box grow="Yes" alignItems="Center" gap="200">
|
||||||
|
<IconButton size="300" radii="300" onClick={requestClose}>
|
||||||
|
<Icon size="50" src={Icons.ArrowLeft} />
|
||||||
|
</IconButton>
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box shrink="No" alignItems="Center" gap="200">
|
||||||
|
<IconButton
|
||||||
|
variant={zoom < 1 ? 'Success' : 'SurfaceVariant'}
|
||||||
|
outlined={zoom < 1}
|
||||||
|
size="300"
|
||||||
|
radii="Pill"
|
||||||
|
onClick={zoomOut}
|
||||||
|
aria-label="Zoom Out"
|
||||||
|
>
|
||||||
|
<Icon size="50" src={Icons.Minus} />
|
||||||
|
</IconButton>
|
||||||
|
<Chip variant="SurfaceVariant" radii="Pill" onClick={() => setZoom(zoom === 1 ? 2 : 1)}>
|
||||||
|
<Text size="B300">{Math.round(zoom * 100)}%</Text>
|
||||||
|
</Chip>
|
||||||
|
<IconButton
|
||||||
|
variant={zoom > 1 ? 'Success' : 'SurfaceVariant'}
|
||||||
|
outlined={zoom > 1}
|
||||||
|
size="300"
|
||||||
|
radii="Pill"
|
||||||
|
onClick={zoomIn}
|
||||||
|
aria-label="Zoom In"
|
||||||
|
>
|
||||||
|
<Icon size="50" src={Icons.Plus} />
|
||||||
|
</IconButton>
|
||||||
|
<Chip
|
||||||
|
variant="Primary"
|
||||||
|
onClick={handleDownload}
|
||||||
|
radii="300"
|
||||||
|
before={<Icon size="50" src={Icons.Download} />}
|
||||||
|
>
|
||||||
|
<Text size="B300">Download</Text>
|
||||||
|
</Chip>
|
||||||
|
</Box>
|
||||||
|
</Header>
|
||||||
|
<Box direction="Column" grow="Yes" alignItems="Center" justifyContent="Center" gap="200">
|
||||||
|
{isLoading && <Spinner variant="Secondary" size="600" />}
|
||||||
|
{isError && (
|
||||||
|
<>
|
||||||
|
<Text>Failed to load PDF</Text>
|
||||||
|
<Button
|
||||||
|
variant="Critical"
|
||||||
|
fill="Soft"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
before={<Icon src={Icons.Warning} size="50" />}
|
||||||
|
onClick={loadPdfJS}
|
||||||
|
>
|
||||||
|
<Text size="B300">Retry</Text>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{docState.status === AsyncStatus.Success && (
|
||||||
|
<Scroll
|
||||||
|
ref={scrollRef}
|
||||||
|
size="300"
|
||||||
|
direction="Both"
|
||||||
|
variant="Surface"
|
||||||
|
visibility="Hover"
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<div className={css.PdfViewerContent} ref={containerRef} />
|
||||||
|
</Box>
|
||||||
|
</Scroll>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{docState.status === AsyncStatus.Success && (
|
||||||
|
<Header as="footer" className={css.PdfViewerFooter} size="400">
|
||||||
|
<Chip
|
||||||
|
variant="Secondary"
|
||||||
|
radii="300"
|
||||||
|
before={<Icon size="50" src={Icons.ChevronLeft} />}
|
||||||
|
onClick={handlePrevPage}
|
||||||
|
aria-disabled={pageNo <= 1}
|
||||||
|
>
|
||||||
|
<Text size="B300">Previous</Text>
|
||||||
|
</Chip>
|
||||||
|
<Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200">
|
||||||
|
<PopOut
|
||||||
|
open={openJump}
|
||||||
|
align="Center"
|
||||||
|
position="Top"
|
||||||
|
content={
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: () => setOpenJump(false),
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu variant="Surface">
|
||||||
|
<Box
|
||||||
|
as="form"
|
||||||
|
onSubmit={handleJumpSubmit}
|
||||||
|
style={{ padding: config.space.S200 }}
|
||||||
|
direction="Column"
|
||||||
|
gap="200"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="jumpInput"
|
||||||
|
size="300"
|
||||||
|
variant="Background"
|
||||||
|
defaultValue={pageNo}
|
||||||
|
min={1}
|
||||||
|
max={docState.data.numPages}
|
||||||
|
step={1}
|
||||||
|
outlined
|
||||||
|
type="number"
|
||||||
|
radii="300"
|
||||||
|
aria-label="Page Number"
|
||||||
|
/>
|
||||||
|
<Button type="submit" size="300" variant="Primary" radii="300">
|
||||||
|
<Text size="B300">Jump To Page</Text>
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Menu>
|
||||||
|
</FocusTrap>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(anchorRef) => (
|
||||||
|
<Chip
|
||||||
|
onClick={() => setOpenJump(!openJump)}
|
||||||
|
ref={anchorRef}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
radii="300"
|
||||||
|
aria-pressed={openJump}
|
||||||
|
>
|
||||||
|
<Text size="B300">{`${pageNo}/${docState.data.numPages}`}</Text>
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</PopOut>
|
||||||
|
</Box>
|
||||||
|
<Chip
|
||||||
|
variant="Primary"
|
||||||
|
radii="300"
|
||||||
|
after={<Icon size="50" src={Icons.ChevronRight} />}
|
||||||
|
onClick={handleNextPage}
|
||||||
|
aria-disabled={pageNo >= docState.data.numPages}
|
||||||
|
>
|
||||||
|
<Text size="B300">Next</Text>
|
||||||
|
</Chip>
|
||||||
|
</Header>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
1
src/app/components/Pdf-viewer/index.ts
Normal file
1
src/app/components/Pdf-viewer/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './PdfViewer';
|
||||||
|
|
@ -54,7 +54,7 @@ export const useEditor = (): Editor => {
|
||||||
return editor;
|
return editor;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditorChangeHandler = ((value: Descendant[]) => void) | undefined;
|
export type EditorChangeHandler = (value: Descendant[]) => void;
|
||||||
type CustomEditorProps = {
|
type CustomEditorProps = {
|
||||||
top?: ReactNode;
|
top?: ReactNode;
|
||||||
bottom?: ReactNode;
|
bottom?: ReactNode;
|
||||||
|
|
@ -64,6 +64,7 @@ type CustomEditorProps = {
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onKeyDown?: KeyboardEventHandler;
|
onKeyDown?: KeyboardEventHandler;
|
||||||
|
onKeyUp?: KeyboardEventHandler;
|
||||||
onChange?: EditorChangeHandler;
|
onChange?: EditorChangeHandler;
|
||||||
onPaste?: ClipboardEventHandler;
|
onPaste?: ClipboardEventHandler;
|
||||||
};
|
};
|
||||||
|
|
@ -78,6 +79,7 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
||||||
editor,
|
editor,
|
||||||
placeholder,
|
placeholder,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
|
onKeyUp,
|
||||||
onChange,
|
onChange,
|
||||||
onPaste,
|
onPaste,
|
||||||
},
|
},
|
||||||
|
|
@ -141,6 +143,7 @@ export const CustomEditor = forwardRef<HTMLDivElement, CustomEditorProps>(
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
renderLeaf={renderLeaf}
|
renderLeaf={renderLeaf}
|
||||||
onKeyDown={handleKeydown}
|
onKeyDown={handleKeydown}
|
||||||
|
onKeyUp={onKeyUp}
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
/>
|
/>
|
||||||
</Scroll>
|
</Scroll>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Scroll, Text } from 'folds';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { RenderElementProps, RenderLeafProps, useFocused, useSelected } from 'slate-react';
|
import { RenderElementProps, RenderLeafProps, useFocused, useSelected } from 'slate-react';
|
||||||
|
|
||||||
import * as css from './Elements.css';
|
import * as css from '../../styles/CustomHtml.css';
|
||||||
import { EmoticonElement, LinkElement, MentionElement } from './slate';
|
import { EmoticonElement, LinkElement, MentionElement } from './slate';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
|
||||||
|
|
@ -145,7 +145,13 @@ export function RenderElement({ attributes, element, children }: RenderElementPr
|
||||||
case BlockType.CodeBlock:
|
case BlockType.CodeBlock:
|
||||||
return (
|
return (
|
||||||
<Text as="pre" className={css.CodeBlock} {...attributes}>
|
<Text as="pre" className={css.CodeBlock} {...attributes}>
|
||||||
<Scroll direction="Horizontal" variant="Warning" size="300" visibility="Hover" hideTrack>
|
<Scroll
|
||||||
|
direction="Horizontal"
|
||||||
|
variant="Secondary"
|
||||||
|
size="300"
|
||||||
|
visibility="Hover"
|
||||||
|
hideTrack
|
||||||
|
>
|
||||||
<div className={css.CodeBlockInternal}>{children}</div>
|
<div className={css.CodeBlockInternal}>{children}</div>
|
||||||
</Scroll>
|
</Scroll>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -242,7 +248,7 @@ export function RenderLeaf({ attributes, leaf, children }: RenderLeafProps) {
|
||||||
);
|
);
|
||||||
if (leaf.spoiler)
|
if (leaf.spoiler)
|
||||||
child = (
|
child = (
|
||||||
<span className={css.Spoiler} {...attributes}>
|
<span className={css.Spoiler()} {...attributes}>
|
||||||
<InlineChromiumBugfix />
|
<InlineChromiumBugfix />
|
||||||
{child}
|
{child}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,9 @@ export function RoomMentionAutocomplete({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rId = autoCompleteRoomIds[0];
|
const rId = autoCompleteRoomIds[0];
|
||||||
const name = mx.getRoom(rId)?.name ?? rId;
|
const r = mx.getRoom(rId);
|
||||||
handleAutocomplete(rId, name);
|
const name = r?.name ?? rId;
|
||||||
|
handleAutocomplete(r?.getCanonicalAlias() ?? rId, name);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -147,7 +148,7 @@ export function RoomMentionAutocomplete({
|
||||||
onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) =>
|
onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) =>
|
||||||
onTabPress(evt, () => handleAutocomplete(rId, room.name))
|
onTabPress(evt, () => handleAutocomplete(rId, room.name))
|
||||||
}
|
}
|
||||||
onClick={() => handleAutocomplete(rId, room.name)}
|
onClick={() => handleAutocomplete(room.getCanonicalAlias() ?? rId, room.name)}
|
||||||
after={
|
after={
|
||||||
<Text size="T200" priority="300" truncate>
|
<Text size="T200" priority="300" truncate>
|
||||||
{room.getCanonicalAlias() ?? ''}
|
{room.getCanonicalAlias() ?? ''}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useRecentEmoji } from '../../hooks/useRecentEmoji';
|
import { useRecentEmoji } from '../../hooks/useRecentEmoji';
|
||||||
import { ExtendedPackImage, ImagePack, PackUsage } from '../../plugins/custom-emoji';
|
import { ExtendedPackImage, ImagePack, PackUsage } from '../../plugins/custom-emoji';
|
||||||
import { isUserId } from '../../utils/matrix';
|
import { isUserId } from '../../utils/matrix';
|
||||||
import { editableActiveElement, inVisibleScrollArea, targetFromEvent } from '../../utils/dom';
|
import { editableActiveElement, isIntersectingScrollView, targetFromEvent } from '../../utils/dom';
|
||||||
import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch';
|
import { useAsyncSearch, UseAsyncSearchOptions } from '../../hooks/useAsyncSearch';
|
||||||
import { useDebounce } from '../../hooks/useDebounce';
|
import { useDebounce } from '../../hooks/useDebounce';
|
||||||
import { useThrottle } from '../../hooks/useThrottle';
|
import { useThrottle } from '../../hooks/useThrottle';
|
||||||
|
|
@ -675,7 +675,7 @@ export function EmojiBoard({
|
||||||
const targetEl = contentScrollRef.current;
|
const targetEl = contentScrollRef.current;
|
||||||
if (!targetEl) return;
|
if (!targetEl) return;
|
||||||
const groupEls = [...targetEl.querySelectorAll('div[data-group-id]')] as HTMLElement[];
|
const groupEls = [...targetEl.querySelectorAll('div[data-group-id]')] as HTMLElement[];
|
||||||
const groupEl = groupEls.find((el) => inVisibleScrollArea(targetEl, el));
|
const groupEl = groupEls.find((el) => isIntersectingScrollView(targetEl, el));
|
||||||
const groupId = groupEl?.getAttribute('data-group-id') ?? undefined;
|
const groupId = groupEl?.getAttribute('data-group-id') ?? undefined;
|
||||||
setActiveGroupId(groupId);
|
setActiveGroupId(groupId);
|
||||||
}, [setActiveGroupId]);
|
}, [setActiveGroupId]);
|
||||||
|
|
|
||||||
21
src/app/components/event-readers/EventReaders.css.ts
Normal file
21
src/app/components/event-readers/EventReaders.css.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
import { DefaultReset, config } from 'folds';
|
||||||
|
|
||||||
|
export const EventReaders = style([
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const Header = style({
|
||||||
|
paddingLeft: config.space.S400,
|
||||||
|
paddingRight: config.space.S300,
|
||||||
|
|
||||||
|
flexShrink: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Content = style({
|
||||||
|
paddingLeft: config.space.S200,
|
||||||
|
paddingBottom: config.space.S400,
|
||||||
|
});
|
||||||
110
src/app/components/event-readers/EventReaders.tsx
Normal file
110
src/app/components/event-readers/EventReaders.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarImage,
|
||||||
|
Box,
|
||||||
|
Header,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Icons,
|
||||||
|
MenuItem,
|
||||||
|
Scroll,
|
||||||
|
Text,
|
||||||
|
as,
|
||||||
|
config,
|
||||||
|
} from 'folds';
|
||||||
|
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||||
|
import { useRoomEventReaders } from '../../hooks/useRoomEventReaders';
|
||||||
|
import { getMemberDisplayName } from '../../utils/room';
|
||||||
|
import { getMxIdLocalPart } from '../../utils/matrix';
|
||||||
|
import * as css from './EventReaders.css';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
import colorMXID from '../../../util/colorMXID';
|
||||||
|
import { openProfileViewer } from '../../../client/action/navigation';
|
||||||
|
|
||||||
|
export type EventReadersProps = {
|
||||||
|
room: Room;
|
||||||
|
eventId: string;
|
||||||
|
requestClose: () => void;
|
||||||
|
};
|
||||||
|
export const EventReaders = as<'div', EventReadersProps>(
|
||||||
|
({ className, room, eventId, requestClose, ...props }, ref) => {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const latestEventReaders = useRoomEventReaders(room, eventId);
|
||||||
|
const followingMembers = latestEventReaders
|
||||||
|
.map((readerId) => room.getMember(readerId))
|
||||||
|
.filter((member) => member) as RoomMember[];
|
||||||
|
|
||||||
|
const getName = (member: RoomMember) =>
|
||||||
|
getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={classNames(css.EventReaders, className)}
|
||||||
|
direction="Column"
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<Header className={css.Header} variant="Surface" size="600">
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Text size="H3">Seen by</Text>
|
||||||
|
</Box>
|
||||||
|
<IconButton size="300" onClick={requestClose}>
|
||||||
|
<Icon src={Icons.Cross} />
|
||||||
|
</IconButton>
|
||||||
|
</Header>
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Scroll visibility="Hover" hideTrack size="300">
|
||||||
|
<Box className={css.Content} direction="Column">
|
||||||
|
{followingMembers.map((member) => {
|
||||||
|
const name = getName(member);
|
||||||
|
const avatarUrl = member.getAvatarUrl(
|
||||||
|
mx.baseUrl,
|
||||||
|
100,
|
||||||
|
100,
|
||||||
|
'crop',
|
||||||
|
undefined,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={member.userId}
|
||||||
|
style={{ padding: `0 ${config.space.S200}` }}
|
||||||
|
radii="400"
|
||||||
|
onClick={() => {
|
||||||
|
requestClose();
|
||||||
|
openProfileViewer(member.userId, room.roomId);
|
||||||
|
}}
|
||||||
|
before={
|
||||||
|
<Avatar size="200">
|
||||||
|
{avatarUrl ? (
|
||||||
|
<AvatarImage src={avatarUrl} />
|
||||||
|
) : (
|
||||||
|
<AvatarFallback
|
||||||
|
style={{
|
||||||
|
background: colorMXID(member.userId),
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="H6">{name[0]}</Text>
|
||||||
|
</AvatarFallback>
|
||||||
|
)}
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="T400" truncate>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Scroll>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
1
src/app/components/event-readers/index.ts
Normal file
1
src/app/components/event-readers/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './EventReaders';
|
||||||
40
src/app/components/image-viewer/ImageViewer.css.ts
Normal file
40
src/app/components/image-viewer/ImageViewer.css.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
import { DefaultReset, color, config } from 'folds';
|
||||||
|
|
||||||
|
export const ImageViewer = style([
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ImageViewerHeader = style([
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
paddingLeft: config.space.S200,
|
||||||
|
paddingRight: config.space.S200,
|
||||||
|
borderBottomWidth: config.borderWidth.B300,
|
||||||
|
flexShrink: 0,
|
||||||
|
gap: config.space.S200,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ImageViewerContent = style([
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
backgroundColor: color.Background.Container,
|
||||||
|
color: color.Background.OnContainer,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ImageViewerImg = style([
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
objectFit: 'contain',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: color.Surface.Container,
|
||||||
|
transition: 'transform 100ms linear',
|
||||||
|
},
|
||||||
|
]);
|
||||||
95
src/app/components/image-viewer/ImageViewer.tsx
Normal file
95
src/app/components/image-viewer/ImageViewer.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||||
|
import React from 'react';
|
||||||
|
import FileSaver from 'file-saver';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Box, Chip, Header, Icon, IconButton, Icons, Text, as } from 'folds';
|
||||||
|
import * as css from './ImageViewer.css';
|
||||||
|
import { useZoom } from '../../hooks/useZoom';
|
||||||
|
import { usePan } from '../../hooks/usePan';
|
||||||
|
|
||||||
|
export type ImageViewerProps = {
|
||||||
|
alt: string;
|
||||||
|
src: string;
|
||||||
|
requestClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImageViewer = as<'div', ImageViewerProps>(
|
||||||
|
({ className, alt, src, requestClose, ...props }, ref) => {
|
||||||
|
const { zoom, zoomIn, zoomOut, setZoom } = useZoom(0.2);
|
||||||
|
const { pan, cursor, onMouseDown } = usePan(zoom !== 1);
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
FileSaver.saveAs(src, alt);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={classNames(css.ImageViewer, className)}
|
||||||
|
direction="Column"
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<Header className={css.ImageViewerHeader} size="400">
|
||||||
|
<Box grow="Yes" alignItems="Center" gap="200">
|
||||||
|
<IconButton size="300" radii="300" onClick={requestClose}>
|
||||||
|
<Icon size="50" src={Icons.ArrowLeft} />
|
||||||
|
</IconButton>
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
{alt}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box shrink="No" alignItems="Center" gap="200">
|
||||||
|
<IconButton
|
||||||
|
variant={zoom < 1 ? 'Success' : 'SurfaceVariant'}
|
||||||
|
outlined={zoom < 1}
|
||||||
|
size="300"
|
||||||
|
radii="Pill"
|
||||||
|
onClick={zoomOut}
|
||||||
|
aria-label="Zoom Out"
|
||||||
|
>
|
||||||
|
<Icon size="50" src={Icons.Minus} />
|
||||||
|
</IconButton>
|
||||||
|
<Chip variant="SurfaceVariant" radii="Pill" onClick={() => setZoom(zoom === 1 ? 2 : 1)}>
|
||||||
|
<Text size="B300">{Math.round(zoom * 100)}%</Text>
|
||||||
|
</Chip>
|
||||||
|
<IconButton
|
||||||
|
variant={zoom > 1 ? 'Success' : 'SurfaceVariant'}
|
||||||
|
outlined={zoom > 1}
|
||||||
|
size="300"
|
||||||
|
radii="Pill"
|
||||||
|
onClick={zoomIn}
|
||||||
|
aria-label="Zoom In"
|
||||||
|
>
|
||||||
|
<Icon size="50" src={Icons.Plus} />
|
||||||
|
</IconButton>
|
||||||
|
<Chip
|
||||||
|
variant="Primary"
|
||||||
|
onClick={handleDownload}
|
||||||
|
radii="300"
|
||||||
|
before={<Icon size="50" src={Icons.Download} />}
|
||||||
|
>
|
||||||
|
<Text size="B300">Download</Text>
|
||||||
|
</Chip>
|
||||||
|
</Box>
|
||||||
|
</Header>
|
||||||
|
<Box
|
||||||
|
grow="Yes"
|
||||||
|
className={css.ImageViewerContent}
|
||||||
|
justifyContent="Center"
|
||||||
|
alignItems="Center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={css.ImageViewerImg}
|
||||||
|
style={{
|
||||||
|
cursor,
|
||||||
|
transform: `scale(${zoom}) translate(${pan.translateX}px, ${pan.translateY}px)`,
|
||||||
|
}}
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
1
src/app/components/image-viewer/index.ts
Normal file
1
src/app/components/image-viewer/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './ImageViewer';
|
||||||
9
src/app/components/media/Image.tsx
Normal file
9
src/app/components/media/Image.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React, { ImgHTMLAttributes, forwardRef } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import * as css from './media.css';
|
||||||
|
|
||||||
|
export const Image = forwardRef<HTMLImageElement, ImgHTMLAttributes<HTMLImageElement>>(
|
||||||
|
({ className, alt, ...props }, ref) => (
|
||||||
|
<img className={classNames(css.Image, className)} alt={alt} {...props} ref={ref} />
|
||||||
|
)
|
||||||
|
);
|
||||||
27
src/app/components/media/MediaControls.tsx
Normal file
27
src/app/components/media/MediaControls.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Box, as } from 'folds';
|
||||||
|
|
||||||
|
export type MediaControlProps = {
|
||||||
|
before?: ReactNode;
|
||||||
|
after?: ReactNode;
|
||||||
|
leftControl?: ReactNode;
|
||||||
|
rightControl?: ReactNode;
|
||||||
|
};
|
||||||
|
export const MediaControl = as<'div', MediaControlProps>(
|
||||||
|
({ before, after, leftControl, rightControl, children, ...props }, ref) => (
|
||||||
|
<Box grow="Yes" direction="Column" gap="300" {...props} ref={ref}>
|
||||||
|
{before && <Box direction="Column">{before}</Box>}
|
||||||
|
<Box alignItems="Center" gap="200">
|
||||||
|
<Box alignItems="Center" grow="Yes" gap="Inherit">
|
||||||
|
{leftControl}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box justifyItems="End" alignItems="Center" gap="Inherit">
|
||||||
|
{rightControl}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{after && <Box direction="Column">{after}</Box>}
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
);
|
||||||
10
src/app/components/media/Video.tsx
Normal file
10
src/app/components/media/Video.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React, { VideoHTMLAttributes, forwardRef } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import * as css from './media.css';
|
||||||
|
|
||||||
|
export const Video = forwardRef<HTMLVideoElement, VideoHTMLAttributes<HTMLVideoElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
// eslint-disable-next-line jsx-a11y/media-has-caption
|
||||||
|
<video className={classNames(css.Image, className)} {...props} ref={ref} />
|
||||||
|
)
|
||||||
|
);
|
||||||
3
src/app/components/media/index.ts
Normal file
3
src/app/components/media/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './Image';
|
||||||
|
export * from './Video';
|
||||||
|
export * from './MediaControls';
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue