diff --git a/package-lock.json b/package-lock.json index a0f4c488f..b6f5e5577 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "@tanstack/react-query": "5.100.13", "@tanstack/react-query-devtools": "5.100.13", "@tanstack/react-virtual": "3.13.25", - "@types/dompurify": "3.2.0", "@workadventure/noise-suppression": "0.0.4", "await-to-js": "3.0.0", "badwords-list": "2.0.1-4", @@ -36,7 +35,6 @@ "dayjs": "1.11.20", "deepfilternet3-noise-filter": "1.2.1", "domhandler": "6.0.1", - "dompurify": "3.4.5", "emojibase": "17.0.0", "emojibase-data": "17.0.0", "file-saver": "2.0.5", @@ -75,7 +73,8 @@ "slate-history": "0.113.1", "slate-react": "0.124.2", "styled-components": "6.4.2", - "ua-parser-js": "2.0.10" + "ua-parser-js": "2.0.10", + "workbox-precaching": "7.4.1" }, "devDependencies": { "@lotusguild/element-call-embedded": "0.20.1-lotus.1", @@ -2697,9 +2696,9 @@ "dev": true }, "node_modules/@matrix-org/matrix-sdk-crypto-wasm": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.0.tgz", - "integrity": "sha512-9a4feyt8QLysARu7PHKaRWT+wcCd+IYH074LXp9QK5WqfN4zUXueRhiSSMNT18Bm+8q3sBR/4zxDxOSDR0M8Kg==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.1.tgz", + "integrity": "sha512-VRjWhE1UgHnPpJ3b9B5+8z71ZC/HICFngPPFIN6ktzmUBKI5RusPujzbAQUoB3CgZ0yU58L99AfSQS4YTztSWw==", "license": "Apache-2.0", "engines": { "node": ">= 18" @@ -3920,16 +3919,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/dompurify": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.2.0.tgz", - "integrity": "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==", - "deprecated": "This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.", - "license": "MIT", - "dependencies": { - "dompurify": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -4051,7 +4040,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "devOptional": true + "dev": true }, "node_modules/@types/ua-parser-js": { "version": "0.7.39", @@ -6196,15 +6185,6 @@ "node": ">=20.19.0" } }, - "node_modules/dompurify": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.5.tgz", - "integrity": "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -13228,7 +13208,6 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.1.tgz", "integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==", - "dev": true, "license": "MIT" }, "node_modules/workbox-expiration": { @@ -13269,7 +13248,6 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.1.tgz", "integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==", - "dev": true, "license": "MIT", "dependencies": { "workbox-core": "7.4.1", @@ -13306,7 +13284,6 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.1.tgz", "integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==", - "dev": true, "license": "MIT", "dependencies": { "workbox-core": "7.4.1" @@ -13316,7 +13293,6 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.1.tgz", "integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==", - "dev": true, "license": "MIT", "dependencies": { "workbox-core": "7.4.1" diff --git a/package.json b/package.json index b94e088c7..a57dc3b46 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "@tanstack/react-query": "5.100.13", "@tanstack/react-query-devtools": "5.100.13", "@tanstack/react-virtual": "3.13.25", - "@types/dompurify": "3.2.0", "@workadventure/noise-suppression": "0.0.4", "await-to-js": "3.0.0", "badwords-list": "2.0.1-4", @@ -61,7 +60,6 @@ "dayjs": "1.11.20", "deepfilternet3-noise-filter": "1.2.1", "domhandler": "6.0.1", - "dompurify": "3.4.5", "emojibase": "17.0.0", "emojibase-data": "17.0.0", "file-saver": "2.0.5", @@ -100,7 +98,8 @@ "slate-history": "0.113.1", "slate-react": "0.124.2", "styled-components": "6.4.2", - "ua-parser-js": "2.0.10" + "ua-parser-js": "2.0.10", + "workbox-precaching": "7.4.1" }, "devDependencies": { "@lotusguild/element-call-embedded": "0.20.1-lotus.1", diff --git a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx index 9a59cf6ab..5c2809a6a 100644 --- a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx @@ -1,4 +1,4 @@ -import React, { KeyboardEvent as ReactKeyboardEvent, useEffect, useMemo } from 'react'; +import React, { KeyboardEvent as ReactKeyboardEvent, useEffect, useMemo, useState } from 'react'; import { Editor } from 'slate'; import { Box, MenuItem, Text, toRem } from 'folds'; import { Room } from 'matrix-js-sdk'; @@ -11,7 +11,7 @@ import { onTabPress } from '../../../utils/keyboard'; import { createEmoticonElement, moveCursor, replaceWithElement } from '../utils'; import { useRecentEmoji } from '../../../hooks/useRecentEmoji'; import { useRelevantImagePacks } from '../../../hooks/useImagePacks'; -import { IEmoji, emojis } from '../../../plugins/emoji'; +import { IEmoji, emojis, loadEmojiData } from '../../../plugins/emoji'; import { useKeyDown } from '../../../hooks/useKeyDown'; import { mxcUrlToHttp } from '../../../utils/matrix'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; @@ -47,13 +47,32 @@ export function EmoticonAutocomplete({ const imagePacks = useRelevantImagePacks(ImageUsage.Emoticon, imagePackRooms); const recentEmoji = useRecentEmoji(mx, 20); + // Lazily load emojibase data (see plugins/emoji `loadEmojiData`). Until it + // resolves, `emojis` is empty and autocomplete matches only custom-emoji + // packs; the unicode emoji list fills in once loaded. + const [loadedEmojis, setLoadedEmojis] = useState(() => emojis); + useEffect(() => { + let alive = true; + loadEmojiData() + // Fresh array reference: loadEmojiData populates the module-level array + // IN PLACE, so state set to the same ref would bail out of re-rendering + // and the search list would never gain the unicode emojis. + .then((loaded) => { + if (alive) setLoadedEmojis(loaded.emojis.slice()); + }) + .catch(() => undefined); + return () => { + alive = false; + }; + }, []); + const searchList = useMemo(() => { const list: Array = []; return list.concat( imagePacks.flatMap((pack) => pack.getImages(ImageUsage.Emoticon)), - emojis, + loadedEmojis, ); - }, [imagePacks]); + }, [imagePacks, loadedEmojis]); const [result, search, resetSearch] = useAsyncSearch( searchList, diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index a1550d79e..2c2af3121 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -8,6 +8,7 @@ import React, { useEffect, useMemo, useRef, + useState, } from 'react'; import { Box, config, Icons, Scroll } from 'folds'; import FocusTrap from 'focus-trap-react'; @@ -15,7 +16,7 @@ import { isKeyHotkey } from 'is-hotkey'; import { Room } from 'matrix-js-sdk'; import { atom, PrimitiveAtom, useAtom, useSetAtom } from 'jotai'; import { useVirtualizer } from '@tanstack/react-virtual'; -import { IEmoji, emojiGroups, emojis } from '../../plugins/emoji'; +import { EmojiData, IEmoji, emojiGroups, emojis, loadEmojiData } from '../../plugins/emoji'; import { useEmojiGroupLabels } from './useEmojiGroupLabels'; import { useEmojiGroupIcons } from './useEmojiGroupIcons'; import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard'; @@ -56,6 +57,33 @@ import { VirtualTile } from '../virtualizer'; const RECENT_GROUP_ID = 'recent_group'; const SEARCH_GROUP_ID = 'search_group'; +/** + * Lazily pull in the emojibase data (see plugins/emoji `loadEmojiData`). The + * `emojis`/`emojiGroups` arrays are populated in place once the promise + * resolves; we wrap them in a fresh object on load so React re-renders and the + * board fills in. Before that, both are empty and the board shows only custom + * image packs / recents (which is fleeting — the load starts on mount). + */ +const useEmojiData = (): EmojiData => { + const [data, setData] = useState(() => ({ emojis, emojiGroups })); + useEffect(() => { + let alive = true; + loadEmojiData() + // Fresh array references (not just a fresh wrapper): downstream memos + // depend on the arrays themselves, which are populated IN PLACE — same + // refs would skip recompute and leave emoji search empty until remount. + .then((loaded) => { + if (alive) + setData({ emojis: loaded.emojis.slice(), emojiGroups: loaded.emojiGroups.slice() }); + }) + .catch(() => undefined); + return () => { + alive = false; + }; + }, []); + return data; +}; + type EmojiGroupItem = { id: string; name: string; @@ -75,6 +103,7 @@ const useGroups = ( const recentEmojis = useRecentEmoji(mx, 21); const labels = useEmojiGroupLabels(); + const { emojiGroups: loadedEmojiGroups } = useEmojiData(); const emojiGroupItems = useMemo(() => { const g: EmojiGroupItem[] = []; @@ -99,7 +128,7 @@ const useGroups = ( }); }); - emojiGroups.forEach((group) => { + loadedEmojiGroups.forEach((group) => { g.push({ id: group.id, name: labels[group.id], @@ -108,7 +137,7 @@ const useGroups = ( }); return g; - }, [mx, recentEmojis, labels, imagePacks, tab]); + }, [mx, recentEmojis, labels, imagePacks, tab, loadedEmojiGroups]); const stickerGroupItems = useMemo(() => { const g: StickerGroupItem[] = []; @@ -177,6 +206,7 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP const usage = ImageUsage.Emoticon; const labels = useEmojiGroupLabels(); const icons = useEmojiGroupIcons(); + const { emojiGroups: loadedEmojiGroups } = useEmojiData(); const packLabels = useMemo(() => { const map = new Map(); @@ -234,7 +264,7 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP }} > - {emojiGroups.map((group) => ( + {loadedEmojiGroups.map((group) => ( { let list: Array = []; list = list.concat(imagePacks.flatMap((pack) => pack.getImages(usage))); - if (emojiTab) list = list.concat(emojis); + if (emojiTab) list = list.concat(loadedEmojis); return list; - }, [emojiTab, usage, imagePacks]); + }, [emojiTab, usage, imagePacks, loadedEmojis]); const [result, search, resetSearch] = useAsyncSearch( searchList, diff --git a/src/app/hooks/useRecentEmoji.ts b/src/app/hooks/useRecentEmoji.ts index 926075f51..c09fabf34 100644 --- a/src/app/hooks/useRecentEmoji.ts +++ b/src/app/hooks/useRecentEmoji.ts @@ -2,11 +2,26 @@ import { useEffect, useState } from 'react'; import { ClientEvent, MatrixClient, MatrixEvent } from 'matrix-js-sdk'; import { getRecentEmojis } from '../plugins/recent-emoji'; import { AccountDataEvent } from '../../types/matrix/accountData'; -import { IEmoji } from '../plugins/emoji'; +import { IEmoji, loadEmojiData } from '../plugins/emoji'; export const useRecentEmoji = (mx: MatrixClient, limit?: number): IEmoji[] => { const [recentEmoji, setRecentEmoji] = useState(() => getRecentEmojis(mx, limit)); + // Recent emojis are resolved against the (now lazily loaded) emojibase data + // via getRecentEmojis. Recompute once loadEmojiData has populated it so the + // recent list fills in on first open. + useEffect(() => { + let alive = true; + loadEmojiData() + .then(() => { + if (alive) setRecentEmoji(getRecentEmojis(mx, limit)); + }) + .catch(() => undefined); + return () => { + alive = false; + }; + }, [mx, limit]); + useEffect(() => { const handleAccountData = (event: MatrixEvent) => { if (event.getType() !== AccountDataEvent.ElementRecentEmoji) return; diff --git a/src/app/plugins/emoji.ts b/src/app/plugins/emoji.ts index 2462b7ffe..9a86b90dd 100644 --- a/src/app/plugins/emoji.ts +++ b/src/app/plugins/emoji.ts @@ -1,7 +1,4 @@ -import { CompactEmoji, fromUnicodeToHexcode } from 'emojibase'; -import emojisData from 'emojibase-data/en/compact.json'; -import joypixels from 'emojibase-data/en/shortcodes/joypixels.json'; -import emojibase from 'emojibase-data/en/shortcodes/emojibase.json'; +import type { CompactEmoji } from 'emojibase'; export type IEmoji = CompactEmoji & { shortcode: string; @@ -24,57 +21,76 @@ export type IEmojiGroup = { emojis: IEmoji[]; }; -export const getShortcodesFor = (hexcode: string): string[] | string | undefined => - joypixels[hexcode] || emojibase[hexcode]; +export type EmojiData = { + emojis: IEmoji[]; + emojiGroups: IEmojiGroup[]; +}; + +type ShortcodeMap = Record; + +/** + * PERF (lazy emojibase split): the heavy `emojibase-data` JSON (compact emoji + * data + the joypixels/emojibase shortcode maps, ~965 KB combined) used to be + * imported statically at module top-level. Because reaction/message rendering + * (`Reaction`, `scaleSystemEmoji`) import this module eagerly, that dragged the + * whole `emojibase` chunk into the initial (eager) bundle graph. + * + * It is now loaded on demand via `loadEmojiData()` (a memoized dynamic import). + * Only lazy emoji surfaces (EmojiBoard, EmoticonAutocomplete, recent-emoji) + * trigger the load. Anything that renders eagerly (reaction/emoji tooltips and + * aria-labels via `getShortcodeFor`) gracefully degrades to `undefined` until + * the data has been loaded — the visible emoji glyph itself never depended on + * this data, so on-screen UX is unchanged; the shortcode label simply resolves + * once emoji data is loaded. `getHexcodeForEmoji` is inlined below so it stays + * synchronous WITHOUT pulling the `emojibase` runtime into the eager graph. + */ + +// Inlined from emojibase's `fromUnicodeToHexcode` so this synchronous helper +// does not import the `emojibase` package (and thus the emojibase chunk) into +// the eager graph. Kept byte-for-byte behaviourally identical. +const SEQUENCE_REMOVAL_PATTERN = /200D|FE0E|FE0F/g; + +export const getHexcodeForEmoji = (unicode: string, strip = true): string => { + const hexcode: string[] = []; + [...unicode].forEach((codepoint) => { + let hex = codepoint.codePointAt(0)?.toString(16).toUpperCase() ?? ''; + while (hex.length < 4) { + hex = `0${hex}`; + } + if (!strip || !hex.match(SEQUENCE_REMOVAL_PATTERN)) { + hexcode.push(hex); + } + }); + return hexcode.join('-'); +}; + +// Populated by loadEmojiData(); `undefined` until the data has been loaded. +let joypixelsShortcodes: ShortcodeMap | undefined; +let emojibaseShortcodes: ShortcodeMap | undefined; + +export const getShortcodesFor = (hexcode: string): string[] | string | undefined => { + if (!joypixelsShortcodes || !emojibaseShortcodes) return undefined; + return joypixelsShortcodes[hexcode] || emojibaseShortcodes[hexcode]; +}; export const getShortcodeFor = (hexcode: string): string | undefined => { - const shortcode = joypixels[hexcode] || emojibase[hexcode]; + const shortcode = getShortcodesFor(hexcode); return Array.isArray(shortcode) ? shortcode[0] : shortcode; }; -export const getHexcodeForEmoji = fromUnicodeToHexcode; - +// Shared, stable array references. They start empty and are populated in place +// the first time loadEmojiData() resolves (mirroring the previous eager module +// side-effect). React consumers await loadEmojiData() and re-render to observe +// the populated data; non-React consumers (recent-emoji) read them after load. export const emojiGroups: IEmojiGroup[] = [ - { - id: EmojiGroupId.People, - order: 0, - emojis: [], - }, - { - id: EmojiGroupId.Nature, - order: 1, - emojis: [], - }, - { - id: EmojiGroupId.Food, - order: 2, - emojis: [], - }, - { - id: EmojiGroupId.Activity, - order: 3, - emojis: [], - }, - { - id: EmojiGroupId.Travel, - order: 4, - emojis: [], - }, - { - id: EmojiGroupId.Object, - order: 5, - emojis: [], - }, - { - id: EmojiGroupId.Symbol, - order: 6, - emojis: [], - }, - { - id: EmojiGroupId.Flag, - order: 7, - emojis: [], - }, + { id: EmojiGroupId.People, order: 0, emojis: [] }, + { id: EmojiGroupId.Nature, order: 1, emojis: [] }, + { id: EmojiGroupId.Food, order: 2, emojis: [] }, + { id: EmojiGroupId.Activity, order: 3, emojis: [] }, + { id: EmojiGroupId.Travel, order: 4, emojis: [] }, + { id: EmojiGroupId.Object, order: 5, emojis: [] }, + { id: EmojiGroupId.Symbol, order: 6, emojis: [] }, + { id: EmojiGroupId.Flag, order: 7, emojis: [] }, ]; export const emojis: IEmoji[] = []; @@ -95,20 +111,51 @@ function getGroupIndex(emoji: IEmoji): number | undefined { return undefined; } -emojisData.forEach((emoji) => { - const myShortCodes = getShortcodesFor(emoji.hexcode); - if (!myShortCodes) return; - if (Array.isArray(myShortCodes) && myShortCodes.length === 0) return; +let emojiDataPromise: Promise | undefined; - const em: IEmoji = { - ...emoji, - shortcode: Array.isArray(myShortCodes) ? myShortCodes[0] : myShortCodes, - shortcodes: Array.isArray(myShortCodes) ? myShortCodes : emoji.shortcodes, - }; +/** + * Lazily load emojibase data (dynamic import → the `emojibase` chunk). Memoized: + * the JSON is fetched/parsed and `emojis`/`emojiGroups` are built exactly once. + */ +export const loadEmojiData = (): Promise => { + if (!emojiDataPromise) { + emojiDataPromise = (async (): Promise => { + const [emojisModule, joypixelsModule, emojibaseModule] = await Promise.all([ + import('emojibase-data/en/compact.json'), + import('emojibase-data/en/shortcodes/joypixels.json'), + import('emojibase-data/en/shortcodes/emojibase.json'), + ]); - const groupIndex = getGroupIndex(em); - if (groupIndex !== undefined) { - addEmojiToGroup(groupIndex, em); - emojis.push(em); + joypixelsShortcodes = joypixelsModule.default as ShortcodeMap; + emojibaseShortcodes = emojibaseModule.default as ShortcodeMap; + + const emojisData = emojisModule.default as unknown as CompactEmoji[]; + emojisData.forEach((emoji) => { + const myShortCodes = getShortcodesFor(emoji.hexcode); + if (!myShortCodes) return; + if (Array.isArray(myShortCodes) && myShortCodes.length === 0) return; + + const em: IEmoji = { + ...emoji, + shortcode: Array.isArray(myShortCodes) ? myShortCodes[0] : myShortCodes, + shortcodes: Array.isArray(myShortCodes) ? myShortCodes : emoji.shortcodes, + }; + + const groupIndex = getGroupIndex(em); + if (groupIndex !== undefined) { + addEmojiToGroup(groupIndex, em); + emojis.push(em); + } + }); + + return { emojis, emojiGroups }; + })(); + // Don't cache a rejection: a transient chunk-load failure (e.g. mid-deploy + // 404) would otherwise permanently disable emoji data until a full reload. + emojiDataPromise = emojiDataPromise.catch((err) => { + emojiDataPromise = undefined; + throw err; + }); } -}); + return emojiDataPromise; +}; diff --git a/src/app/plugins/react-custom-html-parser.tsx b/src/app/plugins/react-custom-html-parser.tsx index 35bc4bff1..f21f095ec 100644 --- a/src/app/plugins/react-custom-html-parser.tsx +++ b/src/app/plugins/react-custom-html-parser.tsx @@ -577,12 +577,12 @@ export const getReactCustomHtmlParser = ( return ( - + ); } - if (htmlSrc) return ; + if (htmlSrc) return ; } } diff --git a/src/app/plugins/react-prism/ReactPrism.tsx b/src/app/plugins/react-prism/ReactPrism.tsx index ab2e9320e..47320a0fb 100644 --- a/src/app/plugins/react-prism/ReactPrism.tsx +++ b/src/app/plugins/react-prism/ReactPrism.tsx @@ -2,307 +2,33 @@ import React, { MutableRefObject, ReactNode, useEffect, useRef } from 'react'; import Prism from 'prismjs'; -import 'prismjs/components/prism-abap.js'; -import 'prismjs/components/prism-abnf.js'; -import 'prismjs/components/prism-actionscript.js'; -import 'prismjs/components/prism-ada.js'; -import 'prismjs/components/prism-agda.js'; -import 'prismjs/components/prism-al.js'; -import 'prismjs/components/prism-antlr4.js'; -import 'prismjs/components/prism-apacheconf.js'; -import 'prismjs/components/prism-apex.js'; -import 'prismjs/components/prism-apl.js'; -import 'prismjs/components/prism-applescript.js'; -import 'prismjs/components/prism-aql.js'; -import 'prismjs/components/prism-arff.js'; -import 'prismjs/components/prism-armasm.js'; -import 'prismjs/components/prism-arturo.js'; -import 'prismjs/components/prism-asciidoc.js'; -import 'prismjs/components/prism-asm6502.js'; -import 'prismjs/components/prism-asmatmel.js'; -import 'prismjs/components/prism-aspnet.js'; -import 'prismjs/components/prism-autohotkey.js'; -import 'prismjs/components/prism-autoit.js'; -import 'prismjs/components/prism-avisynth.js'; -import 'prismjs/components/prism-avro-idl.js'; -import 'prismjs/components/prism-awk.js'; -import 'prismjs/components/prism-bash.js'; -import 'prismjs/components/prism-basic.js'; -import 'prismjs/components/prism-batch.js'; -import 'prismjs/components/prism-bbcode.js'; -import 'prismjs/components/prism-bbj.js'; -import 'prismjs/components/prism-bicep.js'; -import 'prismjs/components/prism-birb.js'; -import 'prismjs/components/prism-bnf.js'; -import 'prismjs/components/prism-bqn.js'; -import 'prismjs/components/prism-brainfuck.js'; -import 'prismjs/components/prism-brightscript.js'; -import 'prismjs/components/prism-bro.js'; -import 'prismjs/components/prism-bsl.js'; -import 'prismjs/components/prism-c.js'; -import 'prismjs/components/prism-cfscript.js'; -import 'prismjs/components/prism-cil.js'; -import 'prismjs/components/prism-cilkc.js'; -import 'prismjs/components/prism-cilkcpp.js'; +// PERF: Prism used to import every bundled language (~574 KB lazy chunk). We now +// ship a curated subset covering the languages actually seen in chat. Imports +// MUST stay in dependency order (Prism component files assume their base grammar +// is already registered): base grammars (markup/css/clike/javascript) first, +// then languages that extend them (e.g. c→cpp, javascript→typescript, +// markup+javascript→jsx, jsx+typescript→tsx, markup→markdown). +import 'prismjs/components/prism-markup.js'; // markup / html / xml / svg +import 'prismjs/components/prism-css.js'; import 'prismjs/components/prism-clike.js'; -import 'prismjs/components/prism-clojure.js'; -import 'prismjs/components/prism-cmake.js'; -import 'prismjs/components/prism-cobol.js'; -import 'prismjs/components/prism-coffeescript.js'; -import 'prismjs/components/prism-concurnas.js'; -import 'prismjs/components/prism-cooklang.js'; -import 'prismjs/components/prism-coq.js'; +import 'prismjs/components/prism-javascript.js'; // js +import 'prismjs/components/prism-json.js'; +import 'prismjs/components/prism-yaml.js'; +import 'prismjs/components/prism-bash.js'; // bash / shell / sh +import 'prismjs/components/prism-python.js'; +import 'prismjs/components/prism-rust.js'; +import 'prismjs/components/prism-go.js'; +import 'prismjs/components/prism-java.js'; +import 'prismjs/components/prism-c.js'; import 'prismjs/components/prism-cpp.js'; import 'prismjs/components/prism-csharp.js'; -import 'prismjs/components/prism-cshtml.js'; -import 'prismjs/components/prism-csp.js'; -import 'prismjs/components/prism-css-extras.js'; -import 'prismjs/components/prism-css.js'; -import 'prismjs/components/prism-csv.js'; -import 'prismjs/components/prism-cue.js'; -import 'prismjs/components/prism-cypher.js'; -import 'prismjs/components/prism-d.js'; -import 'prismjs/components/prism-dart.js'; -import 'prismjs/components/prism-dataweave.js'; -import 'prismjs/components/prism-dax.js'; -import 'prismjs/components/prism-dhall.js'; -import 'prismjs/components/prism-diff.js'; -import 'prismjs/components/prism-dns-zone-file.js'; -import 'prismjs/components/prism-docker.js'; -import 'prismjs/components/prism-dot.js'; -import 'prismjs/components/prism-ebnf.js'; -import 'prismjs/components/prism-editorconfig.js'; -import 'prismjs/components/prism-eiffel.js'; -import 'prismjs/components/prism-ejs.js'; -import 'prismjs/components/prism-elixir.js'; -import 'prismjs/components/prism-elm.js'; -import 'prismjs/components/prism-erb.js'; -import 'prismjs/components/prism-erlang.js'; -import 'prismjs/components/prism-etlua.js'; -import 'prismjs/components/prism-excel-formula.js'; -import 'prismjs/components/prism-factor.js'; -import 'prismjs/components/prism-false.js'; -import 'prismjs/components/prism-firestore-security-rules.js'; -import 'prismjs/components/prism-flow.js'; -import 'prismjs/components/prism-fortran.js'; -import 'prismjs/components/prism-fsharp.js'; -import 'prismjs/components/prism-ftl.js'; -import 'prismjs/components/prism-gap.js'; -import 'prismjs/components/prism-gcode.js'; -import 'prismjs/components/prism-gdscript.js'; -import 'prismjs/components/prism-gedcom.js'; -import 'prismjs/components/prism-gettext.js'; -import 'prismjs/components/prism-gherkin.js'; -import 'prismjs/components/prism-git.js'; -import 'prismjs/components/prism-glsl.js'; -import 'prismjs/components/prism-gml.js'; -import 'prismjs/components/prism-gn.js'; -import 'prismjs/components/prism-go-module.js'; -import 'prismjs/components/prism-go.js'; -import 'prismjs/components/prism-gradle.js'; -import 'prismjs/components/prism-graphql.js'; -import 'prismjs/components/prism-groovy.js'; -import 'prismjs/components/prism-haml.js'; -import 'prismjs/components/prism-handlebars.js'; -import 'prismjs/components/prism-haskell.js'; -import 'prismjs/components/prism-haxe.js'; -import 'prismjs/components/prism-hcl.js'; -import 'prismjs/components/prism-hlsl.js'; -import 'prismjs/components/prism-hoon.js'; -import 'prismjs/components/prism-hpkp.js'; -import 'prismjs/components/prism-hsts.js'; -import 'prismjs/components/prism-http.js'; -import 'prismjs/components/prism-ichigojam.js'; -import 'prismjs/components/prism-icon.js'; -import 'prismjs/components/prism-icu-message-format.js'; -import 'prismjs/components/prism-idris.js'; -import 'prismjs/components/prism-iecst.js'; -import 'prismjs/components/prism-ignore.js'; -import 'prismjs/components/prism-inform7.js'; -import 'prismjs/components/prism-ini.js'; -import 'prismjs/components/prism-io.js'; -import 'prismjs/components/prism-j.js'; -import 'prismjs/components/prism-java.js'; -import 'prismjs/components/prism-javadoclike.js'; -import 'prismjs/components/prism-javascript.js'; -import 'prismjs/components/prism-javastacktrace.js'; -import 'prismjs/components/prism-jexl.js'; -import 'prismjs/components/prism-jolie.js'; -import 'prismjs/components/prism-jq.js'; -import 'prismjs/components/prism-js-extras.js'; -import 'prismjs/components/prism-js-templates.js'; -import 'prismjs/components/prism-json.js'; -import 'prismjs/components/prism-json5.js'; -import 'prismjs/components/prism-jsonp.js'; -import 'prismjs/components/prism-jsstacktrace.js'; -import 'prismjs/components/prism-jsx.js'; -import 'prismjs/components/prism-julia.js'; -import 'prismjs/components/prism-keepalived.js'; -import 'prismjs/components/prism-keyman.js'; -import 'prismjs/components/prism-kotlin.js'; -import 'prismjs/components/prism-kumir.js'; -import 'prismjs/components/prism-kusto.js'; -import 'prismjs/components/prism-latex.js'; -import 'prismjs/components/prism-latte.js'; -import 'prismjs/components/prism-less.js'; -import 'prismjs/components/prism-lilypond.js'; -import 'prismjs/components/prism-linker-script.js'; -import 'prismjs/components/prism-liquid.js'; -import 'prismjs/components/prism-lisp.js'; -import 'prismjs/components/prism-livescript.js'; -import 'prismjs/components/prism-llvm.js'; -import 'prismjs/components/prism-log.js'; -import 'prismjs/components/prism-lolcode.js'; -import 'prismjs/components/prism-lua.js'; -import 'prismjs/components/prism-magma.js'; -import 'prismjs/components/prism-makefile.js'; -import 'prismjs/components/prism-markdown.js'; -import 'prismjs/components/prism-markup-templating.js'; -import 'prismjs/components/prism-markup.js'; -import 'prismjs/components/prism-mata.js'; -import 'prismjs/components/prism-matlab.js'; -import 'prismjs/components/prism-maxscript.js'; -import 'prismjs/components/prism-mel.js'; -import 'prismjs/components/prism-mermaid.js'; -import 'prismjs/components/prism-metafont.js'; -import 'prismjs/components/prism-mizar.js'; -import 'prismjs/components/prism-mongodb.js'; -import 'prismjs/components/prism-monkey.js'; -import 'prismjs/components/prism-moonscript.js'; -import 'prismjs/components/prism-n1ql.js'; -import 'prismjs/components/prism-n4js.js'; -import 'prismjs/components/prism-nand2tetris-hdl.js'; -import 'prismjs/components/prism-naniscript.js'; -import 'prismjs/components/prism-nasm.js'; -import 'prismjs/components/prism-neon.js'; -import 'prismjs/components/prism-nevod.js'; -import 'prismjs/components/prism-nginx.js'; -import 'prismjs/components/prism-nim.js'; -import 'prismjs/components/prism-nix.js'; -import 'prismjs/components/prism-nsis.js'; -import 'prismjs/components/prism-objectivec.js'; -import 'prismjs/components/prism-ocaml.js'; -import 'prismjs/components/prism-odin.js'; -import 'prismjs/components/prism-opencl.js'; -import 'prismjs/components/prism-openqasm.js'; -import 'prismjs/components/prism-oz.js'; -import 'prismjs/components/prism-parigp.js'; -import 'prismjs/components/prism-parser.js'; -import 'prismjs/components/prism-pascal.js'; -import 'prismjs/components/prism-pascaligo.js'; -import 'prismjs/components/prism-pcaxis.js'; -import 'prismjs/components/prism-peoplecode.js'; -import 'prismjs/components/prism-perl.js'; -import 'prismjs/components/prism-php-extras.js'; -import 'prismjs/components/prism-php.js'; -import 'prismjs/components/prism-phpdoc.js'; -import 'prismjs/components/prism-plant-uml.js'; -import 'prismjs/components/prism-powerquery.js'; -import 'prismjs/components/prism-powershell.js'; -import 'prismjs/components/prism-processing.js'; -import 'prismjs/components/prism-prolog.js'; -import 'prismjs/components/prism-promql.js'; -import 'prismjs/components/prism-properties.js'; -import 'prismjs/components/prism-protobuf.js'; -import 'prismjs/components/prism-psl.js'; -import 'prismjs/components/prism-pug.js'; -import 'prismjs/components/prism-puppet.js'; -import 'prismjs/components/prism-pure.js'; -import 'prismjs/components/prism-purebasic.js'; -import 'prismjs/components/prism-purescript.js'; -import 'prismjs/components/prism-python.js'; -import 'prismjs/components/prism-q.js'; -import 'prismjs/components/prism-qml.js'; -import 'prismjs/components/prism-qore.js'; -import 'prismjs/components/prism-qsharp.js'; -import 'prismjs/components/prism-r.js'; -import 'prismjs/components/prism-reason.js'; -import 'prismjs/components/prism-regex.js'; -import 'prismjs/components/prism-rego.js'; -import 'prismjs/components/prism-renpy.js'; -import 'prismjs/components/prism-rescript.js'; -import 'prismjs/components/prism-rest.js'; -import 'prismjs/components/prism-rip.js'; -import 'prismjs/components/prism-roboconf.js'; -import 'prismjs/components/prism-robotframework.js'; -import 'prismjs/components/prism-ruby.js'; -import 'prismjs/components/prism-rust.js'; -import 'prismjs/components/prism-sas.js'; -import 'prismjs/components/prism-sass.js'; -import 'prismjs/components/prism-scala.js'; -import 'prismjs/components/prism-scheme.js'; -import 'prismjs/components/prism-scss.js'; -import 'prismjs/components/prism-shell-session.js'; -import 'prismjs/components/prism-smali.js'; -import 'prismjs/components/prism-smalltalk.js'; -import 'prismjs/components/prism-smarty.js'; -import 'prismjs/components/prism-sml.js'; -import 'prismjs/components/prism-solidity.js'; -import 'prismjs/components/prism-solution-file.js'; -import 'prismjs/components/prism-soy.js'; -import 'prismjs/components/prism-splunk-spl.js'; -import 'prismjs/components/prism-sqf.js'; import 'prismjs/components/prism-sql.js'; -import 'prismjs/components/prism-squirrel.js'; -import 'prismjs/components/prism-stan.js'; -import 'prismjs/components/prism-stata.js'; -import 'prismjs/components/prism-stylus.js'; -import 'prismjs/components/prism-supercollider.js'; -import 'prismjs/components/prism-swift.js'; -import 'prismjs/components/prism-systemd.js'; -import 'prismjs/components/prism-t4-templating.js'; -import 'prismjs/components/prism-t4-vb.js'; -import 'prismjs/components/prism-tap.js'; -import 'prismjs/components/prism-tcl.js'; -import 'prismjs/components/prism-textile.js'; -import 'prismjs/components/prism-toml.js'; -import 'prismjs/components/prism-tremor.js'; +import 'prismjs/components/prism-diff.js'; +import 'prismjs/components/prism-docker.js'; +import 'prismjs/components/prism-markdown.js'; +import 'prismjs/components/prism-typescript.js'; // ts +import 'prismjs/components/prism-jsx.js'; import 'prismjs/components/prism-tsx.js'; -import 'prismjs/components/prism-tt2.js'; -import 'prismjs/components/prism-turtle.js'; -import 'prismjs/components/prism-twig.js'; -import 'prismjs/components/prism-typescript.js'; -import 'prismjs/components/prism-typoscript.js'; -import 'prismjs/components/prism-unrealscript.js'; -import 'prismjs/components/prism-uorazor.js'; -import 'prismjs/components/prism-uri.js'; -import 'prismjs/components/prism-v.js'; -import 'prismjs/components/prism-vala.js'; -import 'prismjs/components/prism-vbnet.js'; -import 'prismjs/components/prism-velocity.js'; -import 'prismjs/components/prism-verilog.js'; -import 'prismjs/components/prism-vhdl.js'; -import 'prismjs/components/prism-vim.js'; -import 'prismjs/components/prism-visual-basic.js'; -import 'prismjs/components/prism-warpscript.js'; -import 'prismjs/components/prism-wasm.js'; -import 'prismjs/components/prism-web-idl.js'; -import 'prismjs/components/prism-wgsl.js'; -import 'prismjs/components/prism-wiki.js'; -import 'prismjs/components/prism-wolfram.js'; -import 'prismjs/components/prism-wren.js'; -import 'prismjs/components/prism-xeora.js'; -import 'prismjs/components/prism-xml-doc.js'; -import 'prismjs/components/prism-xojo.js'; -import 'prismjs/components/prism-xquery.js'; -import 'prismjs/components/prism-yaml.js'; -import 'prismjs/components/prism-yang.js'; -import 'prismjs/components/prism-zig.js'; -import 'prismjs/components/prism-arduino.js'; - -// Broken: -// -// import 'prismjs/components/prism-bison.js'; -// import 'prismjs/components/prism-chaiscript.js'; -// import 'prismjs/components/prism-core.js'; -// import 'prismjs/components/prism-crystal.js'; -// import 'prismjs/components/prism-django.js'; -// import 'prismjs/components/prism-javadoc.js'; -// import 'prismjs/components/prism-jsdoc.js'; -// import 'prismjs/components/prism-plsql.js'; -// import 'prismjs/components/prism-racket.js'; -// import 'prismjs/components/prism-sparql.js'; -// import 'prismjs/components/prism-t4-cs.js'; import './ReactPrism.css'; // using classNames .prism-dark .prism-light from ReactPrism.css diff --git a/src/app/plugins/recent-emoji.test.ts b/src/app/plugins/recent-emoji.test.ts index c23f99a6b..93a543132 100644 --- a/src/app/plugins/recent-emoji.test.ts +++ b/src/app/plugins/recent-emoji.test.ts @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import { MatrixClient, MatrixEvent } from 'matrix-js-sdk'; import { addRecentEmoji, getRecentEmojis, IRecentEmojiContent } from './recent-emoji'; import { AccountDataEvent } from '../../types/matrix/accountData'; -import { emojis } from './emoji'; +import { emojis, loadEmojiData } from './emoji'; // A Map-backed MatrixClient stub supporting get/setAccountData. const createMx = () => { @@ -25,6 +25,9 @@ const createMx = () => { const getStored = (store: Map): IRecentEmojiContent['recent_emoji'] => (store.get(AccountDataEvent.ElementRecentEmoji) as IRecentEmojiContent | undefined)?.recent_emoji; +// Emoji data is now loaded lazily; populate `emojis` before the round trips. +await loadEmojiData(); + // Pick two real unicode emojis to drive add->get round trips. const u1 = emojis[0].unicode; const u2 = emojis[1].unicode; diff --git a/src/sw.ts b/src/sw.ts index b098fd915..2b713dda2 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -1,7 +1,39 @@ /// +import { precacheAndRoute, type PrecacheEntry } from 'workbox-precaching'; + export type {}; -declare const self: ServiceWorkerGlobalScope; +declare const self: ServiceWorkerGlobalScope & { + // Replaced at build time by vite-plugin-pwa (injectManifest) with the list of + // hashed build assets to precache. See vite.config.js VitePWA injectManifest. + __WB_MANIFEST: Array; +}; + +/** + * PRECACHE (workbox-precaching). `self.__WB_MANIFEST` is replaced at build time + * by vite-plugin-pwa with the list of hashed build assets + * (assets/**\/*.{js,css,wasm}; see vite.config.js injectManifest.globPatterns). + * + * DEPLOY-SAFETY INVARIANTS (do not break): + * 1. index.html / navigations are NEVER precached or precache-routed. The + * manifest globs only `assets/**` (content-hashed), so index.html (served + * from the app root) is absent from it and navigation requests fall through + * to the network — a new deploy is picked up immediately, no stale SPA + * shell. We deliberately do NOT register a navigation route / + * createHandlerBoundToURL fallback. + * 2. precacheAndRoute only matches its own manifest URLs (same-origin hashed + * assets). It never matches the media-auth paths handled by the fetch + * listener below — those are cross-origin homeserver URLs absent from the + * manifest — so the existing media fetch behaviour is fully preserved. It + * is registered before that listener; for a media request the precache + * route finds no match and does not call respondWith, so the media handler + * still runs. + * 3. Assets are content-hashed, so a changed asset ships under a new filename; + * PrecacheController drops entries no longer in the current manifest on + * activate, so the precache self-updates each deploy without unbounded + * growth. + */ +precacheAndRoute(self.__WB_MANIFEST); type SessionInfo = { accessToken: string; diff --git a/vite.config.js b/vite.config.js index ac4b42d51..0f75a35d0 100644 --- a/vite.config.js +++ b/vite.config.js @@ -261,7 +261,19 @@ export default defineConfig({ injectRegister: false, manifest: false, injectManifest: { - injectionPoint: undefined, + // PRECACHE (P5): emit `self.__WB_MANIFEST` into src/sw.ts so it can + // precacheAndRoute the hashed build assets. index.html is deliberately + // EXCLUDED from the manifest (globs only `assets/**`) so navigations + // stay network-first and a new deploy is picked up immediately — see + // the deploy-safety invariants documented in src/sw.ts. + injectionPoint: 'self.__WB_MANIFEST', + globPatterns: ['assets/**/*.{js,css,wasm}'], + // Assets are content-hashed, so the filename is the cache key — don't + // append a revision cache-busting param. + dontCacheBustURLsMatching: /assets\//, + // Raised above the 2 MB default so the ~5.5 MB matrix-sdk crypto wasm + // (hash-busted and hot on every session) is precached deliberately. + maximumFileSizeToCacheInBytes: 6 * 1024 * 1024, // codeSplitting: false is not yet supported by vite-plugin-pwa 1.3.0; // the inlineDynamicImports deprecation warning from Vite is from pwa internal build },