Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/api/mutations/sign-transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,24 @@ 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";

// The edit composer pulls the Comment editor (toolbar, image/video upload,
// 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: () => <DialogChunkSpinner /> }
);

const EntryPageEdit = dynamic(
() => import("./entry-page-edit").then((m) => m.EntryPageEdit),
{
Expand Down Expand Up @@ -152,11 +160,13 @@ export function EntryPageBodyViewer({ entry }: Props) {
</SelectionPopover>
)}
{isEdit && entry.parent_author && <EntryPageEdit entry={entry} />}
<TransactionSigner
show={!!signingOperation}
onHide={() => setSigningOperation(undefined)}
operation={signingOperation}
/>
{signingOperation !== undefined && (
<TransactionSigner
show={true}
onHide={() => setSigningOperation(undefined)}
operation={signingOperation}
/>
)}
</EntryPageViewerManager>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/}
<Suspense fallback={null}>
<EntryRelatedFooter entry={entry} />
<div className="[content-visibility:auto] [contain-intrinsic-size:auto_600px]">
<EntryRelatedFooter entry={entry} />
</div>
</Suspense>
<EntryPageContentClient entry={entry} />
<EntryPageDiscussionsWrapper entry={entry} category={category} />
<div className="[content-visibility:auto] [contain-intrinsic-size:auto_150px]">
<EntryPageDiscussionsWrapper entry={entry} category={category} />
</div>
</EntryRenderBoundary>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/app/client-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -69,6 +70,10 @@ export function ClientProviders(props: PropsWithChildren) {
>
<UIManager>
<ClientInit />
{/* Inside UIManager but OUTSIDE DeferredRender on purpose: its
effect must flush with root hydration, not after the lazy
feature cascade (#1668). */}
<EntryStatsPrefetch />
{props.children}
{/* Defer non-critical components for LCP optimization */}
<DeferredRender>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
73 changes: 73 additions & 0 deletions apps/web/src/app/entry-stats-prefetch.tsx
Original file line number Diff line number Diff line change
@@ -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-]+)\/?$/;
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

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]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return null;
}
2 changes: 1 addition & 1 deletion apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/market/_components/hive-barter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/market/_components/open-orders/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/core/caches/pin-tracker-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/core/global-store/modules/global-module.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -86,7 +87,10 @@ export function createGlobalActions(set: (state: Partial<State>) => 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"
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/entities/private-api/drafts.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/features/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -164,7 +164,7 @@ export function initI18next(): Promise<void> {

i18n.on("languageChanged", async function (lang) {
await loadLocale(lang);
dayjs.locale(lang);
await setDayjsLocale(lang);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Await the Day.js chunk before publishing the language

When a user first switches to a locale whose Day.js table is lazy-loaded (for example es-ES), i18next emits languageChanged synchronously and does not await this async listener. Consequently setLang() finishes and triggers the language render while setDayjsLocale() is still fetching its chunk; when the chunk finally applies the locale, it causes no React state update, so static or memoized date strings remain in the previous language until an unrelated rerender (and rapid switches can also finish out of order). Coordinate this promise with the language-setting path before publishing the new state, or explicitly notify consumers after it completes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in faa3f5f, at every language-change choke point rather than in the listener:

  • The store's setLang action (the ordered pipeline every UI switch goes through) now awaits Promise.all([loadLocale, setDayjsLocale]) BEFORE i18next.changeLanguage and before publishing state, so every render triggered by the change has both the resources and the dayjs table.
  • The ?lang= watcher's redundant direct changeLanguage() calls (effect and unmount) are removed; setLang is the single ordered path. The init path and the server-side watcher got the same pre-load ordering.
  • setDayjsLocale carries a stale-request id, so rapid switches whose chunks resolve out of order can never restore an older locale. Pinned by a deterministic regression test (older uncached request racing a newer cached one; verified the test fails without the guard).
  • The languageChanged listener remains as a safety net for any direct changeLanguage caller; the guard makes it idempotent.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 4fa94d5: the server watcher no longer touches dayjs, and setDayjsLocale itself is client-only (typeof window guard with the rationale in a comment), so no request can mutate the process-global locale or race the request id across requests. Server renders always use the built-in default and the client pipeline corrects after hydration, the same contract TimeLabel already documents. Worth noting this also removes a PRE-existing leak: the old eager-import code ran dayjs.locale(lang) server-side through the languageChanged handler on every ?lang= request. Pinned by a new spec (window stubbed off, locale unchanged).

});

// Load the user's preferred locale on demand
Expand All @@ -173,6 +173,7 @@ export function initI18next(): Promise<void> {
if (userLang !== "en-US") {
await loadLocale(userLang);
}
await setDayjsLocale(userLang);
await i18n.changeLanguage(userLang);
})();

Expand Down
Loading
Loading