diff --git a/apps/web/src/api/mutations/sign-transfer.ts b/apps/web/src/api/mutations/sign-transfer.ts
index f0f6d488b0..ef5dbb8b95 100644
--- a/apps/web/src/api/mutations/sign-transfer.ts
+++ b/apps/web/src/api/mutations/sign-transfer.ts
@@ -3,7 +3,7 @@
import { UseQueryResult } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { hpToVests } from "@/features/shared/transfer/hp-to-vests";
-import { TransferAsset, TransferMode } from "@/features/shared";
+import type { TransferAsset, TransferMode } from "@/features/shared/transfer";
import { DEFAULT_DYNAMIC_PROPS } from "@/consts/default-dynamic-props";
import { getDynamicPropsQueryOptions } from "@ecency/sdk";
import { useActiveAccount } from "@/core/hooks/use-active-account";
diff --git a/apps/web/src/api/sdk-mutations/use-upload-image-mutation.ts b/apps/web/src/api/sdk-mutations/use-upload-image-mutation.ts
index 585493a25a..01f211bbe2 100644
--- a/apps/web/src/api/sdk-mutations/use-upload-image-mutation.ts
+++ b/apps/web/src/api/sdk-mutations/use-upload-image-mutation.ts
@@ -4,7 +4,7 @@ import { useAddImage, useUploadImage } from "@ecency/sdk";
import { useActiveUsername } from "@/core/hooks/use-active-username";
import { ensureValidToken } from "@/utils";
import { useMutation } from "@tanstack/react-query";
-import { error, success } from "@/features/shared";
+import { error, success } from "@/features/shared/feedback";
import i18next from "i18next";
import { EcencyConfigManager } from "@/config";
diff --git a/apps/web/src/app/(dynamicPages)/entry/[category]/[author]/[permlink]/_components/entry-page-body-viewer.tsx b/apps/web/src/app/(dynamicPages)/entry/[category]/[author]/[permlink]/_components/entry-page-body-viewer.tsx
index d3a82bbffd..bc6ab9e8ff 100644
--- a/apps/web/src/app/(dynamicPages)/entry/[category]/[author]/[permlink]/_components/entry-page-body-viewer.tsx
+++ b/apps/web/src/app/(dynamicPages)/entry/[category]/[author]/[permlink]/_components/entry-page-body-viewer.tsx
@@ -5,9 +5,9 @@ import { SelectionPopover } from "./selection-popover";
import { EntryPageViewerManager } from "./entry-page-viewer-manager";
import { useContext, useEffect, useState } from "react";
import { SafeTweet } from "@/features/shared/safe-tweet";
-import TransactionSigner from "@/features/shared/transactions/transaction-signer";
import { EntryPageContext } from "./context";
import dynamic from "next/dynamic";
+import { DialogChunkSpinner } from "@/features/shared/dialog-chunk-spinner";
import { makeEntryPath } from "@/utils";
import i18next from "i18next";
@@ -15,6 +15,14 @@ import i18next from "i18next";
// polls, gif picker). It only renders in edit mode, so keep it out of the
// read-path bundle and load it on demand when the user enters editing. The
// loading fallback avoids a blank slot during the first chunk download.
+// Interaction-only: the signing dialog opens when a reader clicks an embedded
+// hive:// operation link. Keep it out of the read-path bundle (#1668) and load
+// it on demand; `show` is false until then, so nothing renders during SSR.
+const TransactionSigner = dynamic(
+ () => import("@/features/shared/transactions/transaction-signer"),
+ { ssr: false, loading: () => }
+);
+
const EntryPageEdit = dynamic(
() => import("./entry-page-edit").then((m) => m.EntryPageEdit),
{
@@ -152,11 +160,13 @@ export function EntryPageBodyViewer({ entry }: Props) {
)}
{isEdit && entry.parent_author && }
- setSigningOperation(undefined)}
- operation={signingOperation}
- />
+ {signingOperation !== undefined && (
+ setSigningOperation(undefined)}
+ operation={signingOperation}
+ />
+ )}
);
}
diff --git a/apps/web/src/app/(dynamicPages)/entry/[category]/[author]/[permlink]/page.tsx b/apps/web/src/app/(dynamicPages)/entry/[category]/[author]/[permlink]/page.tsx
index 3c2ffb35bb..617b8e5464 100644
--- a/apps/web/src/app/(dynamicPages)/entry/[category]/[author]/[permlink]/page.tsx
+++ b/apps/web/src/app/(dynamicPages)/entry/[category]/[author]/[permlink]/page.tsx
@@ -271,11 +271,27 @@ export default async function EntryPage({ params, searchParams }: Props) {
its bounded feed fetches stream as a later chunk instead of
gating the post body's flush / LCP; the anchors still ship in
the same streamed HTML response. */}
+ {/*
+ content-visibility on the two big below-fold sections keeps
+ them out of the first layout/paint pass; the intrinsic-size
+ hints reserve approximate scroll height until they render.
+ The discussions hint matches the ANON SSR shell (heading +
+ "Show N comments" button, ~150px), not loaded comments: on a
+ zero-comment post an oversized hint would reserve blank space
+ and visibly collapse (#1669 review). The auto keyword memoizes
+ the real size after first render.
+ Both wrappers start off-screen on any normal post, which is
+ the precondition for content-visibility:auto to help (#1668).
+ */}
-
+
+
+
-
+
+
+
diff --git a/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-referrals/profile-referrals-table.tsx b/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-referrals/profile-referrals-table.tsx
index 37bb033790..b4ce57f797 100644
--- a/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-referrals/profile-referrals-table.tsx
+++ b/apps/web/src/app/(dynamicPages)/profile/[username]/_components/profile-referrals/profile-referrals-table.tsx
@@ -2,7 +2,8 @@ import { Table, Td, Th, Tr } from "@ui/table";
import dayjs from "@/utils/dayjs";
import React, { useCallback, useMemo, useState } from "react";
import i18next from "i18next";
-import { ProfileLink, Transfer, TransferAsset, TransferMode, UserAvatar } from "@/features/shared";
+import { ProfileLink, UserAvatar } from "@/features/shared";
+import { Transfer, TransferAsset, TransferMode } from "@/features/shared/transfer";
import { getReferralsInfiniteQueryOptions } from "@ecency/sdk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Account } from "@/entities";
diff --git a/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/account-recovery.tsx b/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/account-recovery.tsx
index 159819f9f8..3b402d1122 100644
--- a/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/account-recovery.tsx
+++ b/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/account-recovery.tsx
@@ -2,7 +2,8 @@
import { formatError } from "@/api/format-error";
import { useActiveAccount } from "@/core/hooks/use-active-account";
-import { error, KeyOrHot, success } from "@/features/shared";
+import { error, success } from "@/features/shared";
+import { KeyOrHot } from "@/features/shared/key-or-hot";
import {
getAccountFullQueryOptions,
getAccountPendingRecoveryQueryOptions,
diff --git a/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/add-keys-steps/step-4-confirm.tsx b/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/add-keys-steps/step-4-confirm.tsx
index eece0bd3f4..38cd1096a9 100644
--- a/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/add-keys-steps/step-4-confirm.tsx
+++ b/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/add-keys-steps/step-4-confirm.tsx
@@ -1,7 +1,8 @@
import { formatError } from "@/api/format-error";
import { updateAccountKeysCache } from "@/api/mutations/update-account-keys-cache";
import { useActiveAccount } from "@/core/hooks/use-active-account";
-import { error, success, KeyOrHot } from "@/features/shared";
+import { error, success } from "@/features/shared";
+import { KeyOrHot } from "@/features/shared/key-or-hot";
import { Button } from "@/features/ui";
import { getAccountFullQueryOptions, dedupeAndSortKeyAuths } from "@ecency/sdk";
import { deriveHiveMasterPasswordKeys } from "@ecency/wallets";
diff --git a/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-authorities.tsx b/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-authorities.tsx
index 74992d46ac..5aab2849ea 100644
--- a/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-authorities.tsx
+++ b/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-authorities.tsx
@@ -1,7 +1,8 @@
"use client";
import { useActiveAccount } from "@/core/hooks/use-active-account";
-import { error, KeyOrHot, UserAvatar } from "@/features/shared";
+import { error, UserAvatar } from "@/features/shared";
+import { KeyOrHot } from "@/features/shared/key-or-hot";
import { ProfilePreview } from "@/features/shared/profile-popover/profile-preview";
import { Popover } from "@/features/ui";
import { getAccountFullQueryOptions, useAccountRevokePosting } from "@ecency/sdk";
diff --git a/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys-dialog.tsx b/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys-dialog.tsx
index 751200e84f..26c5a4ffbe 100644
--- a/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys-dialog.tsx
+++ b/apps/web/src/app/(dynamicPages)/profile/[username]/permissions/_components/manage-keys-dialog.tsx
@@ -1,7 +1,8 @@
import { formatError } from "@/api/format-error";
import { updateAccountKeysCache } from "@/api/mutations/update-account-keys-cache";
import { useActiveAccount } from "@/core/hooks/use-active-account";
-import { error, success, KeyOrHot } from "@/features/shared";
+import { error, success } from "@/features/shared";
+import { KeyOrHot } from "@/features/shared/key-or-hot";
import { Button, Modal, ModalBody, ModalHeader } from "@/features/ui";
import {
getAccountFullQueryOptions,
diff --git a/apps/web/src/app/(dynamicPages)/profile/[username]/wallet/(token)/hp/_components/hp-delegations-card.tsx b/apps/web/src/app/(dynamicPages)/profile/[username]/wallet/(token)/hp/_components/hp-delegations-card.tsx
index 64d057b499..1a57a29c74 100644
--- a/apps/web/src/app/(dynamicPages)/profile/[username]/wallet/(token)/hp/_components/hp-delegations-card.tsx
+++ b/apps/web/src/app/(dynamicPages)/profile/[username]/wallet/(token)/hp/_components/hp-delegations-card.tsx
@@ -14,7 +14,7 @@ import { useQuery } from "@tanstack/react-query";
import { ReceivedVesting } from "./received-vesting-dialog";
import { useEffect, useMemo, useState } from "react";
import { DelegatedVesting } from "./delegated-vesting-dialog";
-import { Transfer } from "@/features/shared";
+import { Transfer } from "@/features/shared/transfer";
import { useActiveAccount } from "@/core/hooks/use-active-account";
import useLocalStorage from "react-use/lib/useLocalStorage";
diff --git a/apps/web/src/app/client-providers.tsx b/apps/web/src/app/client-providers.tsx
index 2738e2a049..56da127aea 100644
--- a/apps/web/src/app/client-providers.tsx
+++ b/apps/web/src/app/client-providers.tsx
@@ -3,6 +3,7 @@
import "@/polyfills";
import "@/core/sdk-init";
import { ClientInit } from "@/app/client-init";
+import { EntryStatsPrefetch } from "@/app/entry-stats-prefetch";
import { EcencyConfigManager } from "@/config";
import { getQueryClient } from "@/core/react-query";
import { QueryClientProvider } from "@tanstack/react-query";
@@ -69,6 +70,10 @@ export function ClientProviders(props: PropsWithChildren) {
>
+ {/* Inside UIManager but OUTSIDE DeferredRender on purpose: its
+ effect must flush with root hydration, not after the lazy
+ feature cascade (#1668). */}
+
{props.children}
{/* Defer non-critical components for LCP optimization */}
diff --git a/apps/web/src/app/communities/create/_components/community-create-sign-step.tsx b/apps/web/src/app/communities/create/_components/community-create-sign-step.tsx
index a57bcb1c60..e208d0b2a7 100644
--- a/apps/web/src/app/communities/create/_components/community-create-sign-step.tsx
+++ b/apps/web/src/app/communities/create/_components/community-create-sign-step.tsx
@@ -3,7 +3,7 @@ import {
useCreateCommunityByHivesigner,
useCreateCommunityByKeychain
} from "@/api/mutations";
-import { KeyOrHot } from "@/features/shared";
+import { KeyOrHot } from "@/features/shared/key-or-hot";
import { PrivateKey } from "@ecency/sdk";
import { useCallback } from "react";
import { CommunityCreateCardLayout } from "./community-create-card-layout";
diff --git a/apps/web/src/app/decks/_components/columns/deck-wallet-column.tsx b/apps/web/src/app/decks/_components/columns/deck-wallet-column.tsx
index 9141b3fd4f..5c219cff2c 100644
--- a/apps/web/src/app/decks/_components/columns/deck-wallet-column.tsx
+++ b/apps/web/src/app/decks/_components/columns/deck-wallet-column.tsx
@@ -10,7 +10,7 @@ import { Transaction } from "@/entities";
import { getDynamicPropsQueryOptions, getTransactionsInfiniteQueryOptions } from "@ecency/sdk";
import i18next from "i18next";
import { useQuery, useInfiniteQuery } from "@tanstack/react-query";
-import { TransactionRow } from "@/features/shared";
+import { TransactionRow } from "@/features/shared/transactions";
interface Props {
id: string;
diff --git a/apps/web/src/app/decks/_components/deck-toolbar/deck-toolbar.tsx b/apps/web/src/app/decks/_components/deck-toolbar/deck-toolbar.tsx
index 5897bd6513..1984704fc7 100644
--- a/apps/web/src/app/decks/_components/deck-toolbar/deck-toolbar.tsx
+++ b/apps/web/src/app/decks/_components/deck-toolbar/deck-toolbar.tsx
@@ -6,7 +6,8 @@ import { DeckToolbarToggleArea } from "./deck-toolbar-toggle-area";
import { DeckToolbarManager } from "./deck-toolbar-manager";
import { DeckToolbarCreate } from "./deck-toolbar-create";
import { useGlobalStore } from "@/core/global-store";
-import { LoginDialog, NotificationHandler, PurchaseQrDialog } from "@/features/shared";
+import { LoginDialog, NotificationHandler } from "@/features/shared";
+import { PurchaseQrDialog } from "@/features/shared/purchase-qr";
import { FragmentsDialog } from "@/features/shared/fragments";
import { SchedulesDialog } from "@/features/shared/schedules";
import { BookmarksDialog } from "@/features/shared/bookmarks";
diff --git a/apps/web/src/app/entry-stats-prefetch.tsx b/apps/web/src/app/entry-stats-prefetch.tsx
new file mode 100644
index 0000000000..3d1e21786c
--- /dev/null
+++ b/apps/web/src/app/entry-stats-prefetch.tsx
@@ -0,0 +1,73 @@
+"use client";
+
+import { useEffect } from "react";
+import { usePathname } from "next/navigation";
+import { useQueryClient } from "@tanstack/react-query";
+import { getPostTipsQueryOptions, getProMembersQueryOptions } from "@ecency/sdk";
+
+/*
+ Warms the two auth-free queries that paint an entry page's final pixels
+ (post tips and pro-members badges), firing with root hydration (#1668).
+
+ Without this, both fetch only when their components mount — and those sit
+ under the route-level streamed Suspense boundary, which React hydrates
+ lazily AFTER the DeferredRender feature cascade, pushing the requests to
+ ~4.5s although hydration completes ~2.9s. Prefetching from the root client
+ tree dedupes cleanly: the later component mounts hit the fresh cache entry
+ (staleTime 60s tips / 5min pro-members).
+
+ Section names below must stay in lockstep with the entry-vs-section split
+ in features/next-middleware/cache-policy.ts, which keeps the union in TWO
+ places: NO_CACHE_PROFILE_SECTIONS (wallet/settings/permissions/referrals/
+ insights) plus the inline section array in its 2-segment entry branch. A
+ future section missing here costs exactly one throwaway 404 GET, nothing
+ more.
+*/
+const PROFILE_SECTIONS = new Set([
+ "wallet",
+ "settings",
+ "permissions",
+ "referrals",
+ "insights",
+ "posts",
+ "blog",
+ "comments",
+ "replies",
+ "communities",
+ "trail",
+ "followers",
+ "following",
+ "rss",
+ "rss.xml",
+ "feed"
+]);
+
+// /@author/permlink or /:category/@author/permlink
+const ENTRY_2 = /^\/@([\w.-]+)\/([a-z0-9-]+)\/?$/;
+const ENTRY_3 = /^\/[^/@]+\/@([\w.-]+)\/([a-z0-9-]+)\/?$/;
+
+export function EntryStatsPrefetch(): null {
+ const pathname = usePathname();
+ const queryClient = useQueryClient();
+
+ useEffect(() => {
+ if (!pathname) {
+ return;
+ }
+ const match = ENTRY_2.exec(pathname) ?? ENTRY_3.exec(pathname);
+ if (!match) {
+ return;
+ }
+ // The query key must match what the entry components build from
+ // entry.author, which is always lowercase on chain.
+ const author = match[1].toLowerCase();
+ const permlink = match[2];
+ if (ENTRY_2.exec(pathname) && PROFILE_SECTIONS.has(permlink)) {
+ return;
+ }
+ void queryClient.prefetchQuery(getPostTipsQueryOptions(author, permlink));
+ void queryClient.prefetchQuery(getProMembersQueryOptions());
+ }, [pathname, queryClient]);
+
+ return null;
+}
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index c0e61a69de..9a6029e52a 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -1,7 +1,7 @@
import "@/styles/style.scss";
import "@/core/sdk-init"; // Initialize SDK DMCA filters immediately (SSR)
import Providers from "@/app/providers";
-import { HiringConsoleLog } from "@/app/_components";
+import { HiringConsoleLog } from "@/app/_components/hiring-console-log";
import { cookies } from "next/headers";
import { Theme } from "@/enums";
import { BannerManager } from "@/features/banners";
diff --git a/apps/web/src/app/market/_components/hive-barter/index.tsx b/apps/web/src/app/market/_components/hive-barter/index.tsx
index cbca3205de..844b0c1083 100644
--- a/apps/web/src/app/market/_components/hive-barter/index.tsx
+++ b/apps/web/src/app/market/_components/hive-barter/index.tsx
@@ -3,7 +3,8 @@ import { FormControl, InputGroup } from "@ui/input";
import { Button } from "@ui/button";
import { Form } from "@ui/form";
import { BuySellHiveTransactionType } from "@/enums";
-import { BuySellHiveDialog, error, Skeleton } from "@/features/shared";
+import { error, Skeleton } from "@/features/shared";
+import { BuySellHiveDialog } from "@/features/shared/buy-sell-hive";
import i18next from "i18next";
interface Props {
diff --git a/apps/web/src/app/market/_components/open-orders/index.tsx b/apps/web/src/app/market/_components/open-orders/index.tsx
index b431e1168b..e839a428b3 100644
--- a/apps/web/src/app/market/_components/open-orders/index.tsx
+++ b/apps/web/src/app/market/_components/open-orders/index.tsx
@@ -3,7 +3,8 @@ import { Button } from "@ui/button";
import { Table, Td, Th, Tr } from "@ui/table";
import i18next from "i18next";
import { OpenOrdersData } from "@/entities";
-import { BuySellHiveDialog, Skeleton } from "@/features/shared";
+import { Skeleton } from "@/features/shared";
+import { BuySellHiveDialog } from "@/features/shared/buy-sell-hive";
import { BuySellHiveTransactionType } from "@/enums";
import { dateToFormatted, dateToFullRelative, formattedNumber } from "@/utils";
diff --git a/apps/web/src/app/wallet/setup-external/_components/setup-external-metamask.tsx b/apps/web/src/app/wallet/setup-external/_components/setup-external-metamask.tsx
index 4458080f7b..03260408d3 100644
--- a/apps/web/src/app/wallet/setup-external/_components/setup-external-metamask.tsx
+++ b/apps/web/src/app/wallet/setup-external/_components/setup-external-metamask.tsx
@@ -3,7 +3,8 @@
import { formatError } from "@/api/format-error";
import { useActiveAccount } from "@/core/hooks/use-active-account";
import { getQueryClient } from "@/core/react-query";
-import { error, KeyOrHot, Stepper } from "@/features/shared";
+import { error, Stepper } from "@/features/shared";
+import { KeyOrHot } from "@/features/shared/key-or-hot";
import { Button, Spinner } from "@/features/ui";
import {
EcencyWalletCurrency,
diff --git a/apps/web/src/core/caches/pin-tracker-cache.ts b/apps/web/src/core/caches/pin-tracker-cache.ts
index a6a8cb848b..dfe7446e52 100644
--- a/apps/web/src/core/caches/pin-tracker-cache.ts
+++ b/apps/web/src/core/caches/pin-tracker-cache.ts
@@ -8,7 +8,7 @@ import { usePinPostMutation } from "@/api/sdk-mutations";
import { Community, Entry } from "@/entities";
import { isCommunity } from "@/utils";
import { clone } from "remeda";
-import { error, success } from "@/features/shared";
+import { error, success } from "@/features/shared/feedback";
import i18next from "i18next";
/**
diff --git a/apps/web/src/core/global-store/modules/global-module.ts b/apps/web/src/core/global-store/modules/global-module.ts
index bf25f144b1..e3c5debddf 100644
--- a/apps/web/src/core/global-store/modules/global-module.ts
+++ b/apps/web/src/core/global-store/modules/global-module.ts
@@ -1,7 +1,8 @@
import Cookies from "js-cookie";
import { AllFilter, ListStyle, Theme } from "@/enums";
import * as ls from "@/utils/local-storage";
-import { success } from "@/features/shared";
+import { setDayjsLocale } from "@/utils/dayjs";
+import { success } from "@/features/shared/feedback";
import i18next from "i18next";
import { loadLocale } from "@/features/i18n";
import { getCurrencyRate } from "@ecency/sdk";
@@ -86,7 +87,10 @@ export function createGlobalActions(set: (state: Partial) => void, getSta
setLang: async (lang: string) => {
ls.set("lang", lang);
ls.set("current-language", lang);
- await loadLocale(lang);
+ // Both loads complete BEFORE i18next switches and BEFORE the state
+ // publish below, so every render triggered by the change already has
+ // the translation resources AND the dayjs table (#1669 review).
+ await Promise.all([loadLocale(lang), setDayjsLocale(lang)]);
await i18next.changeLanguage(lang);
set({
lang: lang ?? "en-US"
diff --git a/apps/web/src/entities/private-api/drafts.ts b/apps/web/src/entities/private-api/drafts.ts
index 8330ac7b4a..783de5f234 100644
--- a/apps/web/src/entities/private-api/drafts.ts
+++ b/apps/web/src/entities/private-api/drafts.ts
@@ -1,5 +1,5 @@
import { BeneficiaryRoute, DecentMemesEntry, MetaData, RewardType } from "../operations";
-import { PollSnapshot } from "@/features/polls";
+import type { PollSnapshot } from "@/features/polls";
export interface DraftMetadata extends MetaData {
// Optional because the drafts API genuinely omits them, which is how
diff --git a/apps/web/src/features/i18n/index.ts b/apps/web/src/features/i18n/index.ts
index 5bab79145c..9ec4d79b46 100644
--- a/apps/web/src/features/i18n/index.ts
+++ b/apps/web/src/features/i18n/index.ts
@@ -1,6 +1,6 @@
import i18n from "i18next";
-import dayjs from "@/utils/dayjs";
+import { setDayjsLocale } from "@/utils/dayjs";
import * as ls from "@/utils/local-storage";
export const langOptions = [
@@ -164,7 +164,7 @@ export function initI18next(): Promise {
i18n.on("languageChanged", async function (lang) {
await loadLocale(lang);
- dayjs.locale(lang);
+ await setDayjsLocale(lang);
});
// Load the user's preferred locale on demand
@@ -173,6 +173,7 @@ export function initI18next(): Promise {
if (userLang !== "en-US") {
await loadLocale(userLang);
}
+ await setDayjsLocale(userLang);
await i18n.changeLanguage(userLang);
})();
diff --git a/apps/web/src/features/i18n/navigation-locale-watcher-client.tsx b/apps/web/src/features/i18n/navigation-locale-watcher-client.tsx
index 24dd718b7d..d81b277313 100644
--- a/apps/web/src/features/i18n/navigation-locale-watcher-client.tsx
+++ b/apps/web/src/features/i18n/navigation-locale-watcher-client.tsx
@@ -8,7 +8,7 @@ import { useCallback, useEffect, useMemo } from "react";
import { useGlobalStore } from "@/core/global-store";
import useUnmount from "react-use/lib/useUnmount";
import i18next from "i18next";
-import dayjs from "@/utils/dayjs";
+import { setDayjsLocale } from "@/utils/dayjs";
interface Props {
targetLanguage?: string | null;
@@ -25,12 +25,15 @@ export function NavigationLocaleWatcherClient({ targetLanguage }: Props) {
const lang = useGlobalStore((state) => state.lang);
const setLang = useGlobalStore((state) => state.setLang);
- const localeChanged = useCallback((lang: string) => dayjs.locale(lang), []);
+ const localeChanged = useCallback((lang: string) => void setDayjsLocale(lang), []);
useEffect(() => {
if (derivedLanguage && lang !== derivedLanguage) {
+ // setLang is the ordered pipeline: it loads the i18n resources and the
+ // dayjs table, switches i18next and only then publishes the state. A
+ // separate direct changeLanguage() here would re-render with a
+ // half-switched locale (#1669 review).
setLang(derivedLanguage);
- void i18next.changeLanguage(derivedLanguage);
}
}, [derivedLanguage, lang, setLang]);
@@ -45,7 +48,6 @@ export function NavigationLocaleWatcherClient({ targetLanguage }: Props) {
useUnmount(() => {
const currentLang = ls.get("current-language");
setLang(currentLang);
- i18next.changeLanguage(currentLang);
i18next.off("languageChanged", localeChanged);
});
diff --git a/apps/web/src/features/market/market-swap-form/market-swap-active-orders/index.tsx b/apps/web/src/features/market/market-swap-form/market-swap-active-orders/index.tsx
index f91036a6f8..a0c59dd505 100644
--- a/apps/web/src/features/market/market-swap-form/market-swap-active-orders/index.tsx
+++ b/apps/web/src/features/market/market-swap-form/market-swap-active-orders/index.tsx
@@ -4,7 +4,7 @@ import "./index.scss";
import { OpenOrdersData } from "@/entities";
import { getOpenOrdersQueryOptions } from "@ecency/sdk";
import i18next from "i18next";
-import { BuySellHiveDialog } from "@/features/shared";
+import { BuySellHiveDialog } from "@/features/shared/buy-sell-hive";
import { BuySellHiveTransactionType } from "@/enums";
import { useActiveAccount } from "@/core/hooks/use-active-account";
import { useQueryClient } from "@tanstack/react-query";
diff --git a/apps/web/src/features/shared/available-credits/index.tsx b/apps/web/src/features/shared/available-credits/index.tsx
index 433c8a8247..1795555159 100644
--- a/apps/web/src/features/shared/available-credits/index.tsx
+++ b/apps/web/src/features/shared/available-credits/index.tsx
@@ -20,7 +20,16 @@ import i18next from "i18next";
import dayjs, { Dayjs } from "@/utils/dayjs";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
-import { PurchaseQrDialog } from "../purchase-qr";
+import dynamic from "next/dynamic";
+import { DialogChunkSpinner } from "../dialog-chunk-spinner";
+
+// Modal-on-demand: available-credits stays in the shared barrel, and a static
+// import here was the last path dragging the purchase-qr family into every
+// route's pre-paint graph (#1668). The chunk loads on first open.
+const PurchaseQrDialog = dynamic(
+ () => import("../purchase-qr").then((m) => ({ default: m.PurchaseQrDialog })),
+ { ssr: false, loading: () => }
+);
interface Props {
username: string;
operation: RcOperation;
@@ -210,7 +219,9 @@ export const AvailableCredits = ({ username, payload, className }: Props) => {
,
portalContainer
)}
- setShowPurchaseDialog(v)} />
+ {showPurchaseDialog && (
+ setShowPurchaseDialog(v)} />
+ )}
>
) : (
<>>
diff --git a/apps/web/src/features/shared/dialog-chunk-spinner.tsx b/apps/web/src/features/shared/dialog-chunk-spinner.tsx
new file mode 100644
index 0000000000..c2b607b6ca
--- /dev/null
+++ b/apps/web/src/features/shared/dialog-chunk-spinner.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { Spinner } from "@ui/spinner";
+import type { JSX } from "react";
+
+/**
+ * Fallback for dynamically imported modals (#1669 review): rendered from the
+ * moment a dialog is requested until its chunk mounts, so a first open never
+ * shows nothing while the network fetch runs. Same layer as the modal that
+ * replaces it.
+ */
+export function DialogChunkSpinner(): JSX.Element {
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/features/shared/index.ts b/apps/web/src/features/shared/index.ts
index c9a3e95d38..d1e1e90388 100644
--- a/apps/web/src/features/shared/index.ts
+++ b/apps/web/src/features/shared/index.ts
@@ -13,9 +13,7 @@ export * from "./notification-handler";
export * from "./switch-lang";
export * from "./search-suggester";
export * from "./suggestion-list";
-export * from "./key-or-hot-dialog";
export * from "./entry-link";
-export * from "./purchase-qr";
export * from "./follow-controls";
export * from "./image-upload-button";
export * from "./list-style-toggle";
@@ -29,17 +27,20 @@ export * from "./entry-list-content";
export * from "./entry-list-item";
export * from "./entry-menu";
export * from "./skeleton";
-export * from "./promote";
export * from "./profile-popover";
export * from "./entry-vote-btn";
export * from "./entry-tip-btn";
-export * from "./transfer";
export * from "./entry-payout";
export * from "./entry-votes";
export * from "./entry-reblog-btn";
export * from "./message-no-data";
export * from "./entry-info";
export * from "./bookmark-btn";
+// NOTE: the interaction-only dialog families (./transfer, ./promote, ./boost,
+// ./purchase-qr, ./buy-sell-hive, ./key-or-hot, ./transactions) are intentionally
+// NOT re-exported from this barrel. Re-exporting them dragged their chunks into
+// EVERY route's pre-paint graph through the navbar's client boundary (#1668).
+// Deep-import them: "@/features/shared/".
// NOTE: ./discussion is intentionally NOT re-exported from this barrel. The
// Discussion tree (DiscussionList -> DiscussionItem) imports the reply/edit
// composer, so re-exporting it here dragged that whole path into any page that
@@ -51,19 +52,14 @@ export * from "./entry-delete-btn";
// autocomplete, polls and video upload (~80KB) — importing any unrelated symbol
// from this barrel dragged all of that into pages that never render a composer.
// Import it directly: `import { Comment } from "@/features/shared/comment"`.
-export * from "./transactions";
export * from "./click-away-listener";
export * from "./available-credits";
export * from "./login";
export * from "./wallet-badge";
-export * from "./buy-sell-hive";
-export * from "./boost";
-export * from "./key-or-hot";
export * from "./metamask-sign-button";
export * from "./image-upload-button";
export * from "./notifications";
export * from "./gallery";
-export * from "./boost";
export * from "./static-navbar";
export * from "./edit-history";
// NOTE: ./editor-toolbar is intentionally NOT re-exported here — see the ./comment
diff --git a/apps/web/src/features/shared/key-or-hot-dialog.tsx b/apps/web/src/features/shared/key-or-hot-dialog.tsx
deleted file mode 100644
index cb87b21cd8..0000000000
--- a/apps/web/src/features/shared/key-or-hot-dialog.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-"use client";
-
-import React, { PropsWithChildren, useState } from "react";
-import { PrivateKey } from "@ecency/sdk";
-import { PopoverConfirm } from "@ui/popover-confirm";
-import { Modal, ModalBody, ModalHeader } from "@ui/modal";
-import { KeyOrHot } from "@/features/shared/key-or-hot";
-
-interface Props {
- popOver?: boolean;
- onKey?: (key: PrivateKey) => void;
- onHot?: () => void;
- onKc?: () => void;
- onMetaMask?: () => void;
- onToggle?: () => void;
-}
-
-export function KeyOrHotDialog({
- children,
- onKey,
- onHot,
- onKc,
- onMetaMask,
- popOver = false,
- onToggle
-}: PropsWithChildren) {
- const [keyDialog, setKeyDialog] = useState(false);
-
- const toggleKeyDialog = () => {
- setKeyDialog(!keyDialog);
- onToggle?.();
- };
-
- return (
- <>
- {popOver ? (
-
- toggleKeyDialog()}>
-
e.stopPropagation()}>{children}
-
-
- ) : (
-
setKeyDialog(true)}>{children}
- )}
-
- {keyDialog && (
-
-
-
- {
- toggleKeyDialog();
- onKey?.(key);
- }}
- onHot={() => {
- toggleKeyDialog();
- if (onHot) {
- onHot();
- }
- }}
- onKc={() => {
- toggleKeyDialog();
- if (onKc) {
- onKc();
- }
- }}
- onMetaMask={() => {
- toggleKeyDialog();
- onMetaMask?.();
- }}
- inProgress={false}
- />
-
-
- )}
- >
- );
-}
diff --git a/apps/web/src/features/shared/login/login-user-item.tsx b/apps/web/src/features/shared/login/login-user-item.tsx
index 9459425043..9cdf887857 100644
--- a/apps/web/src/features/shared/login/login-user-item.tsx
+++ b/apps/web/src/features/shared/login/login-user-item.tsx
@@ -1,6 +1,6 @@
import { useActiveAccount } from "@/core/hooks/use-active-account";
import { User } from "@/entities";
-import { UserAvatar } from "@/features/shared";
+import { UserAvatar } from "@/features/shared/user-avatar";
import { UilTrash } from "@tooni/iconscout-unicons-react";
import { Button } from "@ui/button";
import { PopoverConfirm } from "@ui/popover-confirm";
diff --git a/apps/web/src/features/shared/navbar/anon-user-buttons.tsx b/apps/web/src/features/shared/navbar/anon-user-buttons.tsx
index cd0cdcadff..d689c56694 100644
--- a/apps/web/src/features/shared/navbar/anon-user-buttons.tsx
+++ b/apps/web/src/features/shared/navbar/anon-user-buttons.tsx
@@ -3,7 +3,6 @@ import React from "react";
import { useActiveAccount } from "@/core/hooks/use-active-account";
import { useGlobalStore } from "@/core/global-store";
import i18next from "i18next";
-import { preloadLoginDialog } from "@/features/shared";
export function AnonUserButtons() {
const { activeUser } = useActiveAccount();
@@ -19,9 +18,6 @@ export function AnonUserButtons() {
className="btn-login"
outline={true}
onClick={() => toggleUIProp("login")}
- onMouseEnter={preloadLoginDialog}
- onFocus={preloadLoginDialog}
- onPointerDown={preloadLoginDialog}
>
{i18next.t("g.login")}
diff --git a/apps/web/src/features/shared/navbar/navbar-desktop.tsx b/apps/web/src/features/shared/navbar/navbar-desktop.tsx
index 67d0410a03..ef410a9d4d 100644
--- a/apps/web/src/features/shared/navbar/navbar-desktop.tsx
+++ b/apps/web/src/features/shared/navbar/navbar-desktop.tsx
@@ -1,5 +1,5 @@
import { useGlobalStore } from "@/core/global-store";
-import { UserAvatar } from "@/features/shared";
+import { UserAvatar } from "@/features/shared/user-avatar";
import { AnonUserButtons } from "@/features/shared/navbar/anon-user-buttons";
import { NavbarMainSidebar } from "@/features/shared/navbar/navbar-main-sidebar";
import { NavbarMainSidebarToggle } from "@/features/shared/navbar/navbar-main-sidebar-toggle";
diff --git a/apps/web/src/features/shared/navbar/navbar-main-sidebar.tsx b/apps/web/src/features/shared/navbar/navbar-main-sidebar.tsx
index 2f82a86dda..ef00364a66 100644
--- a/apps/web/src/features/shared/navbar/navbar-main-sidebar.tsx
+++ b/apps/web/src/features/shared/navbar/navbar-main-sidebar.tsx
@@ -18,7 +18,7 @@ import { NavbarSideThemeSwitcher } from "@/features/shared/navbar/sidebar";
import { Button } from "@ui/button";
import i18next from "i18next";
import { closeSvg, WavyDashIcon } from "@ui/svg";
-import { SwitchLang } from "@/features/shared";
+import { SwitchLang } from "@/features/shared/switch-lang";
import dynamic from "next/dynamic";
import { useRouter } from "next/navigation";
import Image from "next/image";
diff --git a/apps/web/src/features/shared/navbar/navbar-mobile.tsx b/apps/web/src/features/shared/navbar/navbar-mobile.tsx
index edc5281fed..6ed15f7a72 100644
--- a/apps/web/src/features/shared/navbar/navbar-mobile.tsx
+++ b/apps/web/src/features/shared/navbar/navbar-mobile.tsx
@@ -3,7 +3,7 @@
import { useHydrated } from "@/api/queries";
import { useGlobalStore } from "@/core/global-store";
import { useActiveAccount } from "@/core/hooks/use-active-account";
-import { UserAvatar, preloadLoginDialog } from "@/features/shared";
+import { UserAvatar } from "@/features/shared/user-avatar";
import { NavbarMainSidebar } from "@/features/shared/navbar/navbar-main-sidebar";
import { NavbarNotificationsButton } from "@/features/shared/navbar/navbar-notifications-button";
import { NavbarSide } from "@/features/shared/navbar/sidebar/navbar-side";
@@ -210,9 +210,6 @@ export function NavbarMobile({