mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
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:
@@ -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>{' '}
|
||||
— 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user