+
+
{haveProfile && }
diff --git a/components/user/brain/userPageBrainActivity.helpers.ts b/components/user/brain/userPageBrainActivity.helpers.ts
new file mode 100644
index 0000000000..be8f4023cb
--- /dev/null
+++ b/components/user/brain/userPageBrainActivity.helpers.ts
@@ -0,0 +1,267 @@
+import { numberWithCommas } from "@/helpers/Helpers";
+import { Time } from "@/helpers/time";
+
+const DAY_MS = Time.days(1).toMillis();
+const WEEKDAY_COUNT = 7;
+const MONTH_LABEL_FORMATTER = new Intl.DateTimeFormat("en-US", {
+ month: "short",
+ timeZone: "UTC",
+});
+const DAY_LABEL_FORMATTER = new Intl.DateTimeFormat("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ timeZone: "UTC",
+});
+
+export interface UserPageBrainActivityResponse {
+ readonly last_date: string;
+ readonly date_samples: readonly number[];
+}
+
+type UserPageBrainActivityCellState = "padding" | "empty" | "active";
+type UserPageBrainActivityIntensity = 0 | 1 | 2 | 3 | 4;
+
+export interface UserPageBrainActivityCell {
+ readonly key: string;
+ readonly isoDate: string | null;
+ readonly count: number;
+ readonly ariaLabel: string | null;
+ readonly state: UserPageBrainActivityCellState;
+ readonly intensity: UserPageBrainActivityIntensity;
+}
+
+interface UserPageBrainActivityMonthLabel {
+ readonly key: string;
+ readonly label: string;
+ readonly labelColumn: number;
+ readonly firstVisibleColumn: number;
+ readonly lastVisibleColumn: number;
+}
+
+export interface UserPageBrainActivityViewModel {
+ readonly resetKey: string;
+ readonly periodLabel: string;
+ readonly totalDrops: number;
+ readonly weekCount: number;
+ readonly monthLabels: readonly UserPageBrainActivityMonthLabel[];
+ readonly cells: readonly UserPageBrainActivityCell[];
+}
+
+function parseUtcDate(value: string): Date | null {
+ const [dayPart, monthPart, yearPart] = value.split(".");
+ const day = Number(dayPart);
+ const month = Number(monthPart);
+ const year = Number(yearPart);
+
+ if (
+ !Number.isInteger(day) ||
+ !Number.isInteger(month) ||
+ !Number.isInteger(year) ||
+ day <= 0 ||
+ month <= 0 ||
+ month > 12
+ ) {
+ return null;
+ }
+
+ const date = new Date(Date.UTC(year, month - 1, day));
+ if (
+ date.getUTCFullYear() !== year ||
+ date.getUTCMonth() !== month - 1 ||
+ date.getUTCDate() !== day
+ ) {
+ return null;
+ }
+
+ return date;
+}
+
+function toIsoDate(date: Date): string {
+ return date.toISOString().slice(0, 10);
+}
+
+function getCountLabel(count: number): string {
+ return count === 1
+ ? "1 public post"
+ : `${numberWithCommas(count)} public posts`;
+}
+
+function getQuantileValue(
+ sortedCounts: readonly number[],
+ percentile: number
+): number {
+ if (sortedCounts.length === 0) {
+ return 0;
+ }
+
+ const clampedPercentile = Math.min(1, Math.max(0, percentile));
+ const rank = Math.max(
+ 0,
+ Math.ceil(clampedPercentile * sortedCounts.length) - 1
+ );
+ return sortedCounts[rank] ?? 0;
+}
+
+function getIntensity(
+ count: number,
+ thresholds: Readonly<{
+ first: number;
+ second: number;
+ third: number;
+ max: number;
+ }>
+): UserPageBrainActivityIntensity {
+ if (count <= 0) {
+ return 0;
+ }
+
+ if (count >= thresholds.max) {
+ return 4;
+ }
+
+ if (count <= thresholds.first) {
+ return 1;
+ }
+ if (count <= thresholds.second) {
+ return 2;
+ }
+ if (count <= thresholds.third) {
+ return 3;
+ }
+ return 4;
+}
+
+function getSamplesSignature(samples: readonly number[]): string {
+ let hash = 0;
+
+ for (const sample of samples) {
+ hash = (hash * 31 + sample) >>> 0;
+ }
+
+ return hash.toString(36);
+}
+
+export function buildUserPageBrainActivityViewModel(
+ activity: UserPageBrainActivityResponse
+): UserPageBrainActivityViewModel | null {
+ if (
+ !Array.isArray(activity.date_samples) ||
+ activity.date_samples.length === 0
+ ) {
+ return null;
+ }
+
+ const lastDate = parseUtcDate(activity.last_date);
+ if (!lastDate) {
+ return null;
+ }
+
+ const normalizedSamples = activity.date_samples.map((sample) =>
+ typeof sample === "number" && Number.isFinite(sample) && sample >= 0
+ ? Math.floor(sample)
+ : 0
+ );
+ const activeCounts = normalizedSamples
+ .filter((sample) => sample > 0)
+ .sort((left, right) => left - right);
+ const intensityThresholds = {
+ first: getQuantileValue(activeCounts, 0.25),
+ second: getQuantileValue(activeCounts, 0.5),
+ third: getQuantileValue(activeCounts, 0.75),
+ max: activeCounts.at(-1) ?? 0,
+ } as const;
+ const startDate = new Date(
+ lastDate.getTime() - (normalizedSamples.length - 1) * DAY_MS
+ );
+ const samples = normalizedSamples.map((count, index) => ({
+ date: new Date(startDate.getTime() + index * DAY_MS),
+ count,
+ }));
+
+ const totalDrops = samples.reduce((sum, sample) => sum + sample.count, 0);
+ const leadingPaddingCount = startDate.getUTCDay();
+ const trailingPaddingCount =
+ (WEEKDAY_COUNT - ((leadingPaddingCount + samples.length) % WEEKDAY_COUNT)) %
+ WEEKDAY_COUNT;
+ const monthColumns = new Map<
+ string,
+ {
+ readonly key: string;
+ readonly label: string;
+ preferredLabelColumn: number;
+ firstVisibleColumn: number;
+ lastVisibleColumn: number;
+ }
+ >();
+
+ const cells: UserPageBrainActivityCell[] = Array.from(
+ { length: leadingPaddingCount },
+ (_, index) => ({
+ key: `padding-start-${index}`,
+ isoDate: null,
+ count: 0,
+ ariaLabel: null,
+ state: "padding",
+ intensity: 0,
+ })
+ );
+
+ samples.forEach(({ date, count }, index) => {
+ const isoDate = toIsoDate(date);
+ const monthKey = `${date.getUTCFullYear()}-${date.getUTCMonth()}`;
+ const weekIndex = Math.floor((leadingPaddingCount + index) / WEEKDAY_COUNT);
+ const labelStartColumn = weekIndex + (date.getUTCDay() === 0 ? 0 : 1);
+
+ const existingMonth = monthColumns.get(monthKey);
+ if (existingMonth) {
+ existingMonth.lastVisibleColumn = weekIndex;
+ } else {
+ monthColumns.set(monthKey, {
+ key: `month-${monthKey}`,
+ label: MONTH_LABEL_FORMATTER.format(date),
+ preferredLabelColumn: labelStartColumn,
+ firstVisibleColumn: weekIndex,
+ lastVisibleColumn: weekIndex,
+ });
+ }
+
+ const ariaLabel = `${DAY_LABEL_FORMATTER.format(date)}: ${getCountLabel(count)}`;
+ cells.push({
+ key: isoDate,
+ isoDate,
+ count,
+ ariaLabel,
+ state: count > 0 ? "active" : "empty",
+ intensity: getIntensity(count, intensityThresholds),
+ });
+ });
+
+ for (let index = 0; index < trailingPaddingCount; index += 1) {
+ cells.push({
+ key: `padding-end-${index}`,
+ isoDate: null,
+ count: 0,
+ ariaLabel: null,
+ state: "padding",
+ intensity: 0,
+ });
+ }
+
+ const monthLabels = Array.from(monthColumns.values()).map((month) => ({
+ key: month.key,
+ label: month.label,
+ labelColumn: Math.min(month.preferredLabelColumn, month.lastVisibleColumn),
+ firstVisibleColumn: month.firstVisibleColumn,
+ lastVisibleColumn: month.lastVisibleColumn,
+ }));
+
+ return {
+ resetKey: `${toIsoDate(lastDate)}:${getSamplesSignature(normalizedSamples)}`,
+ periodLabel: "the last 12 months",
+ totalDrops,
+ weekCount: cells.length / WEEKDAY_COUNT,
+ monthLabels,
+ cells,
+ };
+}
diff --git a/components/user/brain/userPageBrainActivityHeatmap.helpers.ts b/components/user/brain/userPageBrainActivityHeatmap.helpers.ts
new file mode 100644
index 0000000000..e26b4a38de
--- /dev/null
+++ b/components/user/brain/userPageBrainActivityHeatmap.helpers.ts
@@ -0,0 +1,237 @@
+import clsx from "clsx";
+import type {
+ UserPageBrainActivityCell,
+ UserPageBrainActivityViewModel,
+} from "./userPageBrainActivity.helpers";
+
+export type HeatmapTooltipData = Readonly<{
+ count: number;
+ isoDate: string;
+ intensity: UserPageBrainActivityCell["intensity"];
+}>;
+
+type HeatmapMonthLabel = UserPageBrainActivityViewModel["monthLabels"][number];
+type HeatmapNodeVariant = "grid" | "tooltip";
+type PlacedMonthLabel = Readonly<{
+ key: string;
+ label: string;
+ leftPx: number;
+}>;
+
+// Layout and sizing
+export const CELL_SIZE_PX = 14;
+export const CELL_GAP_PX = 2;
+export const DAY_LABEL_COLUMN_WIDTH_PX = 30;
+export const MONTH_LABEL_ROW_HEIGHT_PX = 12;
+const HEADER_TO_HEATMAP_GAP_PX = 4;
+const DAY_LABEL_EXTRA_OFFSET_PX = 2;
+export const DAY_LABEL_TOP_OFFSET_PX =
+ MONTH_LABEL_ROW_HEIGHT_PX +
+ HEADER_TO_HEATMAP_GAP_PX +
+ DAY_LABEL_EXTRA_OFFSET_PX;
+export const HEATMAP_GRID_HEIGHT_PX = CELL_SIZE_PX * 7 + CELL_GAP_PX * 6;
+const COLUMN_STRIDE_PX = CELL_SIZE_PX + CELL_GAP_PX;
+export const DAY_LABELS = ["", "Mon", "", "Wed", "", "Fri", ""] as const;
+export const LOADING_MONTH_HEADER_SEGMENTS = [
+ { key: "start", widthPx: 18 },
+ { key: "spring", widthPx: 24 },
+ { key: "early-summer", widthPx: 16 },
+ { key: "late-summer", widthPx: 22 },
+ { key: "autumn", widthPx: 20 },
+ { key: "winter-start", widthPx: 18 },
+ { key: "winter-mid", widthPx: 24 },
+ { key: "end", widthPx: 16 },
+] as const;
+export const MONTH_LABEL_MIN_SPACING_PX = 34;
+const MONTH_LABEL_OVERFLOW_TOLERANCE_PX = 18;
+export const TOOLTIP_DATE_FORMATTER = new Intl.DateTimeFormat("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ timeZone: "UTC",
+});
+export const TOOLTIP_STYLE = {
+ padding: "0",
+ background: "transparent",
+ boxShadow: "none",
+ zIndex: 99999,
+ pointerEvents: "none",
+} as const;
+export const HEATMAP_VIEWPORT_CLASS_NAME =
+ "tw-[scrollbar-gutter:stable] tw-flex-1 tw-overflow-x-auto tw-overflow-y-hidden tw-pb-3 tw-scrollbar-thin tw-scrollbar-track-transparent tw-scrollbar-thumb-iron-700/60 desktop-hover:hover:tw-scrollbar-thumb-iron-600/80";
+export const HEATMAP_CONTENT_CLASS_NAME = "tw-inline-flex tw-pr-2";
+export const HEATMAP_GRID_STYLE = {
+ gridTemplateRows: `repeat(7, ${CELL_SIZE_PX}px)`,
+ gridAutoColumns: `${CELL_SIZE_PX}px`,
+ gap: `${CELL_GAP_PX}px`,
+} as const;
+export const CELL_FRAME_STYLE = {
+ width: `${CELL_SIZE_PX}px`,
+ height: `${CELL_SIZE_PX}px`,
+} as const;
+
+// Cell presentation
+const CELL_NODE_CLASS_NAMES: Readonly<
+ Record<
+ HeatmapNodeVariant,
+ Readonly
>
+ >
+> = {
+ grid: {
+ 0: "tw-h-1 tw-w-1 tw-rounded-[1px] tw-bg-[#1d2229] desktop-hover:group-hover/cell:tw-bg-[#272d35]",
+ 1: "tw-h-[6px] tw-w-[6px] tw-rounded-[1px] tw-bg-[#064e3b] desktop-hover:group-hover/cell:tw-scale-110 desktop-hover:group-hover/cell:tw-brightness-105",
+ 2: "tw-h-[7px] tw-w-[7px] tw-rounded-[1px] tw-bg-[#047857] desktop-hover:group-hover/cell:tw-scale-110 desktop-hover:group-hover/cell:tw-brightness-105",
+ 3: "tw-h-[9px] tw-w-[9px] tw-rounded-sm tw-bg-[#059669] desktop-hover:group-hover/cell:tw-scale-105 desktop-hover:group-hover/cell:tw-brightness-105",
+ 4: "tw-h-[11px] tw-w-[11px] tw-rounded-sm tw-bg-[#34d399] tw-shadow-[0_0_10px_rgba(52,211,153,0.22)] desktop-hover:group-hover/cell:tw-scale-105 desktop-hover:group-hover/cell:tw-brightness-105",
+ },
+ tooltip: {
+ 0: "tw-h-1 tw-w-1 tw-rounded-[1px] tw-bg-iron-500/55",
+ 1: "tw-h-[6px] tw-w-[6px] tw-rounded-[1px] tw-bg-[#064e3b]",
+ 2: "tw-h-[6px] tw-w-[6px] tw-rounded-[1px] tw-bg-[#047857]",
+ 3: "tw-h-[7px] tw-w-[7px] tw-rounded-sm tw-bg-[#059669]",
+ 4: "tw-h-[8px] tw-w-[8px] tw-rounded-sm tw-bg-[#34d399]",
+ },
+} as const;
+
+// Tooltip
+export function getTooltipCountLabel(count: number): string {
+ if (count <= 0) {
+ return "No posts";
+ }
+
+ return count === 1 ? "1 post" : `${count} posts`;
+}
+
+function getTooltipIntensity(
+ value: number
+): UserPageBrainActivityCell["intensity"] {
+ const normalizedValue = Math.max(0, Math.min(4, Math.floor(value)));
+
+ switch (normalizedValue) {
+ case 4:
+ return 4;
+ case 3:
+ return 3;
+ case 2:
+ return 2;
+ case 1:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+export function getCellTooltipAnchorProps(
+ cell: UserPageBrainActivityCell,
+ tooltipId: string
+):
+ | Readonly<{
+ "aria-hidden": true;
+ "data-tooltip-id": string;
+ "data-tooltip-count": string;
+ "data-tooltip-date": string;
+ "data-tooltip-intensity": string;
+ }>
+ | Readonly<{
+ "aria-hidden": true;
+ }> {
+ if (!cell.isoDate) {
+ return { "aria-hidden": true };
+ }
+
+ return {
+ "aria-hidden": true,
+ "data-tooltip-id": tooltipId,
+ "data-tooltip-count": String(cell.count),
+ "data-tooltip-date": cell.isoDate,
+ "data-tooltip-intensity": String(cell.intensity),
+ };
+}
+
+export function getHeatmapTooltipData(
+ activeAnchor: Element | null
+): HeatmapTooltipData | null {
+ if (!(activeAnchor instanceof HTMLElement)) {
+ return null;
+ }
+
+ const { dataset } = activeAnchor;
+ const countValue = Number(dataset["tooltipCount"]);
+ const isoDate = dataset["tooltipDate"];
+
+ if (!isoDate || Number.isNaN(countValue)) {
+ return null;
+ }
+
+ const intensityValue = Number(dataset["tooltipIntensity"]);
+
+ return {
+ count: countValue,
+ isoDate,
+ intensity: getTooltipIntensity(intensityValue),
+ };
+}
+
+// Cell presentation
+export function getCellFrameClassName(cell: UserPageBrainActivityCell): string {
+ if (cell.state === "padding") {
+ return "tw-bg-transparent";
+ }
+
+ return clsx(
+ "tw-flex tw-items-center tw-justify-center tw-rounded tw-transition-colors tw-duration-200",
+ cell.ariaLabel &&
+ "tw-cursor-pointer desktop-hover:hover:tw-z-20 desktop-hover:hover:tw-bg-[#171b21]"
+ );
+}
+
+export function getCellNodeClassName(
+ cell: UserPageBrainActivityCell,
+ variant: HeatmapNodeVariant = "grid"
+): string {
+ return clsx(
+ variant === "grid" &&
+ "tw-transform-gpu tw-transition-all tw-duration-200 tw-ease-out",
+ CELL_NODE_CLASS_NAMES[variant][cell.intensity]
+ );
+}
+
+// Month label placement
+export function getPlacedMonthLabels(
+ monthLabels: readonly HeatmapMonthLabel[],
+ scrollLeft: number,
+ clientWidth: number
+): readonly PlacedMonthLabel[] {
+ const placedMonthLabels: PlacedMonthLabel[] = [];
+ let nextMinimumLeftPx = 0;
+
+ for (const label of monthLabels) {
+ const naturalLeftPx = label.labelColumn * COLUMN_STRIDE_PX - scrollLeft;
+
+ if (
+ clientWidth > 0 &&
+ (naturalLeftPx < -MONTH_LABEL_OVERFLOW_TOLERANCE_PX ||
+ naturalLeftPx > clientWidth)
+ ) {
+ continue;
+ }
+
+ const leftPx = Math.max(-MONTH_LABEL_OVERFLOW_TOLERANCE_PX, naturalLeftPx);
+
+ if (clientWidth > 0 && leftPx >= clientWidth) {
+ continue;
+ }
+ if (leftPx < nextMinimumLeftPx) {
+ continue;
+ }
+
+ placedMonthLabels.push({
+ key: label.key,
+ label: label.label,
+ leftPx,
+ });
+ nextMinimumLeftPx = leftPx + MONTH_LABEL_MIN_SPACING_PX;
+ }
+
+ return placedMonthLabels;
+}
diff --git a/components/user/brain/userPageBrainActivityHeatmap.viewport.ts b/components/user/brain/userPageBrainActivityHeatmap.viewport.ts
new file mode 100644
index 0000000000..3af3a60f79
--- /dev/null
+++ b/components/user/brain/userPageBrainActivityHeatmap.viewport.ts
@@ -0,0 +1,135 @@
+import {
+ useCallback,
+ useLayoutEffect,
+ useRef,
+ useSyncExternalStore,
+} from "react";
+
+type ViewportMetrics = Readonly<{
+ scrollLeft: number;
+ clientWidth: number;
+ scrollWidth: number;
+}>;
+
+type MutableValueRef = {
+ current: T;
+};
+
+const EMPTY_VIEWPORT_METRICS: ViewportMetrics = {
+ scrollLeft: 0,
+ clientWidth: 0,
+ scrollWidth: 0,
+};
+
+function snapViewportToLatest(viewport: HTMLDivElement | null) {
+ if (!viewport) {
+ return;
+ }
+
+ const maxScrollLeft = Math.max(
+ 0,
+ viewport.scrollWidth - viewport.clientWidth
+ );
+ // Show the latest column fully, even when the viewport width is not aligned to a column stride.
+ viewport.scrollLeft = maxScrollLeft;
+}
+
+function readViewportMetrics(
+ viewport: HTMLDivElement | null,
+ cachedMetricsRef: MutableValueRef
+): ViewportMetrics {
+ if (!viewport) {
+ return EMPTY_VIEWPORT_METRICS;
+ }
+
+ const nextMetrics = {
+ scrollLeft: viewport.scrollLeft,
+ clientWidth: viewport.clientWidth,
+ scrollWidth: viewport.scrollWidth,
+ } as const;
+ const previousMetrics = cachedMetricsRef.current;
+
+ if (
+ previousMetrics.scrollLeft === nextMetrics.scrollLeft &&
+ previousMetrics.clientWidth === nextMetrics.clientWidth &&
+ previousMetrics.scrollWidth === nextMetrics.scrollWidth
+ ) {
+ return previousMetrics;
+ }
+
+ cachedMetricsRef.current = nextMetrics;
+ return nextMetrics;
+}
+
+function subscribeToViewportMetrics(
+ viewport: HTMLDivElement | null,
+ onStoreChange: () => void
+) {
+ if (!viewport) {
+ return () => {};
+ }
+
+ const content = viewport.firstElementChild;
+ let frameId = 0;
+
+ const notify = () => {
+ cancelAnimationFrame(frameId);
+ frameId = requestAnimationFrame(onStoreChange);
+ };
+
+ notify();
+ viewport.addEventListener("scroll", notify, { passive: true });
+
+ if (typeof ResizeObserver === "undefined") {
+ window.addEventListener("resize", notify);
+
+ return () => {
+ cancelAnimationFrame(frameId);
+ viewport.removeEventListener("scroll", notify);
+ window.removeEventListener("resize", notify);
+ };
+ }
+
+ const resizeObserver = new ResizeObserver(notify);
+ resizeObserver.observe(viewport);
+ if (content) {
+ resizeObserver.observe(content);
+ }
+
+ return () => {
+ cancelAnimationFrame(frameId);
+ viewport.removeEventListener("scroll", notify);
+ resizeObserver.disconnect();
+ };
+}
+
+export function useHeatmapViewport(resetKey?: string) {
+ const viewportRef = useRef(null);
+ const cachedMetricsRef = useRef(EMPTY_VIEWPORT_METRICS);
+
+ const setViewportRef = useCallback((node: HTMLDivElement | null) => {
+ viewportRef.current = node;
+ cachedMetricsRef.current = EMPTY_VIEWPORT_METRICS;
+ }, []);
+
+ useLayoutEffect(() => {
+ cachedMetricsRef.current = EMPTY_VIEWPORT_METRICS;
+ snapViewportToLatest(viewportRef.current);
+ }, [resetKey]);
+
+ const getViewportSnapshot = useCallback(() => {
+ return readViewportMetrics(viewportRef.current, cachedMetricsRef);
+ }, []);
+
+ const subscribeToViewport = useCallback((onStoreChange: () => void) => {
+ return subscribeToViewportMetrics(viewportRef.current, onStoreChange);
+ }, []);
+
+ const viewportMetrics = useSyncExternalStore(
+ subscribeToViewport,
+ getViewportSnapshot,
+ () => EMPTY_VIEWPORT_METRICS
+ );
+
+ return { viewportRef: setViewportRef, viewportMetrics };
+}
diff --git a/components/waves/drops/WaveDrop.tsx b/components/waves/drops/WaveDrop.tsx
index 8f93530aae..f06ab5269f 100644
--- a/components/waves/drops/WaveDrop.tsx
+++ b/components/waves/drops/WaveDrop.tsx
@@ -89,7 +89,7 @@ const getColorClasses = ({
const ringClasses = isWaveView
? ""
: "tw-ring-1 tw-ring-inset tw-ring-iron-800";
- const bgClass = isWaveView ? "" : "tw-bg-iron-950";
+ const bgClass = isWaveView ? "" : "tw-bg-iron-950/80";
return `${bgClass} ${ringClasses} ${hoverClass}`.trim();
}
diff --git a/hooks/useIdentityActivity.ts b/hooks/useIdentityActivity.ts
new file mode 100644
index 0000000000..c24cc2d25f
--- /dev/null
+++ b/hooks/useIdentityActivity.ts
@@ -0,0 +1,37 @@
+import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
+import { getDefaultQueryRetry } from "@/components/react-query-wrapper/utils/query-utils";
+import type { UserPageBrainActivityResponse } from "@/components/user/brain/userPageBrainActivity.helpers";
+import { Time } from "@/helpers/time";
+import { commonApiFetch } from "@/services/api/common-api";
+import { useQuery } from "@tanstack/react-query";
+
+interface UseIdentityActivityOptions {
+ readonly identity: string | null | undefined;
+ readonly enabled?: boolean | undefined;
+}
+
+export function useIdentityActivity({
+ identity,
+ enabled = true,
+}: Readonly) {
+ const normalizedIdentity = identity?.trim() ?? "";
+ const canonicalIdentity = normalizedIdentity.toLowerCase();
+
+ return useQuery({
+ queryKey: [QueryKey.IDENTITY_ACTIVITY, canonicalIdentity],
+ queryFn: async () => {
+ if (!canonicalIdentity) {
+ throw new Error("Identity is required to fetch brain activity");
+ }
+
+ return await commonApiFetch({
+ endpoint: `identities/${encodeURIComponent(canonicalIdentity)}/activity`,
+ });
+ },
+ enabled: enabled && canonicalIdentity.length > 0,
+ staleTime: Time.minutes(15).toMillis(),
+ gcTime: Time.hours(1).toMillis(),
+ refetchOnWindowFocus: false,
+ ...getDefaultQueryRetry(),
+ });
+}