diff --git a/package-lock.json b/package-lock.json
index 2b466965..8aa6a34c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,7 +34,6 @@
"browser-encrypt-attachment": "0.3.0",
"chroma-js": "3.1.2",
"classnames": "2.3.2",
- "dateformat": "5.0.3",
"dayjs": "1.11.10",
"domhandler": "5.0.3",
"emojibase": "15.3.1",
@@ -116,7 +115,7 @@
"wait-on": "9.0.10"
},
"engines": {
- "node": ">=22.0.0"
+ "node": ">=22.12.0"
}
},
"node_modules/@ampproject/remapping": {
@@ -8266,14 +8265,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/dateformat": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-5.0.3.tgz",
- "integrity": "sha512-Kvr6HmPXUMerlLcLF+Pwq3K7apHpYmGDVqrxcDasBg86UcKeTSNWbEzU8bwdXnxnR44FtMhJAxI4Bov6Y/KUfA==",
- "engines": {
- "node": ">=12.20"
- }
- },
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
diff --git a/package.json b/package.json
index a73f08a9..7d7d3552 100644
--- a/package.json
+++ b/package.json
@@ -76,7 +76,6 @@
"browser-encrypt-attachment": "0.3.0",
"chroma-js": "3.1.2",
"classnames": "2.3.2",
- "dateformat": "5.0.3",
"dayjs": "1.11.10",
"domhandler": "5.0.3",
"emojibase": "15.3.1",
diff --git a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
index d358ff7d..a2387eb8 100644
--- a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
+++ b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx
@@ -11,7 +11,8 @@ 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 } from '../../../plugins/emoji';
+import { emojis } from '../../../plugins/emoji-data';
import { useKeyDown } from '../../../hooks/useKeyDown';
import { mxcUrlToHttp } from '../../../utils/matrix';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx
index d5a76c71..be10a212 100644
--- a/src/app/components/emoji-board/EmojiBoard.tsx
+++ b/src/app/components/emoji-board/EmojiBoard.tsx
@@ -15,7 +15,8 @@ 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 { IEmoji } from '../../plugins/emoji';
+import { emojiGroups, emojis } from '../../plugins/emoji-data';
import { useEmojiGroupLabels } from './useEmojiGroupLabels';
import { useEmojiGroupIcons } from './useEmojiGroupIcons';
import { preventScrollWithArrowKey, stopPropagation } from '../../utils/keyboard';
diff --git a/src/app/components/mobile-tabs-pager/MobileTabsLayout.tsx b/src/app/components/mobile-tabs-pager/MobileTabsLayout.tsx
index 5f1cfcb5..d7ed9ccc 100644
--- a/src/app/components/mobile-tabs-pager/MobileTabsLayout.tsx
+++ b/src/app/components/mobile-tabs-pager/MobileTabsLayout.tsx
@@ -1,9 +1,17 @@
-import React from 'react';
+import React, { Suspense } from 'react';
import { Outlet, useMatch } from 'react-router-dom';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { isNativePlatform } from '../../utils/capacitor';
import { BOTS_PATH, CHANNELS_PATH, CHANNELS_SPACE_PATH, DIRECT_PATH } from '../../pages/paths';
-import { MobileTabsPager } from './MobileTabsPager';
+
+// MobileTabsPager is the ONLY static importer of the Channels and Bots feature
+// modules. It renders only on `mobile && native` (below), so lazy-loading it
+// here removes the static edge that otherwise pinned Channels + Bots into the
+// boot bundle for every target. On native the chunk streams from the local APK
+// filesystem (no network), so a null Suspense fallback is imperceptible.
+const MobileTabsPager = React.lazy(() =>
+ import('./MobileTabsPager').then((m) => ({ default: m.MobileTabsPager }))
+);
// Router-level wrapper around the three listing tabs (/direct/,
// /channels/, /bots/). When all of (mobile breakpoint, Capacitor
@@ -44,5 +52,9 @@ export function MobileTabsLayout() {
if (!(mobile && native) || !onListingRoot) {
return ;
}
- return ;
+ return (
+
+
+
+ );
}
diff --git a/src/app/components/uia-stages/ReCaptchaStage.tsx b/src/app/components/uia-stages/ReCaptchaStage.tsx
index 68b3fcf4..78dcc98f 100644
--- a/src/app/components/uia-stages/ReCaptchaStage.tsx
+++ b/src/app/components/uia-stages/ReCaptchaStage.tsx
@@ -1,9 +1,13 @@
-import React from 'react';
+import React, { Suspense } from 'react';
import { Dialog, Text, Box, Button, config } from 'folds';
import { AuthType } from 'matrix-js-sdk';
-import ReCAPTCHA from 'react-google-recaptcha';
import { StageComponentProps } from './types';
+// react-google-recaptcha (+ its grecaptcha loader) is only ever rendered in
+// the registration UIA captcha stage — a cold, rare path. Lazy-loading it
+// keeps it out of the boot bundle.
+const ReCAPTCHA = React.lazy(() => import('react-google-recaptcha'));
+
function ReCaptchaErrorDialog({
title,
message,
@@ -57,7 +61,9 @@ export function ReCaptchaStageDialog({ stageData, submitAuthDict, onCancel }: St
);
diff --git a/src/app/features/common-settings/permissions/PowersEditor.tsx b/src/app/features/common-settings/permissions/PowersEditor.tsx
index c94a0f7b..c3fc3c7b 100644
--- a/src/app/features/common-settings/permissions/PowersEditor.tsx
+++ b/src/app/features/common-settings/permissions/PowersEditor.tsx
@@ -1,4 +1,11 @@
-import React, { FormEventHandler, MouseEventHandler, useCallback, useMemo, useState } from 'react';
+import React, {
+ FormEventHandler,
+ MouseEventHandler,
+ Suspense,
+ useCallback,
+ useMemo,
+ useState,
+} from 'react';
import {
Box,
Text,
@@ -37,7 +44,6 @@ import { useRoom } from '../../../hooks/useRoom';
import { HexColorPickerPopOut } from '../../../components/HexColorPickerPopOut';
import { PowerColorBadge, PowerIcon } from '../../../components/power';
import { UseStateProvider } from '../../../components/UseStateProvider';
-import { EmojiBoard } from '../../../components/emoji-board';
import { useImagePackRooms } from '../../../hooks/useImagePackRooms';
import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
@@ -52,6 +58,17 @@ import { BetaNoticeBadge } from '../../../components/BetaNoticeBadge';
import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag';
import { creatorsSupported } from '../../../utils/matrix';
+// Lazy-loaded: EmojiBoard pulls the heavy emoji picker dataset (`emoji-data`,
+// ~506 KB compact.json). PowersEditor is reachable from the boot graph
+// (RoomSettingsRenderer / SpaceSettingsRenderer are mounted at the app root),
+// so a static import here would drag `emoji-data` into the initial bundle. The
+// board only mounts inside the PopOut below when the user opens the icon
+// picker, so deferring it keeps `emoji-data` off boot (the chat composer's own
+// static EmojiBoard import keeps the chunk in the lazy Room bundle).
+const EmojiBoard = React.lazy(() =>
+ import('../../../components/emoji-board').then((m) => ({ default: m.EmojiBoard }))
+);
+
type EditPowerProps = {
maxPower: number;
power?: number;
@@ -208,23 +225,25 @@ function EditPower({ maxPower, power, tag, onSave, onClose }: EditPowerProps) {
position="Bottom"
anchor={cords}
content={
- {
- setTagIcon({ key });
- setCords(undefined);
- }}
- onCustomEmojiSelect={(mxc) => {
- setTagIcon({ key: mxc });
- setCords(undefined);
- }}
- requestClose={() => {
- setCords(undefined);
- }}
- />
+
+ {
+ setTagIcon({ key });
+ setCords(undefined);
+ }}
+ onCustomEmojiSelect={(mxc) => {
+ setTagIcon({ key: mxc });
+ setCords(undefined);
+ }}
+ requestClose={() => {
+ setCords(undefined);
+ }}
+ />
+
}
>