chore(layout): remove MatureContentMigrationAlert

Drop the green-domain 'mature content moved to civitai.red' banner: the component file, its AppLayout usage, and the stale test-checklist reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
briant
2026-06-29 13:45:35 -06:00
parent b7a23ed785
commit 05839641c6
3 changed files with 3 additions and 157 deletions
@@ -1,152 +0,0 @@
import { useCallback, useRef } from 'react';
import { Button, CloseButton, Text, ThemeIcon } from '@mantine/core';
import { IconArrowRight, IconPepper } from '@tabler/icons-react';
import { useSession } from '~/providers/SessionProvider';
import { useFeatureFlags, useFeatureFlagsReady } from '~/providers/FeatureFlagsProvider';
import { useServerDomains } from '~/providers/AppProvider';
import { nsfwBrowsingLevelsFlag } from '~/shared/constants/browsingLevel.constants';
import { Flags } from '~/shared/utils/flags';
import { syncAccount } from '~/utils/sync-account';
import { trpc } from '~/utils/trpc';
const ALERT_ID = 'mature-content-migration';
export function MatureContentMigrationAlert() {
const features = useFeatureFlags();
const serverDomains = useServerDomains();
const { data: session } = useSession();
// Check the raw session user preferences (before domain override).
// On green, the BrowserSettingsProvider forces showNsfw=false, so we
// need the original values to know if the user would see NSFW elsewhere.
const user = session?.user;
const ready = useFeatureFlagsReady();
// `AppProvider` seeds `user.getSettings` with SSR `initialData`, and the
// dismiss mutation keeps the cache current via an optimistic update — so the
// SSR snapshot is authoritative for `dismissedAlerts` without a per-mount
// refetch. Gate visibility on `ready` (the per-user feature-flag overlay) so
// we don't flash against the anon `features.isGreen` value.
const { data: settings } = trpc.user.getSettings.useQuery(undefined, {
enabled: !!user,
});
const isDismissed = (settings?.dismissedAlerts ?? []).includes(ALERT_ID);
const hasNsfwEnabled =
user?.showNsfw && Flags.intersects(user.browsingLevel, nsfwBrowsingLevelsFlag);
const utils = trpc.useUtils();
const dismissMutation = trpc.user.dismissAlert.useMutation({
onMutate: async () => {
await utils.user.getSettings.cancel();
const prev = utils.user.getSettings.getData();
utils.user.getSettings.setData(undefined, (old) => ({
...old,
dismissedAlerts: [...(old?.dismissedAlerts ?? []), ALERT_ID],
}));
return { prev };
},
onError: (_err, _vars, ctx) => {
if (ctx?.prev) utils.user.getSettings.setData(undefined, ctx.prev);
},
// Reconcile with server truth after the optimistic update: the optimistic
// setData spreads `...old`, so if the cached base was incomplete (e.g. a
// failed SSR settings snapshot) it could persist a truncated settings
// object. Refetch once on dismiss to restore the full object + confirm the
// stored dismissedAlerts. One request per dismiss (rare) — not per mount.
onSettled: () => {
utils.user.getSettings.invalidate();
},
});
// Spotlight effect — ref + direct DOM to avoid re-renders on mouse move
const spotlightRef = useRef<HTMLDivElement>(null);
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
const el = spotlightRef.current;
if (!el) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
el.style.background = `radial-gradient(300px circle at ${x}px ${y}px, rgba(239,68,68,0.08), transparent 70%)`;
el.style.opacity = '1';
}, []);
const handleMouseLeave = useCallback(() => {
const el = spotlightRef.current;
if (el) el.style.opacity = '0';
}, []);
// `!settings` guards the rare path where the SSR `/api/user/settings` snapshot
// failed (→ undefined initialData): the query self-heals via a mount fetch, and
// until it lands we must not render against undefined `dismissedAlerts` (which
// would briefly re-show an already-dismissed alert). On the normal SSR-seeded
// path `settings` is defined immediately, so this adds no delay or refetch.
if (!features.isGreen || !hasNsfwEnabled || !ready || !settings || isDismissed) return null;
const redDomain = serverDomains.red;
const redUrl = syncAccount(`//${redDomain}`);
const handleDismiss = () => dismissMutation.mutate({ alertId: ALERT_ID });
return (
<div className="container mb-3">
<div
className="relative overflow-hidden rounded-lg border border-red-9/30 bg-gradient-to-r from-red-9/15 via-red-9/5 to-transparent"
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
{/* Spotlight glow — styled via ref to avoid re-renders */}
<div
ref={spotlightRef}
className="pointer-events-none absolute inset-0 transition-opacity duration-500"
style={{ opacity: 0 }}
/>
<div className="relative flex items-center gap-4 px-4 py-3">
<ThemeIcon variant="light" color="red" size="lg" radius="xl" className="shrink-0">
<IconPepper size={20} />
</ThemeIcon>
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-4 gap-y-2">
<Text size="sm" className="text-gray-2">
<span className="font-semibold text-red-4">Mature content</span> now lives at{' '}
<Text
component="a"
href={redUrl}
target="_blank"
rel="noreferrer nofollow"
size="sm"
fw={700}
className="text-red-4 underline decoration-red-4/40 underline-offset-2 transition-colors hover:text-red-3 hover:decoration-red-3"
>
civitai.red
</Text>{' '}
&mdash; same account, same Buzz, new home.
</Text>
<Button
component="a"
href={redUrl}
target="_blank"
rel="noreferrer nofollow"
color="red"
variant="outline"
size="compact-sm"
radius="xl"
rightSection={<IconArrowRight size={14} />}
className="shrink-0"
>
Explore civitai.red
</Button>
</div>
<CloseButton
size="sm"
variant="subtle"
color="gray"
radius="xl"
onClick={handleDismiss}
aria-label="Dismiss"
className="shrink-0"
/>
</div>
</div>
</div>
);
}
-2
View File
@@ -9,7 +9,6 @@ import { SubNav2 } from '~/components/AppLayout/SubNav';
import { PageLoader } from '~/components/PageLoader/PageLoader';
import { ScrollArea } from '~/components/ScrollArea/ScrollArea';
import { useScrollAreaRef } from '~/components/ScrollArea/ScrollAreaContext';
import { MatureContentMigrationAlert } from '~/components/Alerts/MatureContentMigrationAlert';
import { Announcements } from '~/components/Announcements/Announcements';
import type { ScrollAreaProps } from '~/components/ScrollArea/ScrollArea';
import { AdhesiveAd } from '~/components/Ads/AdhesiveAd';
@@ -131,7 +130,6 @@ export function MainContent({
)}
{!subNav && <RewardsBonusBanner />}
{announcements && <Announcements className="mb-3" />}
{announcements && <MatureContentMigrationAlert />}
{children}
</main>
{footer}
+3 -3
View File
@@ -136,9 +136,9 @@ test.describe('SSR-injected browsingSettingsAddons (logged-in)', () => {
* [ ] Chat icon no-flash: logged-in user WITH chat disabled — the chat icon
* must NOT appear-then-disappear on load. It should gate on
* useFeatureFlagsReady() and only render once the user flag overlay settles.
* [ ] No per-mount getSettings refetch: with the four ambient consumers mounted
* (chat icon + MatureContentMigrationAlert + NavTidyNotice +
* YellowBuzzMigrationNotice), client-side nav between pages must NOT trigger
* [ ] No per-mount getSettings refetch: with the ambient consumers mounted
* (chat icon + NavTidyNotice + YellowBuzzMigrationNotice), client-side nav
* between pages must NOT trigger
* a `user.getSettings` refetch per mount (they now read the SSR-seeded
* cache + gate on useFeatureFlagsReady, not staleTime:0 + isFetched).
* [ ] Alert render/dismiss/persist: a migration/nav alert renders from