From 3a95d0da01c5c69d16fda6421188e1576ddae90b Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Fri, 6 Oct 2023 13:44:06 +1100 Subject: [PATCH] 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 --- .eslintrc.js | 3 + package-lock.json | 648 ++++++- package.json | 13 +- .../components/Pdf-viewer/PdfViewer.css.ts | 37 + src/app/components/Pdf-viewer/PdfViewer.tsx | 257 +++ src/app/components/Pdf-viewer/index.ts | 1 + src/app/components/editor/Editor.tsx | 5 +- src/app/components/editor/Elements.tsx | 12 +- .../autocomplete/RoomMentionAutocomplete.tsx | 7 +- src/app/components/emoji-board/EmojiBoard.tsx | 4 +- .../event-readers/EventReaders.css.ts | 21 + .../components/event-readers/EventReaders.tsx | 110 ++ src/app/components/event-readers/index.ts | 1 + .../image-viewer/ImageViewer.css.ts | 40 + .../components/image-viewer/ImageViewer.tsx | 95 + src/app/components/image-viewer/index.ts | 1 + src/app/components/media/Image.tsx | 9 + src/app/components/media/MediaControls.tsx | 27 + src/app/components/media/Video.tsx | 10 + src/app/components/media/index.ts | 3 + src/app/components/media/media.css.ts | 20 + .../message/MessageContentFallback.tsx | 66 + src/app/components/message/Reaction.css.ts | 75 + src/app/components/message/Reaction.tsx | 113 ++ src/app/components/message/Reply.css.ts | 25 + src/app/components/message/Reply.tsx | 100 + src/app/components/message/Time.tsx | 27 + .../message/attachment/Attachment.css.ts | 42 + .../message/attachment/Attachment.tsx | 44 + .../components/message/attachment/index.ts | 1 + src/app/components/message/index.ts | 7 + src/app/components/message/layout/Base.tsx | 25 + src/app/components/message/layout/Bubble.tsx | 18 + src/app/components/message/layout/Compact.tsx | 18 + src/app/components/message/layout/Modern.tsx | 18 + src/app/components/message/layout/index.ts | 4 + .../components/message/layout/layout.css.ts | 155 ++ .../placeholder/CompactPlaceholder.tsx | 22 + .../placeholder/DefaultPlaceholder.tsx | 25 + .../placeholder/LinePlaceholder.css.ts | 12 + .../message/placeholder/LinePlaceholder.tsx | 8 + .../components/message/placeholder/index.ts | 3 + src/app/components/room-intro/RoomIntro.tsx | 114 ++ src/app/components/room-intro/index.ts | 1 + .../components/text-viewer/TextViewer.css.ts | 37 + src/app/components/text-viewer/TextViewer.tsx | 69 + src/app/components/text-viewer/index.ts | 1 + .../typing-indicator/TypingIndicator.css.ts | 49 + .../typing-indicator/TypingIndicator.tsx | 21 + src/app/components/typing-indicator/index.ts | 1 + src/app/hooks/media/index.ts | 6 + src/app/hooks/media/useMediaLoading.ts | 51 + src/app/hooks/media/useMediaPlay.ts | 46 + .../hooks/media/useMediaPlayTimeCallback.ts | 24 + src/app/hooks/media/useMediaPlaybackRate.ts | 40 + src/app/hooks/media/useMediaSeek.ts | 51 + src/app/hooks/media/useMediaVolume.ts | 60 + src/app/hooks/useIntersectionObserver.ts | 2 + src/app/hooks/useMatrixEventRenderer.ts | 80 + src/app/hooks/useMemberEventParser.tsx | 218 +++ src/app/hooks/usePan.ts | 62 + src/app/hooks/usePowerLevels.ts | 43 +- src/app/hooks/useRelations.ts | 25 + src/app/hooks/useResizeObserver.ts | 2 + src/app/hooks/useRoomEventReaders.ts | 35 + src/app/hooks/useRoomLatestEvent.ts | 29 + src/app/hooks/useRoomMsgContentRenderer.ts | 68 + src/app/hooks/useVirtualPaginator.ts | 405 ++++ src/app/hooks/useZoom.ts | 26 + src/app/organisms/room/MembersDrawer.tsx | 33 +- src/app/organisms/room/Room.jsx | 79 - src/app/organisms/room/Room.tsx | 46 + src/app/organisms/room/RoomInput.tsx | 31 +- src/app/organisms/room/RoomTimeline.css.ts | 30 + src/app/organisms/room/RoomTimeline.tsx | 1689 +++++++++++++++++ src/app/organisms/room/RoomView.jsx | 39 +- .../organisms/room/RoomViewFollowing.css.ts | 31 + src/app/organisms/room/RoomViewFollowing.tsx | 141 ++ src/app/organisms/room/RoomViewTyping.css.ts | 24 + src/app/organisms/room/RoomViewTyping.tsx | 102 + .../organisms/room/message/AudioContent.tsx | 192 ++ .../room/message/EncryptedContent.tsx | 22 + .../organisms/room/message/EventContent.tsx | 37 + .../organisms/room/message/FileContent.tsx | 250 +++ src/app/organisms/room/message/FileHeader.tsx | 22 + .../organisms/room/message/ImageContent.tsx | 170 ++ src/app/organisms/room/message/Message.tsx | 993 ++++++++++ src/app/organisms/room/message/Reactions.tsx | 133 ++ .../organisms/room/message/StickerContent.tsx | 41 + .../organisms/room/message/VideoContent.tsx | 176 ++ .../organisms/room/message/fileRenderer.tsx | 45 + src/app/organisms/room/message/index.ts | 10 + src/app/organisms/room/message/styles.css.ts | 72 + src/app/organisms/room/message/util.ts | 23 + src/app/organisms/room/msgContent.ts | 30 +- .../reaction-viewer/ReactionViewer.css.ts | 31 + .../room/reaction-viewer/ReactionViewer.tsx | 155 ++ .../organisms/room/reaction-viewer/index.ts | 1 + src/app/organisms/settings/Settings.jsx | 84 +- src/app/plugins/emoji.ts | 14 +- src/app/plugins/pdfjs-dist.ts | 47 + src/app/plugins/react-custom-html-parser.tsx | 274 +++ src/app/plugins/react-prism/ReactPrism.css | 97 + src/app/plugins/react-prism/ReactPrism.tsx | 35 + src/app/state/settings.ts | 17 +- src/app/state/typingMembers.ts | 70 + .../CustomHtml.css.ts} | 86 +- src/app/templates/client/Client.jsx | 21 +- src/app/templates/client/ClientContent.jsx | 49 + src/app/utils/blurHash.ts | 8 +- src/app/utils/common.ts | 47 + src/app/utils/dom.ts | 59 +- src/app/utils/matrix.ts | 50 +- src/app/utils/mimeTypes.ts | 65 +- src/app/utils/room.ts | 34 + src/app/utils/sanitize.ts | 142 ++ src/app/utils/time.ts | 35 + src/client/state/settings.js | 3 + src/ext.d.ts | 5 + src/index.scss | 16 +- src/types/matrix/common.ts | 60 +- src/types/matrix/room.ts | 27 + tsconfig.json | 1 + vite.config.js | 4 + 124 files changed, 9438 insertions(+), 258 deletions(-) create mode 100644 src/app/components/Pdf-viewer/PdfViewer.css.ts create mode 100644 src/app/components/Pdf-viewer/PdfViewer.tsx create mode 100644 src/app/components/Pdf-viewer/index.ts create mode 100644 src/app/components/event-readers/EventReaders.css.ts create mode 100644 src/app/components/event-readers/EventReaders.tsx create mode 100644 src/app/components/event-readers/index.ts create mode 100644 src/app/components/image-viewer/ImageViewer.css.ts create mode 100644 src/app/components/image-viewer/ImageViewer.tsx create mode 100644 src/app/components/image-viewer/index.ts create mode 100644 src/app/components/media/Image.tsx create mode 100644 src/app/components/media/MediaControls.tsx create mode 100644 src/app/components/media/Video.tsx create mode 100644 src/app/components/media/index.ts create mode 100644 src/app/components/media/media.css.ts create mode 100644 src/app/components/message/MessageContentFallback.tsx create mode 100644 src/app/components/message/Reaction.css.ts create mode 100644 src/app/components/message/Reaction.tsx create mode 100644 src/app/components/message/Reply.css.ts create mode 100644 src/app/components/message/Reply.tsx create mode 100644 src/app/components/message/Time.tsx create mode 100644 src/app/components/message/attachment/Attachment.css.ts create mode 100644 src/app/components/message/attachment/Attachment.tsx create mode 100644 src/app/components/message/attachment/index.ts create mode 100644 src/app/components/message/index.ts create mode 100644 src/app/components/message/layout/Base.tsx create mode 100644 src/app/components/message/layout/Bubble.tsx create mode 100644 src/app/components/message/layout/Compact.tsx create mode 100644 src/app/components/message/layout/Modern.tsx create mode 100644 src/app/components/message/layout/index.ts create mode 100644 src/app/components/message/layout/layout.css.ts create mode 100644 src/app/components/message/placeholder/CompactPlaceholder.tsx create mode 100644 src/app/components/message/placeholder/DefaultPlaceholder.tsx create mode 100644 src/app/components/message/placeholder/LinePlaceholder.css.ts create mode 100644 src/app/components/message/placeholder/LinePlaceholder.tsx create mode 100644 src/app/components/message/placeholder/index.ts create mode 100644 src/app/components/room-intro/RoomIntro.tsx create mode 100644 src/app/components/room-intro/index.ts create mode 100644 src/app/components/text-viewer/TextViewer.css.ts create mode 100644 src/app/components/text-viewer/TextViewer.tsx create mode 100644 src/app/components/text-viewer/index.ts create mode 100644 src/app/components/typing-indicator/TypingIndicator.css.ts create mode 100644 src/app/components/typing-indicator/TypingIndicator.tsx create mode 100644 src/app/components/typing-indicator/index.ts create mode 100644 src/app/hooks/media/index.ts create mode 100644 src/app/hooks/media/useMediaLoading.ts create mode 100644 src/app/hooks/media/useMediaPlay.ts create mode 100644 src/app/hooks/media/useMediaPlayTimeCallback.ts create mode 100644 src/app/hooks/media/useMediaPlaybackRate.ts create mode 100644 src/app/hooks/media/useMediaSeek.ts create mode 100644 src/app/hooks/media/useMediaVolume.ts create mode 100644 src/app/hooks/useMatrixEventRenderer.ts create mode 100644 src/app/hooks/useMemberEventParser.tsx create mode 100644 src/app/hooks/usePan.ts create mode 100644 src/app/hooks/useRelations.ts create mode 100644 src/app/hooks/useRoomEventReaders.ts create mode 100644 src/app/hooks/useRoomLatestEvent.ts create mode 100644 src/app/hooks/useRoomMsgContentRenderer.ts create mode 100644 src/app/hooks/useVirtualPaginator.ts create mode 100644 src/app/hooks/useZoom.ts delete mode 100644 src/app/organisms/room/Room.jsx create mode 100644 src/app/organisms/room/Room.tsx create mode 100644 src/app/organisms/room/RoomTimeline.css.ts create mode 100644 src/app/organisms/room/RoomTimeline.tsx create mode 100644 src/app/organisms/room/RoomViewFollowing.css.ts create mode 100644 src/app/organisms/room/RoomViewFollowing.tsx create mode 100644 src/app/organisms/room/RoomViewTyping.css.ts create mode 100644 src/app/organisms/room/RoomViewTyping.tsx create mode 100644 src/app/organisms/room/message/AudioContent.tsx create mode 100644 src/app/organisms/room/message/EncryptedContent.tsx create mode 100644 src/app/organisms/room/message/EventContent.tsx create mode 100644 src/app/organisms/room/message/FileContent.tsx create mode 100644 src/app/organisms/room/message/FileHeader.tsx create mode 100644 src/app/organisms/room/message/ImageContent.tsx create mode 100644 src/app/organisms/room/message/Message.tsx create mode 100644 src/app/organisms/room/message/Reactions.tsx create mode 100644 src/app/organisms/room/message/StickerContent.tsx create mode 100644 src/app/organisms/room/message/VideoContent.tsx create mode 100644 src/app/organisms/room/message/fileRenderer.tsx create mode 100644 src/app/organisms/room/message/index.ts create mode 100644 src/app/organisms/room/message/styles.css.ts create mode 100644 src/app/organisms/room/message/util.ts create mode 100644 src/app/organisms/room/reaction-viewer/ReactionViewer.css.ts create mode 100644 src/app/organisms/room/reaction-viewer/ReactionViewer.tsx create mode 100644 src/app/organisms/room/reaction-viewer/index.ts create mode 100644 src/app/plugins/pdfjs-dist.ts create mode 100644 src/app/plugins/react-custom-html-parser.tsx create mode 100644 src/app/plugins/react-prism/ReactPrism.css create mode 100644 src/app/plugins/react-prism/ReactPrism.tsx create mode 100644 src/app/state/typingMembers.ts rename src/app/{components/editor/Elements.css.ts => styles/CustomHtml.css.ts} (59%) create mode 100644 src/app/templates/client/ClientContent.jsx create mode 100644 src/app/utils/time.ts diff --git a/.eslintrc.js b/.eslintrc.js index 7043741..36101fb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,6 +20,9 @@ module.exports = { ecmaVersion: 'latest', sourceType: 'module', }, + "globals": { + JSX: "readonly" + }, plugins: [ 'react', '@typescript-eslint' diff --git a/package-lock.json b/package-lock.json index d5867ae..6f00efe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,22 +23,26 @@ "browser-encrypt-attachment": "0.3.0", "classnames": "2.3.2", "dateformat": "5.0.3", + "dayjs": "1.11.10", "emojibase": "6.1.0", "emojibase-data": "7.0.1", "file-saver": "2.0.5", "flux": "4.0.3", "focus-trap-react": "10.0.2", - "folds": "1.3.0", + "folds": "1.5.0", "formik": "2.2.9", - "html-react-parser": "3.0.4", + "html-react-parser": "4.2.0", "immer": "9.0.16", "is-hotkey": "0.2.0", "jotai": "1.12.0", "katex": "0.16.4", "linkify-html": "4.0.2", + "linkify-react": "4.1.1", "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", "react-autosize-textarea": "7.1.0", @@ -46,8 +50,10 @@ "react-dnd": "15.1.2", "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", "slate": "0.90.0", "slate-history": "0.93.0", @@ -60,9 +66,12 @@ "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rollup/plugin-inject": "5.0.3", "@rollup/plugin-wasm": "6.1.1", + "@types/file-saver": "2.0.5", "@types/node": "18.11.18", + "@types/prismjs": "1.26.0", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", + "@types/sanitize-html": "2.9.0", "@types/ua-parser-js": "0.7.36", "@typescript-eslint/eslint-plugin": "5.46.1", "@typescript-eslint/parser": "5.46.1", @@ -963,6 +972,41 @@ "react-dom": "16.14.0" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@matrix-org/matrix-sdk-crypto-js": { "version": "0.1.0-alpha.5", "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.5.tgz", @@ -1155,6 +1199,12 @@ "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" }, + "node_modules/@types/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==", + "dev": true + }, "node_modules/@types/is-hotkey": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.7.tgz", @@ -1183,6 +1233,12 @@ "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", "dev": true }, + "node_modules/@types/prismjs": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.0.tgz", + "integrity": "sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==", + "dev": true + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -1212,6 +1268,15 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, + "node_modules/@types/sanitize-html": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.9.0.tgz", + "integrity": "sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg==", + "dev": true, + "dependencies": { + "htmlparser2": "^8.0.0" + } + }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -1644,6 +1709,12 @@ "vite": "^4.0.0" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, "node_modules/acorn": { "version": "8.8.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", @@ -1665,6 +1736,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ahocorasick": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/ahocorasick/-/ahocorasick-1.0.2.tgz", @@ -1723,6 +1806,25 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1862,7 +1964,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "devOptional": true }, "node_modules/base-x": { "version": "4.0.0", @@ -1907,7 +2009,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2026,6 +2128,21 @@ } ] }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -2078,6 +2195,15 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, "node_modules/classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -2109,6 +2235,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/compute-scroll-into-view": { "version": "1.0.20", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", @@ -2123,7 +2258,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "devOptional": true }, "node_modules/confusing-browser-globals": { "version": "1.0.11", @@ -2131,6 +2266,12 @@ "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", "dev": true }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, "node_modules/content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", @@ -2218,6 +2359,11 @@ "node": ">=12.20" } }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2234,6 +2380,18 @@ } } }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2269,6 +2427,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2354,13 +2527,13 @@ } }, "node_modules/domutils": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", - "domhandler": "^5.0.1" + "domhandler": "^5.0.3" }, "funding": { "url": "https://github.com/fb55/domutils?sponsor=1" @@ -2399,9 +2572,9 @@ } }, "node_modules/entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "engines": { "node": ">=0.12" }, @@ -3215,21 +3388,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/flatted": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", @@ -3271,9 +3429,9 @@ } }, "node_modules/folds": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/folds/-/folds-1.3.0.tgz", - "integrity": "sha512-Jcv6xN9woJWaTaATDGCD9xFqUhjuSw+afvChYoUt4UsAyY351hfpkGNYzglN+gA5fvJw6N9oa6Ogjj2p84kFfA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/folds/-/folds-1.5.0.tgz", + "integrity": "sha512-1QNHzD57OxFZT5SOe0nWcrKQvWmfMRv1f5sTF8xhGtwx9rajjv36T9SwCcj9Fh58PbERqOdBiwvpdhu+BQTVjg==", "peerDependencies": { "@vanilla-extract/css": "^1.9.2", "@vanilla-extract/recipes": "^0.3.0", @@ -3326,11 +3484,35 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "devOptional": true }, "node_modules/fsevents": { "version": "2.3.2", @@ -3379,6 +3561,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3429,7 +3631,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, + "devOptional": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3576,6 +3778,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -3585,23 +3793,41 @@ } }, "node_modules/html-dom-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-3.1.2.tgz", - "integrity": "sha512-mLTtl3pVn3HnqZSZzW3xVs/mJAKrG1yIw3wlp+9bdoZHHLaBRvELdpfShiPVLyjPypq1Fugv2KMDoGHW4lVXnw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-4.0.0.tgz", + "integrity": "sha512-TUa3wIwi80f5NF8CVWzkopBVqVAtlawUzJoLwVLHns0XSJGynss4jiY0mTWpiDOsuyw+afP+ujjMgRh9CoZcXw==", "dependencies": { "domhandler": "5.0.3", - "htmlparser2": "8.0.1" + "htmlparser2": "9.0.0" + } + }, + "node_modules/html-dom-parser/node_modules/htmlparser2": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.0.0.tgz", + "integrity": "sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" } }, "node_modules/html-react-parser": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-3.0.4.tgz", - "integrity": "sha512-va68PSmC7uA6PbOEc9yuw5Mu3OHPXmFKUpkLGvUPdTuNrZ0CJZk1s/8X/FaHjswK/6uZghu2U02tJjussT8+uw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-4.2.0.tgz", + "integrity": "sha512-gzU55AS+FI6qD7XaKe5BLuLFM2Xw0/LodfMWZlxV9uOHe7LCD5Lukx/EgYuBI3c0kLu0XlgFXnSzO0qUUn3Vrg==", "dependencies": { "domhandler": "5.0.3", - "html-dom-parser": "3.1.2", + "html-dom-parser": "4.0.0", "react-property": "2.0.0", - "style-to-js": "1.1.1" + "style-to-js": "1.1.3" }, "peerDependencies": { "react": "0.14 || 15 || 16 || 17 || 18" @@ -3625,6 +3851,19 @@ "entities": "^4.3.0" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -3698,7 +3937,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, + "devOptional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -3708,7 +3947,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "devOptional": true }, "node_modules/inline-style-parser": { "version": "0.1.1", @@ -4192,6 +4431,15 @@ "linkifyjs": "^4.0.0" } }, + "node_modules/linkify-react": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/linkify-react/-/linkify-react-4.1.1.tgz", + "integrity": "sha512-2K9Y1cUdvq40dFWqCJ//X+WP19nlzIVITFGI93RjLnA0M7KbnxQ/ffC3AZIZaEIrLangF9Hjt3i0GQ9/anEG5A==", + "peerDependencies": { + "linkifyjs": "^4.0.0", + "react": ">= 15.0.0" + } + }, "node_modules/linkifyjs": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.0.2.tgz", @@ -4254,7 +4502,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -4274,6 +4522,21 @@ "node": ">=12" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/matrix-events-sdk": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", @@ -4351,6 +4614,18 @@ "millify": "bin/millify" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mini-svg-data-uri": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", @@ -4364,7 +4639,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4381,11 +4656,63 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", @@ -4439,6 +4766,21 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==" }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4448,6 +4790,18 @@ "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4557,7 +4911,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, + "devOptional": true, "dependencies": { "wrappy": "1" } @@ -4653,7 +5007,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -4682,6 +5036,27 @@ "node": ">=8" } }, + "node_modules/path2d-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pdfjs-dist": { + "version": "3.10.111", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.10.111.tgz", + "integrity": "sha512-+SXXGN/3YTNQSK5Ae7EyqQuR+4IAsNunJq/Us5ByOkRJ45qBXXOwkiWi3RIDU+CyF+ak5eSWXl2FQW2PKBrsRA==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -4778,6 +5153,14 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", @@ -4922,6 +5305,17 @@ "react": "17.0.2" } }, + "node_modules/react-error-boundary": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.10.tgz", + "integrity": "sha512-pvVKdi77j2OoPHo+p3rorgE43OjDWiqFkaqkJz8sJKK6uf/u8xtzuaVfj5qJ2JnDLIgF1De3zY5AJDijp+LVPA==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-fast-compare": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", @@ -4972,6 +5366,15 @@ "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz", "integrity": "sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw==" }, + "node_modules/react-range": { + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/react-range/-/react-range-1.8.14.tgz", + "integrity": "sha512-v2nyD5106rHf9dwHzq+WRlhCes83h1wJRHIMFjbZsYYsO6LF4mG/mR3cH7Cf+dkeHq65DItuqIbLn/3jjYjsHg==", + "peerDependencies": { + "react": "^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0", + "react-dom": "^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0" + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -4981,6 +5384,20 @@ "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -5090,6 +5507,21 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "devOptional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/rollup": { "version": "3.25.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz", @@ -5129,6 +5561,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, "node_modules/safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -5225,6 +5677,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -5265,6 +5723,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -5329,6 +5824,15 @@ "node": ">=0.10.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5427,17 +5931,17 @@ } }, "node_modules/style-to-js": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.1.tgz", - "integrity": "sha512-RJ18Z9t2B02sYhZtfWKQq5uplVctgvjTfLWT7+Eb1zjUjIrWzX5SdlkwLGQozrqarTmEzJJ/YmdNJCUNI47elg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.3.tgz", + "integrity": "sha512-zKI5gN/zb7LS/Vm0eUwjmjrXWw8IMtyA8aPBJZdYiQTXj4+wQ3IucOLIOnF7zCHxvW8UhIGh/uZh/t9zEHXNTQ==", "dependencies": { - "style-to-object": "0.3.0" + "style-to-object": "0.4.1" } }, "node_modules/style-to-object": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", - "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.1.tgz", + "integrity": "sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==", "dependencies": { "inline-style-parser": "0.1.1" } @@ -5470,6 +5974,23 @@ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.0.1.tgz", "integrity": "sha512-SYJSIgeyXW7EuX1ytdneO5e8jip42oHWg9xl/o3oTYhmXusZVgiA+VlPvjIN+kHii9v90AmzTZEBcsEvuAY+TA==" }, + "node_modules/tar": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", + "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5696,6 +6217,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "optional": true + }, "node_modules/uuid": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", @@ -6247,6 +6774,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -6306,7 +6842,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "devOptional": true }, "node_modules/y18n": { "version": "5.0.8", @@ -6320,7 +6856,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "devOptional": true }, "node_modules/yaml": { "version": "1.10.2", diff --git a/package.json b/package.json index 5eb3fa9..83850a8 100644 --- a/package.json +++ b/package.json @@ -33,22 +33,26 @@ "browser-encrypt-attachment": "0.3.0", "classnames": "2.3.2", "dateformat": "5.0.3", + "dayjs": "1.11.10", "emojibase": "6.1.0", "emojibase-data": "7.0.1", "file-saver": "2.0.5", "flux": "4.0.3", "focus-trap-react": "10.0.2", - "folds": "1.3.0", + "folds": "1.5.0", "formik": "2.2.9", - "html-react-parser": "3.0.4", + "html-react-parser": "4.2.0", "immer": "9.0.16", "is-hotkey": "0.2.0", "jotai": "1.12.0", "katex": "0.16.4", "linkify-html": "4.0.2", + "linkify-react": "4.1.1", "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", "react-autosize-textarea": "7.1.0", @@ -56,8 +60,10 @@ "react-dnd": "15.1.2", "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", "slate": "0.90.0", "slate-history": "0.93.0", @@ -70,9 +76,12 @@ "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rollup/plugin-inject": "5.0.3", "@rollup/plugin-wasm": "6.1.1", + "@types/file-saver": "2.0.5", "@types/node": "18.11.18", + "@types/prismjs": "1.26.0", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", + "@types/sanitize-html": "2.9.0", "@types/ua-parser-js": "0.7.36", "@typescript-eslint/eslint-plugin": "5.46.1", "@typescript-eslint/parser": "5.46.1", diff --git a/src/app/components/Pdf-viewer/PdfViewer.css.ts b/src/app/components/Pdf-viewer/PdfViewer.css.ts new file mode 100644 index 0000000..4655109 --- /dev/null +++ b/src/app/components/Pdf-viewer/PdfViewer.css.ts @@ -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, + }, +]); diff --git a/src/app/components/Pdf-viewer/PdfViewer.tsx b/src/app/components/Pdf-viewer/PdfViewer.tsx new file mode 100644 index 0000000..c440cce --- /dev/null +++ b/src/app/components/Pdf-viewer/PdfViewer.tsx @@ -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(null); + const scrollRef = useRef(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 = (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 ( + +
+ + + + + + {name} + + + + + + + setZoom(zoom === 1 ? 2 : 1)}> + {Math.round(zoom * 100)}% + + 1 ? 'Success' : 'SurfaceVariant'} + outlined={zoom > 1} + size="300" + radii="Pill" + onClick={zoomIn} + aria-label="Zoom In" + > + + + } + > + Download + + +
+ + {isLoading && } + {isError && ( + <> + Failed to load PDF + + + )} + {docState.status === AsyncStatus.Success && ( + + +
+ + + )} + + {docState.status === AsyncStatus.Success && ( +
+ } + onClick={handlePrevPage} + aria-disabled={pageNo <= 1} + > + Previous + + + setOpenJump(false), + clickOutsideDeactivates: true, + }} + > + + + + + + + + } + > + {(anchorRef) => ( + setOpenJump(!openJump)} + ref={anchorRef} + variant="SurfaceVariant" + radii="300" + aria-pressed={openJump} + > + {`${pageNo}/${docState.data.numPages}`} + + )} + + + } + onClick={handleNextPage} + aria-disabled={pageNo >= docState.data.numPages} + > + Next + +
+ )} + + ); + } +); diff --git a/src/app/components/Pdf-viewer/index.ts b/src/app/components/Pdf-viewer/index.ts new file mode 100644 index 0000000..5fc0566 --- /dev/null +++ b/src/app/components/Pdf-viewer/index.ts @@ -0,0 +1 @@ +export * from './PdfViewer'; diff --git a/src/app/components/editor/Editor.tsx b/src/app/components/editor/Editor.tsx index f4241e0..e5377f2 100644 --- a/src/app/components/editor/Editor.tsx +++ b/src/app/components/editor/Editor.tsx @@ -54,7 +54,7 @@ export const useEditor = (): Editor => { return editor; }; -export type EditorChangeHandler = ((value: Descendant[]) => void) | undefined; +export type EditorChangeHandler = (value: Descendant[]) => void; type CustomEditorProps = { top?: ReactNode; bottom?: ReactNode; @@ -64,6 +64,7 @@ type CustomEditorProps = { editor: Editor; placeholder?: string; onKeyDown?: KeyboardEventHandler; + onKeyUp?: KeyboardEventHandler; onChange?: EditorChangeHandler; onPaste?: ClipboardEventHandler; }; @@ -78,6 +79,7 @@ export const CustomEditor = forwardRef( editor, placeholder, onKeyDown, + onKeyUp, onChange, onPaste, }, @@ -141,6 +143,7 @@ export const CustomEditor = forwardRef( renderElement={renderElement} renderLeaf={renderLeaf} onKeyDown={handleKeydown} + onKeyUp={onKeyUp} onPaste={onPaste} /> diff --git a/src/app/components/editor/Elements.tsx b/src/app/components/editor/Elements.tsx index 59893e5..2df8099 100644 --- a/src/app/components/editor/Elements.tsx +++ b/src/app/components/editor/Elements.tsx @@ -2,7 +2,7 @@ import { Scroll, Text } from 'folds'; import React from '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 { useMatrixClient } from '../../hooks/useMatrixClient'; @@ -145,7 +145,13 @@ export function RenderElement({ attributes, element, children }: RenderElementPr case BlockType.CodeBlock: return ( - +
{children}
@@ -242,7 +248,7 @@ export function RenderLeaf({ attributes, leaf, children }: RenderLeafProps) { ); if (leaf.spoiler) child = ( - + {child} diff --git a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx index 6bea195..baa217c 100644 --- a/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/RoomMentionAutocomplete.tsx @@ -122,8 +122,9 @@ export function RoomMentionAutocomplete({ return; } const rId = autoCompleteRoomIds[0]; - const name = mx.getRoom(rId)?.name ?? rId; - handleAutocomplete(rId, name); + const r = mx.getRoom(rId); + const name = r?.name ?? rId; + handleAutocomplete(r?.getCanonicalAlias() ?? rId, name); }); }); @@ -147,7 +148,7 @@ export function RoomMentionAutocomplete({ onKeyDown={(evt: ReactKeyboardEvent) => onTabPress(evt, () => handleAutocomplete(rId, room.name)) } - onClick={() => handleAutocomplete(rId, room.name)} + onClick={() => handleAutocomplete(room.getCanonicalAlias() ?? rId, room.name)} after={ {room.getCanonicalAlias() ?? ''} diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index 76c6f05..a730983 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -42,7 +42,7 @@ import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useRecentEmoji } from '../../hooks/useRecentEmoji'; import { ExtendedPackImage, ImagePack, PackUsage } from '../../plugins/custom-emoji'; 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 { useDebounce } from '../../hooks/useDebounce'; import { useThrottle } from '../../hooks/useThrottle'; @@ -675,7 +675,7 @@ export function EmojiBoard({ const targetEl = contentScrollRef.current; if (!targetEl) return; 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; setActiveGroupId(groupId); }, [setActiveGroupId]); diff --git a/src/app/components/event-readers/EventReaders.css.ts b/src/app/components/event-readers/EventReaders.css.ts new file mode 100644 index 0000000..36f47b5 --- /dev/null +++ b/src/app/components/event-readers/EventReaders.css.ts @@ -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, +}); diff --git a/src/app/components/event-readers/EventReaders.tsx b/src/app/components/event-readers/EventReaders.tsx new file mode 100644 index 0000000..c05efc5 --- /dev/null +++ b/src/app/components/event-readers/EventReaders.tsx @@ -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 ( + +
+ + Seen by + + + + +
+ + + + {followingMembers.map((member) => { + const name = getName(member); + const avatarUrl = member.getAvatarUrl( + mx.baseUrl, + 100, + 100, + 'crop', + undefined, + false + ); + + return ( + { + requestClose(); + openProfileViewer(member.userId, room.roomId); + }} + before={ + + {avatarUrl ? ( + + ) : ( + + {name[0]} + + )} + + } + > + + {name} + + + ); + })} + + + +
+ ); + } +); diff --git a/src/app/components/event-readers/index.ts b/src/app/components/event-readers/index.ts new file mode 100644 index 0000000..8a37548 --- /dev/null +++ b/src/app/components/event-readers/index.ts @@ -0,0 +1 @@ +export * from './EventReaders'; diff --git a/src/app/components/image-viewer/ImageViewer.css.ts b/src/app/components/image-viewer/ImageViewer.css.ts new file mode 100644 index 0000000..fc2f508 --- /dev/null +++ b/src/app/components/image-viewer/ImageViewer.css.ts @@ -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', + }, +]); diff --git a/src/app/components/image-viewer/ImageViewer.tsx b/src/app/components/image-viewer/ImageViewer.tsx new file mode 100644 index 0000000..4fd06b7 --- /dev/null +++ b/src/app/components/image-viewer/ImageViewer.tsx @@ -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 ( + +
+ + + + + + {alt} + + + + + + + setZoom(zoom === 1 ? 2 : 1)}> + {Math.round(zoom * 100)}% + + 1 ? 'Success' : 'SurfaceVariant'} + outlined={zoom > 1} + size="300" + radii="Pill" + onClick={zoomIn} + aria-label="Zoom In" + > + + + } + > + Download + + +
+ + {alt} + +
+ ); + } +); diff --git a/src/app/components/image-viewer/index.ts b/src/app/components/image-viewer/index.ts new file mode 100644 index 0000000..69943f2 --- /dev/null +++ b/src/app/components/image-viewer/index.ts @@ -0,0 +1 @@ +export * from './ImageViewer'; diff --git a/src/app/components/media/Image.tsx b/src/app/components/media/Image.tsx new file mode 100644 index 0000000..dda21a5 --- /dev/null +++ b/src/app/components/media/Image.tsx @@ -0,0 +1,9 @@ +import React, { ImgHTMLAttributes, forwardRef } from 'react'; +import classNames from 'classnames'; +import * as css from './media.css'; + +export const Image = forwardRef>( + ({ className, alt, ...props }, ref) => ( + {alt} + ) +); diff --git a/src/app/components/media/MediaControls.tsx b/src/app/components/media/MediaControls.tsx new file mode 100644 index 0000000..95a344a --- /dev/null +++ b/src/app/components/media/MediaControls.tsx @@ -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) => ( + + {before && {before}} + + + {leftControl} + + + + {rightControl} + + + {after && {after}} + {children} + + ) +); diff --git a/src/app/components/media/Video.tsx b/src/app/components/media/Video.tsx new file mode 100644 index 0000000..ab13c5b --- /dev/null +++ b/src/app/components/media/Video.tsx @@ -0,0 +1,10 @@ +import React, { VideoHTMLAttributes, forwardRef } from 'react'; +import classNames from 'classnames'; +import * as css from './media.css'; + +export const Video = forwardRef>( + ({ className, ...props }, ref) => ( + // eslint-disable-next-line jsx-a11y/media-has-caption +
- +
- + ); diff --git a/src/app/templates/client/ClientContent.jsx b/src/app/templates/client/ClientContent.jsx new file mode 100644 index 0000000..ada7008 --- /dev/null +++ b/src/app/templates/client/ClientContent.jsx @@ -0,0 +1,49 @@ +import React, { useState, useEffect } from 'react'; + +import initMatrix from '../../../client/initMatrix'; +import cons from '../../../client/state/cons'; +import navigation from '../../../client/state/navigation'; +import { openNavigation } from '../../../client/action/navigation'; + +import Welcome from '../../organisms/welcome/Welcome'; +import { RoomBaseView } from '../../organisms/room/Room'; + +export function ClientContent() { + const [roomInfo, setRoomInfo] = useState({ + room: null, + eventId: null, + }); + + const mx = initMatrix.matrixClient; + + useEffect(() => { + const handleRoomSelected = (rId, pRoomId, eId) => { + roomInfo.roomTimeline?.removeInternalListeners(); + const r = mx.getRoom(rId); + if (r) { + setRoomInfo({ + room: r, + eventId: eId ?? null, + }); + } else { + setRoomInfo({ + room: null, + eventId: null, + }); + } + }; + + navigation.on(cons.events.navigation.ROOM_SELECTED, handleRoomSelected); + return () => { + navigation.removeListener(cons.events.navigation.ROOM_SELECTED, handleRoomSelected); + }; + }, [roomInfo, mx]); + + const { room, eventId } = roomInfo; + if (!room) { + setTimeout(() => openNavigation()); + return ; + } + + return ; +} diff --git a/src/app/utils/blurHash.ts b/src/app/utils/blurHash.ts index 0de5a92..3fe1ade 100644 --- a/src/app/utils/blurHash.ts +++ b/src/app/utils/blurHash.ts @@ -1,15 +1,15 @@ import { encode } from 'blurhash'; -export const MATRIX_BLUR_HASH_PROPERTY_NAME = 'xyz.amorgan.blurhash'; - export const encodeBlurHash = ( img: HTMLImageElement | HTMLVideoElement, width?: number, height?: number ): string | undefined => { + const imgWidth = img instanceof HTMLVideoElement ? img.videoWidth : img.width; + const imgHeight = img instanceof HTMLVideoElement ? img.videoHeight : img.height; const canvas = document.createElement('canvas'); - canvas.width = width || img.width; - canvas.height = height || img.height; + canvas.width = width || imgWidth; + canvas.height = height || imgHeight; const context = canvas.getContext('2d'); if (!context) return undefined; diff --git a/src/app/utils/common.ts b/src/app/utils/common.ts index d3804ae..e007f22 100644 --- a/src/app/utils/common.ts +++ b/src/app/utils/common.ts @@ -11,6 +11,19 @@ export const bytesToSize = (bytes: number): string => { return `${(bytes / 1000 ** sizeIndex).toFixed(1)} ${sizes[sizeIndex]}`; }; +export const millisecondsToMinutesAndSeconds = (milliseconds: number): string => { + const seconds = Math.floor(milliseconds / 1000); + const mm = Math.floor(seconds / 60); + const ss = Math.round(seconds % 60); + return `${mm}:${ss < 10 ? '0' : ''}${ss}`; +}; + +export const secondsToMinutesAndSeconds = (seconds: number): string => { + const mm = Math.floor(seconds / 60); + const ss = Math.round(seconds % 60); + return `${mm}:${ss < 10 ? '0' : ''}${ss}`; +}; + export const getFileTypeIcon = (icons: Record, fileType: string): IconSrc => { const type = fileType.toLowerCase(); if (type.startsWith('audio')) { @@ -30,3 +43,37 @@ export const fulfilledPromiseSettledResult = (prs: PromiseSettledResult[]) if (pr.status === 'fulfilled') values.push(pr.value); return values; }, []); + +export const binarySearch = (items: T[], match: (item: T) => -1 | 0 | 1): T | undefined => { + const search = (start: number, end: number): T | undefined => { + if (start > end) return undefined; + + const mid = Math.floor((start + end) / 2); + + const result = match(items[mid]); + if (result === 0) return items[mid]; + + if (result === 1) return search(start, mid - 1); + return search(mid + 1, end); + }; + + return search(0, items.length - 1); +}; + +export const randomNumberBetween = (min: number, max: number) => + Math.floor(Math.random() * (max - min + 1)) + min; + +export const scaleYDimension = (x: number, scaledX: number, y: number): number => { + const scaleFactor = scaledX / x; + return scaleFactor * y; +}; + +export const parseGeoUri = (location: string) => { + const [, data] = location.split(':'); + const [cords] = data.split(';'); + const [latitude, longitude] = cords.split(','); + return { + latitude, + longitude, + }; +}; diff --git a/src/app/utils/dom.ts b/src/app/utils/dom.ts index d717adf..a8dc4be 100644 --- a/src/app/utils/dom.ts +++ b/src/app/utils/dom.ts @@ -7,7 +7,7 @@ export const editableActiveElement = (): boolean => !!document.activeElement && /^(input)|(textarea)$/.test(document.activeElement.nodeName.toLowerCase()); -export const inVisibleScrollArea = ( +export const isIntersectingScrollView = ( scrollElement: HTMLElement, childElement: HTMLElement ): boolean => { @@ -18,10 +18,25 @@ export const inVisibleScrollArea = ( const childBottom = childTop + childElement.clientHeight; if (childTop >= scrollTop && childTop < scrollBottom) return true; - if (childTop < scrollTop && childBottom > scrollTop) return true; + if (childBottom > scrollTop && childBottom <= scrollBottom) return true; + if (childTop < scrollTop && childBottom > scrollBottom) return true; return false; }; +export const isInScrollView = (scrollElement: HTMLElement, childElement: HTMLElement): boolean => { + const scrollTop = scrollElement.offsetTop + scrollElement.scrollTop; + const scrollBottom = scrollTop + scrollElement.offsetHeight; + return ( + childElement.offsetTop >= scrollTop && + childElement.offsetTop + childElement.offsetHeight <= scrollBottom + ); +}; + +export const canFitInScrollView = ( + scrollElement: HTMLElement, + childElement: HTMLElement +): boolean => childElement.offsetHeight < scrollElement.offsetHeight; + export type FilesOrFile = T extends true ? File[] : File; export const selectFile = ( @@ -131,3 +146,43 @@ export const getThumbnail = ( resolve(thumbnail ?? undefined); }, thumbnailMimeType ?? 'image/jpeg'); }); + +export type ScrollInfo = { + offsetTop: number; + top: number; + height: number; + viewHeight: number; + scrollable: boolean; +}; +export const getScrollInfo = (target: HTMLElement): ScrollInfo => ({ + offsetTop: Math.round(target.offsetTop), + top: Math.round(target.scrollTop), + height: Math.round(target.scrollHeight), + viewHeight: Math.round(target.offsetHeight), + scrollable: target.scrollHeight > target.offsetHeight, +}); + +export const scrollToBottom = (scrollEl: HTMLElement, behavior?: 'auto' | 'instant' | 'smooth') => { + scrollEl.scrollTo({ + top: Math.round(scrollEl.scrollHeight - scrollEl.offsetHeight), + behavior, + }); +}; + +export const copyToClipboard = (text: string) => { + if (navigator.clipboard) { + navigator.clipboard.writeText(text); + } else { + const host = document.body; + const copyInput = document.createElement('input'); + copyInput.style.position = 'fixed'; + copyInput.style.opacity = '0'; + copyInput.value = text; + host.append(copyInput); + + copyInput.select(); + copyInput.setSelectionRange(0, 99999); + document.execCommand('Copy'); + copyInput.remove(); + } +}; diff --git a/src/app/utils/matrix.ts b/src/app/utils/matrix.ts index 7f2fc0f..91bd80f 100644 --- a/src/app/utils/matrix.ts +++ b/src/app/utils/matrix.ts @@ -1,5 +1,16 @@ -import { EncryptedAttachmentInfo, encryptAttachment } from 'browser-encrypt-attachment'; -import { MatrixClient, MatrixError, UploadProgress, UploadResponse } from 'matrix-js-sdk'; +import { + EncryptedAttachmentInfo, + decryptAttachment, + encryptAttachment, +} from 'browser-encrypt-attachment'; +import { + MatrixClient, + MatrixError, + MatrixEvent, + Room, + UploadProgress, + UploadResponse, +} from 'matrix-js-sdk'; import { IImageInfo, IThumbnailContent, IVideoInfo } from '../../types/matrix/common'; export const matchMxId = (id: string): RegExpMatchArray | null => @@ -13,6 +24,13 @@ export const getMxIdLocalPart = (userId: string): string | undefined => matchMxI export const isUserId = (id: string): boolean => validMxId(id) && id.startsWith('@'); +export const isRoomId = (id: string): boolean => validMxId(id) && id.startsWith('!'); + +export const isRoomAlias = (id: string): boolean => validMxId(id) && id.startsWith('#'); + +export const getRoomWithCanonicalAlias = (mx: MatrixClient, alias: string): Room | undefined => + mx.getRooms()?.find((room) => room.getCanonicalAlias() === alias); + export const getImageInfo = (img: HTMLImageElement, fileOrBlob: File | Blob): IImageInfo => { const info: IImageInfo = {}; info.w = img.width; @@ -24,7 +42,7 @@ export const getImageInfo = (img: HTMLImageElement, fileOrBlob: File | Blob): II export const getVideoInfo = (video: HTMLVideoElement, fileOrBlob: File | Blob): IVideoInfo => { const info: IVideoInfo = {}; - info.duration = Number.isNaN(video.duration) ? undefined : video.duration; + info.duration = Number.isNaN(video.duration) ? undefined : Math.floor(video.duration * 1000); info.w = video.videoWidth; info.h = video.videoHeight; info.mimetype = fileOrBlob.type; @@ -79,6 +97,16 @@ export const encryptFile = async ( }; }; +export const decryptFile = async ( + dataBuffer: ArrayBuffer, + type: string, + encInfo: EncryptedAttachmentInfo +): Promise => { + const dataArray = await decryptAttachment(dataBuffer, encInfo); + const blob = new Blob([dataArray], { type }); + return blob; +}; + export type TUploadContent = File | Blob; export type ContentUploadOptions = { @@ -116,3 +144,19 @@ export const uploadContent = async ( onError(new MatrixError({ error, errcode })); } }; + +export const matrixEventByRecency = (m1: MatrixEvent, m2: MatrixEvent) => m2.getTs() - m1.getTs(); + +export const factoryEventSentBy = (senderId: string) => (ev: MatrixEvent) => + ev.getSender() === senderId; + +export const eventWithShortcode = (ev: MatrixEvent) => + typeof ev.getContent().shortcode === 'string'; + +export const trimReplyFromBody = (body: string): string => { + if (body.match(/^> <.+>/) === null) return body; + + const trimmedBody = body.slice(body.indexOf('\n\n') + 2); + + return trimmedBody || body; +}; diff --git a/src/app/utils/mimeTypes.ts b/src/app/utils/mimeTypes.ts index c432bdc..c883ddb 100644 --- a/src/app/utils/mimeTypes.ts +++ b/src/app/utils/mimeTypes.ts @@ -1,17 +1,15 @@ -// https://github.com/matrix-org/matrix-react-sdk/blob/cd15e08fc285da42134817cce50de8011809cd53/src/utils/blobs.ts -export const ALLOWED_BLOB_MIMETYPES = [ +export const IMAGE_MIME_TYPES = [ 'image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/avif', +]; - 'video/mp4', - 'video/webm', - 'video/ogg', - 'video/quicktime', +export const VIDEO_MIME_TYPES = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime']; +export const AUDIO_MIME_TYPES = [ 'audio/mp4', 'audio/webm', 'audio/aac', @@ -25,11 +23,55 @@ export const ALLOWED_BLOB_MIMETYPES = [ 'audio/x-flac', ]; +export const APPLICATION_MIME_TYPES = [ + 'application/pdf', + 'application/json', + 'application/x-sh', + 'application/ecmascript', + 'application/javascript', + 'application/xhtml+xml', + 'application/xml', +]; + +export const TEXT_MIME_TYPE = [ + 'text/plain', + 'text/html', + 'text/css', + 'text/javascript', + 'text/x-c', + 'text/csv', + 'text/tab-separated-values', + 'text/yaml', + 'text/x-java-source,java', + 'text/markdown', +]; + +export const READABLE_TEXT_MIME_TYPES = [ + 'application/json', + 'application/x-sh', + 'application/ecmascript', + 'application/javascript', + 'application/xhtml+xml', + 'application/xml', + + ...TEXT_MIME_TYPE, +]; + +export const ALLOWED_BLOB_MIME_TYPES = [ + ...IMAGE_MIME_TYPES, + ...VIDEO_MIME_TYPES, + ...AUDIO_MIME_TYPES, + ...APPLICATION_MIME_TYPES, + ...TEXT_MIME_TYPE, +]; + +export const FALLBACK_MIMETYPE = 'application/octet-stream'; + export const getBlobSafeMimeType = (mimeType: string) => { - if (typeof mimeType !== 'string') return 'application/octet-stream'; + if (typeof mimeType !== 'string') return FALLBACK_MIMETYPE; const [type] = mimeType.split(';'); - if (!ALLOWED_BLOB_MIMETYPES.includes(type)) { - return 'application/octet-stream'; + if (!ALLOWED_BLOB_MIME_TYPES.includes(type)) { + return FALLBACK_MIMETYPE; } // Required for Chromium browsers if (type === 'video/quicktime') { @@ -45,3 +87,8 @@ export const safeFile = (f: File) => { } return f; }; + +export const mimeTypeToExt = (mimeType: string): string => { + const extStart = mimeType.lastIndexOf('/') + 1; + return mimeType.slice(extStart); +}; diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index daf9560..f863783 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -1,6 +1,7 @@ import { IconName, IconSrc } from 'folds'; import { + EventTimeline, IPushRule, IPushRules, JoinRule, @@ -9,6 +10,7 @@ import { NotificationCountType, Room, } from 'matrix-js-sdk'; +import { CryptoBackend } from 'matrix-js-sdk/lib/common-crypto/CryptoBackend'; import { AccountDataEvent } from '../../types/matrix/accountData'; import { NotificationType, @@ -263,3 +265,35 @@ export const parseReplyFormattedBody = ( return `
${replyToLink}${userLink}
${formattedBody}
`; }; + +export const getMemberDisplayName = (room: Room, userId: string): string | undefined => { + const member = room.getMember(userId); + const name = member?.rawDisplayName; + if (name === userId) return undefined; + return name; +}; + +export const getMemberAvatarMxc = (room: Room, userId: string): string | undefined => { + const member = room.getMember(userId); + return member?.getMxcAvatarUrl(); +}; + +export const decryptAllTimelineEvent = async (mx: MatrixClient, timeline: EventTimeline) => { + const crypto = mx.getCrypto(); + if (!crypto) return; + const decryptionPromises = timeline + .getEvents() + .filter((event) => event.isEncrypted()) + .reverse() + .map((event) => event.attemptDecryption(crypto as CryptoBackend, { isRetry: true })); + await Promise.allSettled(decryptionPromises); +}; + +export const getReactionContent = (eventId: string, key: string, shortcode?: string) => ({ + 'm.relates_to': { + event_id: eventId, + key, + rel_type: 'm.annotation', + }, + shortcode, +}); diff --git a/src/app/utils/sanitize.ts b/src/app/utils/sanitize.ts index 555089d..6a03ca7 100644 --- a/src/app/utils/sanitize.ts +++ b/src/app/utils/sanitize.ts @@ -1,3 +1,145 @@ +import sanitizeHtml, { Transformer } from 'sanitize-html'; + +const MAX_TAG_NESTING = 100; + +const permittedHtmlTags = [ + 'font', + 'del', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'blockquote', + 'p', + 'a', + 'ul', + 'ol', + 'sup', + 'sub', + 'li', + 'b', + 'i', + 'u', + 'strong', + 'em', + 'strike', + 's', + 'code', + 'hr', + 'br', + 'div', + 'table', + 'thead', + 'tbody', + 'tr', + 'th', + 'td', + 'caption', + 'pre', + 'span', + 'img', + 'details', + 'summary', +]; + +const urlSchemes = ['https', 'http', 'ftp', 'mailto', 'magnet']; + +const permittedTagToAttributes = { + font: ['style', 'data-mx-bg-color', 'data-mx-color', 'color'], + span: [ + 'style', + 'data-mx-bg-color', + 'data-mx-color', + 'data-mx-spoiler', + 'data-mx-maths', + 'data-mx-pill', + 'data-mx-ping', + ], + div: ['data-mx-maths'], + a: ['name', 'target', 'href', 'rel'], + img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'], + ol: ['start'], + code: ['class'], +}; + +const transformFontTag: Transformer = (tagName, attribs) => ({ + tagName, + attribs: { + ...attribs, + style: `background-color: ${attribs['data-mx-bg-color']}; color: ${attribs['data-mx-color']}`, + }, +}); + +const transformSpanTag: Transformer = (tagName, attribs) => ({ + tagName, + attribs: { + ...attribs, + style: `background-color: ${attribs['data-mx-bg-color']}; color: ${attribs['data-mx-color']}`, + }, +}); + +const transformATag: Transformer = (tagName, attribs) => ({ + tagName, + attribs: { + ...attribs, + rel: 'noopener', + target: '_blank', + }, +}); + +const transformImgTag: Transformer = (tagName, attribs) => { + const { src } = attribs; + if (src.startsWith('mxc://') === false) { + return { + tagName: 'a', + attribs: { + href: src, + rel: 'noopener', + target: '_blank', + }, + text: attribs.alt || src, + }; + } + return { + tagName, + attribs: { + ...attribs, + }, + }; +}; + +export const sanitizeCustomHtml = (customHtml: string): string => + sanitizeHtml(customHtml, { + allowedTags: permittedHtmlTags, + allowedAttributes: permittedTagToAttributes, + disallowedTagsMode: 'discard', + allowedSchemes: urlSchemes, + allowedSchemesByTag: { + a: urlSchemes, + }, + allowedSchemesAppliedToAttributes: ['href'], + allowProtocolRelative: false, + allowedClasses: { + code: ['language-*'], + }, + allowedStyles: { + '*': { + color: [/^#(?:[0-9a-fA-F]{3}){1,2}$/], + 'background-color': [/^#(?:[0-9a-fA-F]{3}){1,2}$/], + }, + }, + transformTags: { + font: transformFontTag, + span: transformSpanTag, + a: transformATag, + img: transformImgTag, + }, + nonTextTags: ['style', 'script', 'textarea', 'option', 'noscript', 'mx-reply'], + nestingLimit: MAX_TAG_NESTING, + }); + export const sanitizeText = (body: string) => { const tagsToReplace: Record = { '&': '&', diff --git a/src/app/utils/time.ts b/src/app/utils/time.ts new file mode 100644 index 0000000..3ee6720 --- /dev/null +++ b/src/app/utils/time.ts @@ -0,0 +1,35 @@ +import dayjs from 'dayjs'; +import isToday from 'dayjs/plugin/isToday'; +import isYesterday from 'dayjs/plugin/isYesterday'; + +dayjs.extend(isToday); +dayjs.extend(isYesterday); + +export const today = (ts: number): boolean => dayjs(ts).isToday(); + +export const yesterday = (ts: number): boolean => dayjs(ts).isYesterday(); + +export const timeHourMinute = (ts: number): string => dayjs(ts).format('hh:mm A'); + +export const timeDayMonYear = (ts: number): string => dayjs(ts).format('D MMM YYYY'); + +export const timeDayMonthYear = (ts: number): string => dayjs(ts).format('D MMMM YYYY'); + +export const inSameDay = (ts1: number, ts2: number): boolean => { + const dt1 = new Date(ts1); + const dt2 = new Date(ts2); + return ( + dt2.getFullYear() === dt1.getFullYear() && + dt2.getMonth() === dt1.getMonth() && + dt2.getDate() === dt1.getDate() + ); +}; + +export const minuteDifference = (ts1: number, ts2: number): number => { + const dt1 = new Date(ts1); + const dt2 = new Date(ts2); + + let diff = (dt2.getTime() - dt1.getTime()) / 1000; + diff /= 60; + return Math.abs(Math.round(diff)); +}; diff --git a/src/client/state/settings.js b/src/client/state/settings.js index af2e279..cc1193c 100644 --- a/src/client/state/settings.js +++ b/src/client/state/settings.js @@ -59,6 +59,8 @@ class Settings extends EventEmitter { this.themes.forEach((themeName, index) => { if (themeName !== '') document.body.classList.remove(themeName); document.body.classList.remove(this.themeClasses[index]); + document.body.classList.remove('prism-light') + document.body.classList.remove('prism-dark') }); } @@ -69,6 +71,7 @@ class Settings extends EventEmitter { if (this.themes[themeIndex] === undefined) return if (this.themes[themeIndex]) document.body.classList.add(this.themes[themeIndex]); document.body.classList.add(this.themeClasses[themeIndex]); + document.body.classList.add(themeIndex < 2 ? 'prism-light' : 'prism-dark'); } setTheme(themeIndex) { diff --git a/src/ext.d.ts b/src/ext.d.ts index 55f5932..5593b6e 100644 --- a/src/ext.d.ts +++ b/src/ext.d.ts @@ -20,4 +20,9 @@ declare module 'browser-encrypt-attachment' { } export function encryptAttachment(dataBuffer: ArrayBuffer): Promise; + + export function decryptAttachment( + dataBuffer: ArrayBuffer, + info: EncryptedAttachmentInfo + ): Promise; } diff --git a/src/index.scss b/src/index.scss index 93443fe..04125a1 100644 --- a/src/index.scss +++ b/src/index.scss @@ -210,14 +210,14 @@ .dark-theme, .butter-theme { /* background color | --bg-[background type]: value */ - --bg-surface: hsl(208, 8%, 20%); - --bg-surface-transparent: hsla(208, 8%, 20%, 0); - --bg-surface-low: hsl(208, 8%, 16%); - --bg-surface-low-transparent: hsla(208, 8%, 16%, 0); - --bg-surface-extra-low: hsl(208, 8%, 14%); - --bg-surface-extra-low-transparent: hsla(208, 8%, 14%, 0); - --bg-surface-hover: rgba(255, 255, 255, 3%); - --bg-surface-active: rgba(255, 255, 255, 5%); + --bg-surface: #1f2326; + --bg-surface-transparent: #1f232600; + --bg-surface-low: #15171a; + --bg-surface-low-transparent: #15171a00; + --bg-surface-extra-low: #15171a; + --bg-surface-extra-low-transparent: #15171a00; + --bg-surface-hover: #1f2326; + --bg-surface-active: #2a2e33; --bg-surface-border: rgba(0, 0, 0, 20%); --bg-primary: rgb(42, 98, 166); diff --git a/src/types/matrix/common.ts b/src/types/matrix/common.ts index 94a46a9..cc20d45 100644 --- a/src/types/matrix/common.ts +++ b/src/types/matrix/common.ts @@ -1,16 +1,35 @@ import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment'; +import { MsgType } from 'matrix-js-sdk'; + +export const MATRIX_BLUR_HASH_PROPERTY_NAME = 'xyz.amorgan.blurhash'; export type IImageInfo = { w?: number; h?: number; mimetype?: string; size?: number; + [MATRIX_BLUR_HASH_PROPERTY_NAME]?: string; }; -export type IVideoInfo = IImageInfo & { +export type IVideoInfo = { + w?: number; + h?: number; + mimetype?: string; + size?: number; duration?: number; }; +export type IAudioInfo = { + mimetype?: string; + size?: number; + duration?: number; +}; + +export type IFileInfo = { + mimetype?: string; + size?: number; +}; + export type IEncryptedFile = EncryptedAttachmentInfo & { url: string; }; @@ -20,3 +39,42 @@ export type IThumbnailContent = { thumbnail_file?: IEncryptedFile; thumbnail_url?: string; }; + +export type IImageContent = { + msgtype: MsgType.Image; + body?: string; + url?: string; + info?: IImageInfo & IThumbnailContent; + file?: IEncryptedFile; +}; + +export type IVideoContent = { + msgtype: MsgType.Video; + body?: string; + url?: string; + info?: IVideoInfo & IThumbnailContent; + file?: IEncryptedFile; +}; + +export type IAudioContent = { + msgtype: MsgType.Audio; + body?: string; + url?: string; + info?: IAudioInfo; + file?: IEncryptedFile; +}; + +export type IFileContent = { + msgtype: MsgType.File; + body?: string; + url?: string; + info?: IFileInfo & IThumbnailContent; + file?: IEncryptedFile; +}; + +export type ILocationContent = { + msgtype: MsgType.Location; + body?: string; + geo_uri?: string; + info?: IThumbnailContent; +}; diff --git a/src/types/matrix/room.ts b/src/types/matrix/room.ts index 93e8761..33419ce 100644 --- a/src/types/matrix/room.ts +++ b/src/types/matrix/room.ts @@ -6,6 +6,14 @@ export enum Membership { Ban = 'ban', } +export type IMemberContent = { + avatar_url?: string; + displayname?: string; + membership?: Membership; + reason?: string; + is_direct?: boolean; +}; + export enum StateEvent { RoomCanonicalAlias = 'm.room.canonical_alias', RoomCreate = 'm.room.create', @@ -29,6 +37,14 @@ export enum StateEvent { PoniesRoomEmotes = 'im.ponies.room_emotes', } +export enum MessageEvent { + RoomMessage = 'm.room.message', + RoomMessageEncrypted = 'm.room.encrypted', + Sticker = 'm.sticker', + RoomRedaction = 'm.room.redaction', + Reaction = 'm.reaction', +} + export enum RoomType { Space = 'm.space', } @@ -40,6 +56,17 @@ export enum NotificationType { Mute = 'mute', } +export type IRoomCreateContent = { + creator?: string; + ['m.federate']?: boolean; + room_version: string; + type?: string; + predecessor?: { + event_id: string; + room_id: string; + }; +}; + export type RoomToParents = Map>; export type RoomToUnread = Map< string, diff --git a/tsconfig.json b/tsconfig.json index 02eb184..d2f1e8a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "sourceMap": true, "jsx": "react", "target": "ES2016", + "module": "ES2020", "allowJs": true, "strict": true, "esModuleInterop": true, diff --git a/vite.config.js b/vite.config.js index f09aa71..8357339 100644 --- a/vite.config.js +++ b/vite.config.js @@ -13,6 +13,10 @@ 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: '',