initial changes
This commit is contained in:
parent
2a1bf4a42a
commit
a2b8b127f4
19 changed files with 2177 additions and 3102 deletions
4718
package-lock.json
generated
4718
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -52,7 +52,6 @@
|
||||||
"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",
|
"prismjs": "1.29.0",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
|
@ -63,7 +62,6 @@
|
||||||
"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-error-boundary": "4.0.10",
|
||||||
"react-google-recaptcha": "2.1.0",
|
|
||||||
"react-modal": "3.16.1",
|
"react-modal": "3.16.1",
|
||||||
"react-range": "1.8.14",
|
"react-range": "1.8.14",
|
||||||
"sanitize-html": "2.8.0",
|
"sanitize-html": "2.8.0",
|
||||||
|
@ -71,8 +69,7 @@
|
||||||
"slate-history": "0.93.0",
|
"slate-history": "0.93.0",
|
||||||
"slate-react": "0.98.4",
|
"slate-react": "0.98.4",
|
||||||
"tippy.js": "6.3.7",
|
"tippy.js": "6.3.7",
|
||||||
"twemoji": "14.0.2",
|
"twemoji": "14.0.2"
|
||||||
"ua-parser-js": "1.0.35"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: 42px;
|
width: 42px;
|
||||||
height: 42px;
|
height: 42px;
|
||||||
border-radius: var(--bo-radius);
|
border-radius: 50%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&__large {
|
&__large {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||||
import './ContextMenu.scss';
|
import './ContextMenu.scss';
|
||||||
|
|
||||||
import Tippy from '@tippyjs/react';
|
import Tippy from '@tippyjs/react';
|
||||||
import 'tippy.js/animations/scale-extreme.css';
|
import 'tippy.js/animations/scale-subtle.css';
|
||||||
|
|
||||||
import Text from '../text/Text';
|
import Text from '../text/Text';
|
||||||
import Button from '../button/Button';
|
import Button from '../button/Button';
|
||||||
|
@ -22,7 +22,7 @@ function ContextMenu({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tippy
|
<Tippy
|
||||||
animation="scale-extreme"
|
animation="scale-subtle"
|
||||||
className="context-menu"
|
className="context-menu"
|
||||||
visible={isVisible}
|
visible={isVisible}
|
||||||
onClickOutside={hideMenu}
|
onClickOutside={hideMenu}
|
||||||
|
@ -31,7 +31,7 @@ function ContextMenu({
|
||||||
interactive
|
interactive
|
||||||
arrow={false}
|
arrow={false}
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
duration={200}
|
duration={125}
|
||||||
>
|
>
|
||||||
{render(isVisible ? hideMenu : showMenu)}
|
{render(isVisible ? hideMenu : showMenu)}
|
||||||
</Tippy>
|
</Tippy>
|
||||||
|
|
|
@ -39,20 +39,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ReactModal__Overlay {
|
.ReactModal__Overlay {
|
||||||
animation: raw-modal--overlay 150ms;
|
animation: raw-modal--overlay 190ms;
|
||||||
|
animation-timing-function: cubic-bezier(0.77,0,0.18,1);
|
||||||
|
contain: strict;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ReactModal__Content {
|
.ReactModal__Content {
|
||||||
animation: raw-modal--content 150ms;
|
animation: raw-modal--content 190ms;
|
||||||
|
animation-timing-function: cubic-bezier(0.77,0,0.18,1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes raw-modal--content {
|
@keyframes raw-modal--content {
|
||||||
0% {
|
0% {
|
||||||
transform: translateY(100px);
|
transform: scale(0.90);
|
||||||
opacity: .5;
|
opacity: .4;
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
transform: translateY(0);
|
transform: scale(1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,13 @@ import { isInSameDay } from '../../../util/common';
|
||||||
function Time({ timestamp, fullTime }) {
|
function Time({ timestamp, fullTime }) {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
|
|
||||||
const formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT');
|
let formattedFullTime;
|
||||||
let formattedDate = formattedFullTime;
|
let formattedDate;
|
||||||
|
|
||||||
if (!fullTime) {
|
if (fullTime) {
|
||||||
|
formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT');
|
||||||
|
formattedDate = formattedFullTime;
|
||||||
|
} else {
|
||||||
const compareDate = new Date();
|
const compareDate = new Date();
|
||||||
const isToday = isInSameDay(date, compareDate);
|
const isToday = isInSameDay(date, compareDate);
|
||||||
compareDate.setDate(compareDate.getDate() - 1);
|
compareDate.setDate(compareDate.getDate() - 1);
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
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,
|
|
||||||
},
|
|
||||||
]);
|
|
|
@ -1,257 +0,0 @@
|
||||||
/* 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 +0,0 @@
|
||||||
export * from './PdfViewer';
|
|
|
@ -30,7 +30,6 @@ import {
|
||||||
import * as css from './Editor.css';
|
import * as css from './Editor.css';
|
||||||
import { BlockType, MarkType } from './types';
|
import { BlockType, MarkType } from './types';
|
||||||
import { HeadingLevel } from './slate';
|
import { HeadingLevel } from './slate';
|
||||||
import { isMacOS } from '../../utils/user-agent';
|
|
||||||
import { KeySymbol } from '../../utils/key-symbol';
|
import { KeySymbol } from '../../utils/key-symbol';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
|
@ -121,7 +120,7 @@ export function HeadingBlockButton() {
|
||||||
const level = headingLevel(editor);
|
const level = headingLevel(editor);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const isActive = isBlockActive(editor, BlockType.Heading);
|
const isActive = isBlockActive(editor, BlockType.Heading);
|
||||||
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
|
const modKey = 'Ctrl';
|
||||||
|
|
||||||
const handleMenuSelect = (selectedLevel: HeadingLevel) => {
|
const handleMenuSelect = (selectedLevel: HeadingLevel) => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
@ -247,7 +246,7 @@ export function ExitFormatting({ tooltip }: ExitFormattingProps) {
|
||||||
|
|
||||||
export function Toolbar() {
|
export function Toolbar() {
|
||||||
const editor = useSlate();
|
const editor = useSlate();
|
||||||
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
|
const modKey = 'Ctrl';
|
||||||
const disableInline = isBlockActive(editor, BlockType.CodeBlock);
|
const disableInline = isBlockActive(editor, BlockType.CodeBlock);
|
||||||
|
|
||||||
const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph);
|
const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph);
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
@use '../../partials/text';
|
@use '../../partials/text';
|
||||||
|
|
||||||
.people-selector {
|
.people-selector {
|
||||||
width: 100%;
|
flex-grow: 1;
|
||||||
padding: var(--sp-extra-tight) var(--sp-normal);
|
padding: var(--sp-extra-tight) var(--sp-normal);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&__container {
|
&__container {
|
||||||
|
margin: var(--sp-extra-tight);
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
& .room-selector {
|
& .room-selector {
|
||||||
width: calc(100% - var(--sp-extra-tight));
|
margin: var(--sp-ultra-tight);
|
||||||
@include dir.side(margin, auto, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -29,7 +29,6 @@ import {
|
||||||
getFileNameExt,
|
getFileNameExt,
|
||||||
mimeTypeToExt,
|
mimeTypeToExt,
|
||||||
} from '../../../utils/mimeTypes';
|
} from '../../../utils/mimeTypes';
|
||||||
import { PdfViewer } from '../../../components/Pdf-viewer';
|
|
||||||
import * as css from './styles.css';
|
import * as css from './styles.css';
|
||||||
|
|
||||||
export type FileContentProps = {
|
export type FileContentProps = {
|
||||||
|
@ -149,72 +148,6 @@ function ReadTextFile({ body, mimeType, url, encInfo }: Omit<FileContentProps, '
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReadPdfFile({ body, mimeType, url, encInfo }: Omit<FileContentProps, 'info'>) {
|
|
||||||
const mx = useMatrixClient();
|
|
||||||
const [pdfViewer, setPdfViewer] = useState(false);
|
|
||||||
|
|
||||||
const [pdfState, loadPdf] = useAsyncCallback(
|
|
||||||
useCallback(async () => {
|
|
||||||
const httpUrl = await getFileSrcUrl(mx.mxcUrlToHttp(url) ?? '', mimeType, encInfo);
|
|
||||||
setPdfViewer(true);
|
|
||||||
return httpUrl;
|
|
||||||
}, [mx, url, mimeType, encInfo])
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{pdfState.status === AsyncStatus.Success && (
|
|
||||||
<Overlay open={pdfViewer} backdrop={<OverlayBackdrop />}>
|
|
||||||
<OverlayCenter>
|
|
||||||
<FocusTrap
|
|
||||||
focusTrapOptions={{
|
|
||||||
initialFocus: false,
|
|
||||||
onDeactivate: () => setPdfViewer(false),
|
|
||||||
clickOutsideDeactivates: true,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Modal
|
|
||||||
className={css.ModalWide}
|
|
||||||
size="500"
|
|
||||||
onContextMenu={(evt: any) => evt.stopPropagation()}
|
|
||||||
>
|
|
||||||
<PdfViewer
|
|
||||||
name={body}
|
|
||||||
src={pdfState.data}
|
|
||||||
requestClose={() => setPdfViewer(false)}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
</FocusTrap>
|
|
||||||
</OverlayCenter>
|
|
||||||
</Overlay>
|
|
||||||
)}
|
|
||||||
{pdfState.status === AsyncStatus.Error ? (
|
|
||||||
renderErrorButton(loadPdf, 'Open PDF')
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="Secondary"
|
|
||||||
fill="Solid"
|
|
||||||
radii="300"
|
|
||||||
size="400"
|
|
||||||
onClick={() => (pdfState.status === AsyncStatus.Success ? setPdfViewer(true) : loadPdf())}
|
|
||||||
disabled={pdfState.status === AsyncStatus.Loading}
|
|
||||||
before={
|
|
||||||
pdfState.status === AsyncStatus.Loading ? (
|
|
||||||
<Spinner fill="Solid" size="100" variant="Secondary" />
|
|
||||||
) : (
|
|
||||||
<Icon size="100" src={Icons.ArrowRight} filled />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text size="B400" truncate>
|
|
||||||
Open PDF
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DownloadFile({ body, mimeType, url, info, encInfo }: FileContentProps) {
|
function DownloadFile({ body, mimeType, url, info, encInfo }: FileContentProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
|
||||||
|
@ -260,9 +193,6 @@ export const FileContent = as<'div', FileContentProps>(
|
||||||
READABLE_EXT_TO_MIME_TYPE[getFileNameExt(body)]) && (
|
READABLE_EXT_TO_MIME_TYPE[getFileNameExt(body)]) && (
|
||||||
<ReadTextFile body={body} mimeType={mimeType} url={url} encInfo={encInfo} />
|
<ReadTextFile body={body} mimeType={mimeType} url={url} encInfo={encInfo} />
|
||||||
)}
|
)}
|
||||||
{mimeType === 'application/pdf' && (
|
|
||||||
<ReadPdfFile body={body} mimeType={mimeType} url={url} encInfo={encInfo} />
|
|
||||||
)}
|
|
||||||
<DownloadFile body={body} mimeType={mimeType} url={url} info={info} encInfo={encInfo} />
|
<DownloadFile body={body} mimeType={mimeType} url={url} info={info} encInfo={encInfo} />
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, Suspense, lazy } from 'react';
|
||||||
import './Settings.scss';
|
import './Settings.scss';
|
||||||
|
|
||||||
import initMatrix from '../../../client/initMatrix';
|
import initMatrix from '../../../client/initMatrix';
|
||||||
|
@ -29,7 +29,6 @@ import KeywordNotification from '../../molecules/global-notification/KeywordNoti
|
||||||
import IgnoreUserList from '../../molecules/global-notification/IgnoreUserList';
|
import IgnoreUserList from '../../molecules/global-notification/IgnoreUserList';
|
||||||
|
|
||||||
import ProfileEditor from '../profile-editor/ProfileEditor';
|
import ProfileEditor from '../profile-editor/ProfileEditor';
|
||||||
import CrossSigning from './CrossSigning';
|
|
||||||
import KeyBackup from './KeyBackup';
|
import KeyBackup from './KeyBackup';
|
||||||
import DeviceManage from './DeviceManage';
|
import DeviceManage from './DeviceManage';
|
||||||
|
|
||||||
|
@ -45,8 +44,10 @@ import CinnySVG from '../../../../public/res/svg/cinny.svg';
|
||||||
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
import { isMacOS } from '../../utils/user-agent';
|
|
||||||
import { KeySymbol } from '../../utils/key-symbol';
|
|
||||||
|
const CrossSigning = lazy(() => import("./CrossSigning"));
|
||||||
|
|
||||||
|
|
||||||
function AppearanceSection() {
|
function AppearanceSection() {
|
||||||
const [, updateState] = useState({});
|
const [, updateState] = useState({});
|
||||||
|
@ -151,7 +152,7 @@ function AppearanceSection() {
|
||||||
onToggle={() => setEnterForNewline(!enterForNewline) }
|
onToggle={() => setEnterForNewline(!enterForNewline) }
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
content={<Text variant="b3">{`Use ${isMacOS() ? KeySymbol.Command : 'Ctrl'} + ENTER to send message and ENTER for newline.`}</Text>}
|
content={<Text variant="b3">{'Use Ctrl + ENTER to send message and ENTER for newline.'}</Text>}
|
||||||
/>
|
/>
|
||||||
<SettingTile
|
<SettingTile
|
||||||
title="Markdown formatting"
|
title="Markdown formatting"
|
||||||
|
@ -302,7 +303,9 @@ function SecuritySection() {
|
||||||
<div className="settings-security">
|
<div className="settings-security">
|
||||||
<div className="settings-security__card">
|
<div className="settings-security__card">
|
||||||
<MenuHeader>Cross signing and backup</MenuHeader>
|
<MenuHeader>Cross signing and backup</MenuHeader>
|
||||||
|
<Suspense fallback={ <Text>Loading...</Text> }>
|
||||||
<CrossSigning />
|
<CrossSigning />
|
||||||
|
</Suspense>
|
||||||
<KeyBackup />
|
<KeyBackup />
|
||||||
</div>
|
</div>
|
||||||
<DeviceManage />
|
<DeviceManage />
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
import React, { StrictMode } from 'react';
|
import React, { StrictMode, Suspense, lazy } from 'react';
|
||||||
import { Provider } from 'jotai';
|
import { Provider } from 'jotai';
|
||||||
|
|
||||||
import { isAuthenticated } from '../../client/state/auth';
|
import { isAuthenticated } from '../../client/state/auth';
|
||||||
|
|
||||||
import Auth from '../templates/auth/Auth';
|
|
||||||
import Client from '../templates/client/Client';
|
import Client from '../templates/client/Client';
|
||||||
|
import Text from '../atoms/text/Text';
|
||||||
|
|
||||||
|
const Auth = lazy(() => import("../templates/auth/Auth"));
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<Provider>{isAuthenticated() ? <Client /> : <Auth />}</Provider>
|
<Provider>{
|
||||||
|
isAuthenticated() ?
|
||||||
|
<Client /> :
|
||||||
|
<Suspense fallback={ <Text>Loading...</Text> }><Auth /></Suspense>}
|
||||||
|
</Provider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { useCallback } from 'react';
|
|
||||||
import type * as PdfJsDist from 'pdfjs-dist';
|
|
||||||
import type { GetViewportParameters } from 'pdfjs-dist/types/src/display/api';
|
|
||||||
import { useAsyncCallback } from '../hooks/useAsyncCallback';
|
|
||||||
|
|
||||||
export const usePdfJSLoader = () =>
|
|
||||||
useAsyncCallback(
|
|
||||||
useCallback(async () => {
|
|
||||||
const pdf = await import('pdfjs-dist');
|
|
||||||
pdf.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js';
|
|
||||||
return pdf;
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
export const usePdfDocumentLoader = (pdfJS: typeof PdfJsDist | undefined, src: string) =>
|
|
||||||
useAsyncCallback(
|
|
||||||
useCallback(async () => {
|
|
||||||
if (!pdfJS) {
|
|
||||||
throw new Error('PdfJS is not loaded');
|
|
||||||
}
|
|
||||||
const doc = await pdfJS.getDocument(src).promise;
|
|
||||||
return doc;
|
|
||||||
}, [pdfJS, src])
|
|
||||||
);
|
|
||||||
|
|
||||||
export const createPage = async (
|
|
||||||
doc: PdfJsDist.PDFDocumentProxy,
|
|
||||||
pNo: number,
|
|
||||||
opts: GetViewportParameters
|
|
||||||
): Promise<HTMLCanvasElement> => {
|
|
||||||
const page = await doc.getPage(pNo);
|
|
||||||
const pageViewport = page.getViewport(opts);
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
|
|
||||||
if (!context) throw new Error('failed to render page.');
|
|
||||||
|
|
||||||
canvas.width = pageViewport.width;
|
|
||||||
canvas.height = pageViewport.height;
|
|
||||||
|
|
||||||
page.render({
|
|
||||||
canvasContext: context,
|
|
||||||
viewport: pageViewport,
|
|
||||||
});
|
|
||||||
|
|
||||||
return canvas;
|
|
||||||
};
|
|
|
@ -2,7 +2,6 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import './Auth.scss';
|
import './Auth.scss';
|
||||||
import ReCAPTCHA from 'react-google-recaptcha';
|
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
|
|
||||||
import * as auth from '../../../client/action/auth';
|
import * as auth from '../../../client/action/auth';
|
||||||
|
@ -400,17 +399,6 @@ function Register({ registerInfo, loginFlow, baseUrl }) {
|
||||||
asyncProcess();
|
asyncProcess();
|
||||||
}, [process]);
|
}, [process]);
|
||||||
|
|
||||||
const handleRecaptcha = async (value) => {
|
|
||||||
if (typeof value !== 'string') return;
|
|
||||||
const [username, password] = getInputs();
|
|
||||||
const d = await auth.completeRegisterStage(baseUrl, username, password, {
|
|
||||||
type: 'm.login.recaptcha',
|
|
||||||
response: value,
|
|
||||||
session,
|
|
||||||
});
|
|
||||||
if (d.done) refreshWindow();
|
|
||||||
else setProcess({ type: 'processing', message: 'Registration in progress...' });
|
|
||||||
};
|
|
||||||
const handleTerms = async () => {
|
const handleTerms = async () => {
|
||||||
const [username, password] = getInputs();
|
const [username, password] = getInputs();
|
||||||
const d = await auth.completeRegisterStage(baseUrl, username, password, {
|
const d = await auth.completeRegisterStage(baseUrl, username, password, {
|
||||||
|
@ -435,7 +423,7 @@ function Register({ registerInfo, loginFlow, baseUrl }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{process.type === 'processing' && <LoadingScreen message={process.message} />}
|
{process.type === 'processing' && <LoadingScreen message={process.message} />}
|
||||||
{process.type === 'm.login.recaptcha' && <Recaptcha message="Please check the box below to proceed." sitekey={process.sitekey} onChange={handleRecaptcha} />}
|
{process.type === 'm.login.recaptcha' && <Text weight="medium">CAPTCHA is not supported</Text>}
|
||||||
{process.type === 'm.login.terms' && <Terms url={process.url} onSubmit={handleTerms} />}
|
{process.type === 'm.login.terms' && <Terms url={process.url} onSubmit={handleTerms} />}
|
||||||
{process.type === 'm.login.email.identity' && <EmailVerify email={process.email} onContinue={handleEmailVerify} />}
|
{process.type === 'm.login.email.identity' && <EmailVerify email={process.email} onContinue={handleEmailVerify} />}
|
||||||
<div className="auth-form__heading">
|
<div className="auth-form__heading">
|
||||||
|
@ -607,22 +595,6 @@ LoadingScreen.propTypes = {
|
||||||
message: PropTypes.string.isRequired,
|
message: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function Recaptcha({ message, sitekey, onChange }) {
|
|
||||||
return (
|
|
||||||
<ProcessWrapper>
|
|
||||||
<div style={{ marginBottom: 'var(--sp-normal)' }}>
|
|
||||||
<Text variant="s1" weight="medium">{message}</Text>
|
|
||||||
</div>
|
|
||||||
<ReCAPTCHA sitekey={sitekey} onChange={onChange} />
|
|
||||||
</ProcessWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Recaptcha.propTypes = {
|
|
||||||
message: PropTypes.string.isRequired,
|
|
||||||
sitekey: PropTypes.string.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
function Terms({ url, onSubmit }) {
|
function Terms({ url, onSubmit }) {
|
||||||
return (
|
return (
|
||||||
<ProcessWrapper>
|
<ProcessWrapper>
|
||||||
|
|
|
@ -1,13 +1,3 @@
|
||||||
import { UAParser } from 'ua-parser-js';
|
|
||||||
|
|
||||||
export const ua = () => UAParser(window.navigator.userAgent);
|
|
||||||
|
|
||||||
export const isMacOS = () => ua().os.name === 'Mac OS';
|
|
||||||
|
|
||||||
export const mobileOrTablet = (): boolean => {
|
export const mobileOrTablet = (): boolean => {
|
||||||
const userAgent = ua();
|
return window.navigator && window.navigator.maxTouchPoints > 0;
|
||||||
const { os, device } = userAgent;
|
|
||||||
if (device.type === 'mobile' || device.type === 'tablet') return true;
|
|
||||||
if (os.name === 'Android' || os.name === 'iOS') return true;
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,10 +13,6 @@ const copyFiles = {
|
||||||
src: 'node_modules/@matrix-org/olm/olm.wasm',
|
src: 'node_modules/@matrix-org/olm/olm.wasm',
|
||||||
dest: '',
|
dest: '',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
src: 'node_modules/pdfjs-dist/build/pdf.worker.min.js',
|
|
||||||
dest: '',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
src: '_redirects',
|
src: '_redirects',
|
||||||
dest: '',
|
dest: '',
|
||||||
|
@ -41,7 +37,7 @@ export default defineConfig({
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
base: "",
|
base: "",
|
||||||
server: {
|
server: {
|
||||||
port: 8080,
|
port: 8000,
|
||||||
host: true,
|
host: true,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
Loading…
Reference in a new issue