initial changes

This commit is contained in:
hippoz 2023-12-05 19:33:29 +02:00
parent 2a1bf4a42a
commit a2b8b127f4
Signed by: hippoz
GPG key ID: 56C4E02A85F2FBED
19 changed files with 2177 additions and 3102 deletions

4724
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -52,7 +52,6 @@
"linkifyjs": "4.0.2",
"matrix-js-sdk": "24.1.0",
"millify": "6.1.0",
"pdfjs-dist": "3.10.111",
"prismjs": "1.29.0",
"prop-types": "15.8.1",
"react": "17.0.2",
@ -63,7 +62,6 @@
"react-dnd-html5-backend": "15.1.3",
"react-dom": "17.0.2",
"react-error-boundary": "4.0.10",
"react-google-recaptcha": "2.1.0",
"react-modal": "3.16.1",
"react-range": "1.8.14",
"sanitize-html": "2.8.0",
@ -71,8 +69,7 @@
"slate-history": "0.93.0",
"slate-react": "0.98.4",
"tippy.js": "6.3.7",
"twemoji": "14.0.2",
"ua-parser-js": "1.0.35"
"twemoji": "14.0.2"
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
@ -103,4 +100,4 @@
"vite": "4.3.9",
"vite-plugin-static-copy": "0.13.0"
}
}
}

View file

