* 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
257 lines
8.5 KiB
TypeScript
257 lines
8.5 KiB
TypeScript
/* 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>
|
|
);
|
|
}
|
|
);
|