Prevent local admin profile media from blurring during NSFW node updates

Hop-State: A_06FPE5MS0ZXE3ESZJJ27C98
Hop-Proposal: R_06FPE5KR2QGEVQD0MW8DSZR
Hop-Task: T_06FPE564WJTQYM53WG3W8P8
Hop-Attempt: AT_06FPE564WKTFF688KW6DM80
This commit is contained in:
2026-07-15 11:42:42 -07:00
committed by Hop
parent 30c917b3a9
commit a1841c51a7
3 changed files with 20 additions and 4 deletions
+7
View File
@@ -8,10 +8,12 @@ import { useToast } from '@/lib/contexts/ToastContext';
import { useAccentColor } from '@/lib/contexts/AccentColorContext'; import { useAccentColor } from '@/lib/contexts/AccentColorContext';
import { getStorageProvider, MediaUploadError, uploadMediaFile } from '@/lib/stuffbox/browser-upload'; import { getStorageProvider, MediaUploadError, uploadMediaFile } from '@/lib/stuffbox/browser-upload';
import { hasUnsavedChanges } from '@/lib/forms/dirty-state'; import { hasUnsavedChanges } from '@/lib/forms/dirty-state';
import { useRuntimeConfig } from '@/lib/contexts/ConfigContext';
export default function AdminPage() { export default function AdminPage() {
const { showToast } = useToast(); const { showToast } = useToast();
const { refreshAccentColor } = useAccentColor(); const { refreshAccentColor } = useAccentColor();
const { setNodeNsfw } = useRuntimeConfig();
const [isAdmin, setIsAdmin] = useState<boolean | null>(null); const [isAdmin, setIsAdmin] = useState<boolean | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [nodeSettings, setNodeSettings] = useState({ const [nodeSettings, setNodeSettings] = useState({
@@ -93,6 +95,11 @@ export default function AdminPage() {
savedNodeSettingsRef.current = payload; savedNodeSettingsRef.current = payload;
const data = await res.json().catch(() => ({})); const data = await res.json().catch(() => ({}));
if (data.node) { if (data.node) {
// Keep the global local-node classification and the sidebar's
// node payload in the same React batch. Otherwise profile
// media briefly sees "remote NSFW node on a safe local node"
// and applies a blur until the next full config refresh.
setNodeNsfw(data.node.isNsfw === true);
window.dispatchEvent(new CustomEvent('synapsis:node-updated', { detail: data.node })); window.dispatchEvent(new CustomEvent('synapsis:node-updated', { detail: data.node }));
} }
showToast('Settings saved!', 'success'); showToast('Settings saved!', 'success');
+5 -2
View File
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { AvatarImage } from './AvatarImage'; import { AvatarImage } from './AvatarImage';
import { ProfileBanner } from './ProfileBanner'; import { ProfileBanner } from './ProfileBanner';
import { useRuntimeConfig } from '@/lib/contexts/ConfigContext';
interface Admin { interface Admin {
handle: string; handle: string;
@@ -34,6 +35,8 @@ function formatNetworkTotal(value: number | undefined): string {
} }
export function RightSidebar() { export function RightSidebar() {
const { config } = useRuntimeConfig();
const localNodeIsNsfw = config?.isNsfw ?? false;
const fallbackDescription = process.env.NEXT_PUBLIC_NODE_DESCRIPTION || 'A swarm social network node.'; const fallbackDescription = process.env.NEXT_PUBLIC_NODE_DESCRIPTION || 'A swarm social network node.';
const [nodeInfo, setNodeInfo] = useState<NodeInfo>({ const [nodeInfo, setNodeInfo] = useState<NodeInfo>({
name: process.env.NEXT_PUBLIC_NODE_NAME || 'Synapsis Node', name: process.env.NEXT_PUBLIC_NODE_NAME || 'Synapsis Node',
@@ -138,7 +141,7 @@ export function RightSidebar() {
{nodeInfo.bannerUrl && ( {nodeInfo.bannerUrl && (
<ProfileBanner <ProfileBanner
url={nodeInfo.bannerUrl} url={nodeInfo.bannerUrl}
nodeIsNsfw={nodeInfo.isNsfw} nodeIsNsfw={localNodeIsNsfw}
height={140} height={140}
borderBottom="1px solid var(--border)" borderBottom="1px solid var(--border)"
/> />
@@ -187,7 +190,7 @@ export function RightSidebar() {
style={{ display: 'flex', alignItems: 'center', gap: '10px', textDecoration: 'none', color: 'inherit' }} style={{ display: 'flex', alignItems: 'center', gap: '10px', textDecoration: 'none', color: 'inherit' }}
> >
<div className="avatar avatar-sm" style={{ flexShrink: 0 }}> <div className="avatar avatar-sm" style={{ flexShrink: 0 }}>
<AvatarImage avatarUrl={admin.avatarUrl} seed={admin.handle} isNsfw={admin.isNsfw} nodeIsNsfw={nodeInfo.isNsfw} alt={admin.displayName || admin.handle} /> <AvatarImage avatarUrl={admin.avatarUrl} seed={admin.handle} isNsfw={admin.isNsfw} nodeIsNsfw={localNodeIsNsfw} alt={admin.displayName || admin.handle} />
</div> </div>
<div> <div>
<div style={{ fontWeight: 500, fontSize: '14px' }}> <div style={{ fontWeight: 500, fontSize: '14px' }}>
+8 -2
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'; import { createContext, useCallback, useContext, useEffect, useState, ReactNode } from 'react';
interface RuntimeConfig { interface RuntimeConfig {
domain: string; domain: string;
@@ -10,17 +10,23 @@ interface RuntimeConfig {
interface ConfigContextType { interface ConfigContextType {
config: RuntimeConfig | null; config: RuntimeConfig | null;
isLoading: boolean; isLoading: boolean;
setNodeNsfw: (isNsfw: boolean) => void;
} }
const ConfigContext = createContext<ConfigContextType>({ const ConfigContext = createContext<ConfigContextType>({
config: null, config: null,
isLoading: true, isLoading: true,
setNodeNsfw: () => { },
}); });
export function ConfigProvider({ children }: { children: ReactNode }) { export function ConfigProvider({ children }: { children: ReactNode }) {
const [config, setConfig] = useState<RuntimeConfig | null>(null); const [config, setConfig] = useState<RuntimeConfig | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const setNodeNsfw = useCallback((isNsfw: boolean) => {
setConfig((current) => current ? { ...current, isNsfw } : current);
}, []);
useEffect(() => { useEffect(() => {
// Fetch runtime config on mount // Fetch runtime config on mount
fetch('/api/config') fetch('/api/config')
@@ -44,7 +50,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
}, []); }, []);
return ( return (
<ConfigContext.Provider value={{ config, isLoading }}> <ConfigContext.Provider value={{ config, isLoading, setNodeNsfw }}>
{children} {children}
</ConfigContext.Provider> </ConfigContext.Provider>
); );