@ -4,7 +4,7 @@
display: inline-flex;
width: 42px;
height: 42px;
border-radius: var(--bo-radius);
border-radius: 50%;
position: relative;
&__large {

View file

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import './ContextMenu.scss';
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 Button from '../button/Button';
@ -22,7 +22,7 @@ function ContextMenu({
return (
<Tippy
animation="scale-extreme"
animation="scale-subtle"
className="context-menu"
visible={isVisible}
onClickOutside={hideMenu}
@ -31,7 +31,7 @@ function ContextMenu({
interactive
arrow={false}
maxWidth={maxWidth}
duration={200}
duration={125}
>
{render(isVisible ? hideMenu : showMenu)}
</Tippy>

View file

@ -39,20 +39,23 @@
}
.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 {
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 {
0% {
transform: translateY(100px);
opacity: .5;
transform: scale(0.90);
opacity: .4;
}
100% {
transform: translateY(0);
transform: scale(1);
opacity: 1;
}
}

View file

@ -7,10 +7,13 @@ import { isInSameDay } from '../../../util/common';
function Time({ timestamp, fullTime }) {
const date = new Date(timestamp);
const formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT');
let formattedDate = formattedFullTime;
let formattedFullTime;
let formattedDate;
if (!fullTime) {
if (fullTime) {
formattedFullTime = dateFormat(date, 'dd mmmm yyyy, hh:MM TT');
formattedDate = formattedFullTime;
} else {
const compareDate = new Date();
const isToday = isInSameDay(date, compareDate);
compareDate.setDate(compareDate.getDate() - 1);

View file

@ -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,
},
]);

View file

@ -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>
);
}
);

View file

@ -1 +0,0 @@
export * from './PdfViewer';

View file

@ -30,7 +30,6 @@ import {
import * as css from './Editor.css';
import { BlockType, MarkType } from './types';
import { HeadingLevel } from './slate';
import { isMacOS } from '../../utils/user-agent';
import { KeySymbol } from '../../utils/key-symbol';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
@ -121,7 +120,7 @@ export function HeadingBlockButton() {
const level = headingLevel(editor);
const [open, setOpen] = useState(false);
const isActive = isBlockActive(editor, BlockType.Heading);
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
const modKey = 'Ctrl';
const handleMenuSelect = (selectedLevel: HeadingLevel) => {
setOpen(false);
@ -247,7 +246,7 @@ export function ExitFormatting({ tooltip }: ExitFormattingProps) {
export function Toolbar() {
const editor = useSlate();
const modKey = isMacOS() ? KeySymbol.Command : 'Ctrl';
const modKey = 'Ctrl';
const disableInline = isBlockActive(editor, BlockType.CodeBlock);
const canEscape = isAnyMarkActive(editor) || !isBlockActive(editor, BlockType.Paragraph);

View file

@ -1,13 +1,14 @@
@use '../../partials/text';
.people-selector {
width: 100%;
flex-grow: 1;
padding: var(--sp-extra-tight) var(--sp-normal);
display: flex;
align-items: center;
cursor: pointer;
&__container {
margin: var(--sp-extra-tight);
display: flex;
}

View file

@ -48,7 +48,6 @@
}
& .room-selector {
width: calc(100% - var(--sp-extra-tight));
@include dir.side(margin, auto, 0);
margin: var(--sp-ultra-tight);
}
}

View file

@ -29,7 +29,6 @@ import {
getFileNameExt,
mimeTypeToExt,
} from '../../../utils/mimeTypes';
import { PdfViewer } from '../../../components/Pdf-viewer';
import * as css from './styles.css';
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) {
const mx = useMatrixClient();
@ -260,9 +193,6 @@ export const FileContent = as<'div', FileContentProps>(
READABLE_EXT_TO_MIME_TYPE[getFileNameExt(body)]) && (
<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} />
</Box>
)

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, Suspense, lazy } from 'react';
import './Settings.scss';
import initMatrix from '../../../client/initMatrix';
@ -29,7 +29,6 @@ import KeywordNotification from '../../molecules/global-notification/KeywordNoti
import IgnoreUserList from '../../molecules/global-notification/IgnoreUserList';
import ProfileEditor from '../profile-editor/ProfileEditor';
import CrossSigning from './CrossSigning';
import KeyBackup from './KeyBackup';
import DeviceManage from './DeviceManage';
@ -45,8 +44,10 @@ import CinnySVG from '../../../../public/res/svg/cinny.svg';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import { useSetting } from '../../state/hooks/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() {
const [, updateState] = useState({});
@ -151,7 +152,7 @@ function AppearanceSection() {
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
title="Markdown formatting"
@ -302,7 +303,9 @@ function SecuritySection() {
<div className="settings-security">
<div className="settings-security__card">
<MenuHeader>Cross signing and backup</MenuHeader>
<CrossSigning />
<Suspense fallback={ <Text>Loading...</Text> }>
<CrossSigning />
</Suspense>
<KeyBackup />
</div>
<DeviceManage />

View file

@ -1,15 +1,21 @@
import React, { StrictMode } from 'react';
import React, { StrictMode, Suspense, lazy } from 'react';
import { Provider } from 'jotai';
import { isAuthenticated } from '../../client/state/auth';
import Auth from '../templates/auth/Auth';
import Client from '../templates/client/Client';
import Text from '../atoms/text/Text';
const Auth = lazy(() => import("../templates/auth/Auth"));
function App() {
return (
<StrictMode>
<Provider>{isAuthenticated() ? <Client /> : <Auth />}</Provider>
<Provider>{
isAuthenticated() ?
<Client /> :
<Suspense fallback={ <Text>Loading...</Text> }><Auth /></Suspense>}
</Provider>
</StrictMode>
);
}

View file

@ -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;
};

View file

@ -2,7 +2,6 @@
import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import './Auth.scss';
import ReCAPTCHA from 'react-google-recaptcha';
import { Formik } from 'formik';
import * as auth from '../../../client/action/auth';
@ -400,17 +399,6 @@ function Register({ registerInfo, loginFlow, baseUrl }) {
asyncProcess();
}, [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 [username, password] = getInputs();
const d = await auth.completeRegisterStage(baseUrl, username, password, {
@ -435,7 +423,7 @@ function Register({ registerInfo, loginFlow, baseUrl }) {
return (
<>
{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.email.identity' && <EmailVerify email={process.email} onContinue={handleEmailVerify} />}
<div className="auth-form__heading">
@ -607,22 +595,6 @@ LoadingScreen.propTypes = {
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 }) {
return (
<ProcessWrapper>

View file

@ -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 => {
const userAgent = ua();
const { os, device } = userAgent;
if (device.type === 'mobile' || device.type === 'tablet') return true;
if (os.name === 'Android' || os.name === 'iOS') return true;
return false;
return window.navigator && window.navigator.maxTouchPoints > 0;
};

View file

@ -13,10 +13,6 @@ const copyFiles = {
src: 'node_modules/@matrix-org/olm/olm.wasm',
dest: '',
},
{
src: 'node_modules/pdfjs-dist/build/pdf.worker.min.js',
dest: '',
},
{
src: '_redirects',
dest: '',
@ -41,7 +37,7 @@ export default defineConfig({
publicDir: false,
base: "",
server: {
port: 8080,
port: 8000,
host: true,
},
plugins: [
@ -53,16 +49,16 @@ export default defineConfig({
],
optimizeDeps: {
esbuildOptions: {
define: {
global: 'globalThis'
},
plugins: [
// Enable esbuild polyfill plugins
NodeGlobalsPolyfillPlugin({
process: false,
buffer: true,
}),
]
define: {
global: 'globalThis'
},
plugins: [
// Enable esbuild polyfill plugins
NodeGlobalsPolyfillPlugin({
process: false,
buffer: true,
}),
]
}
},
build: {