Fix username display issues (#6471)

# Description of Changes
Main fixes:
- Fix the display of the username in the bottom left
- Now displays as "User" when not logged in on self-hosted (desktop) and
"Guest" on SaaS when logged in anonymously
- Now updates properly when the user logs in/out in SaaS, desktop and
self-hosted
- Fix incremental build issues in the desktop app that have been here
since the start (I hope at least - I think the issue is that the JLink
is built read-only and then on subsequent builds you get OS errors when
trying to override the JLink with the new version. There's no real need
for it to be read-only that I know of, so we might as well just make it
R/W and ship like that)
This commit is contained in:
James Brunton
2026-05-29 14:35:47 +00:00
committed by GitHub
parent 83ea07ed6a
commit 4d5eeb103f
17 changed files with 561 additions and 238 deletions
@@ -1,6 +1,19 @@
export interface AuthContextType {
session: null;
user: { id?: string; email?: string; [key: string]: unknown } | null;
/**
* Human-readable name to show in the UI for the current session.
* - A real identity (username/email/full_name) when the user is signed in.
* - A layer-specific placeholder (e.g. "Guest" in SaaS, "User" in
* proprietary) for anonymous sessions.
* - null only when there is no user object at all (signed-out, or core
* OSS with no auth context) - consumers can fall back to whatever
* makes sense in their build.
*
* Each layer derives this from its own native user shape - consumers
* should treat the resulting string as opaque display text.
*/
displayName: string | null;
loading: boolean;
error: Error | null;
signOut: () => Promise<void>;
@@ -15,6 +28,7 @@ export function useAuth(): AuthContextType {
return {
session: null,
user: null,
displayName: null,
loading: false,
error: null,
signOut: async () => {},
@@ -22,11 +22,9 @@ export default function AppConfigModalLazy({
if (opened) setShouldMount(true);
}, [opened]);
if (!shouldMount) return null;
return (
<Suspense fallback={null}>
<AppConfigModal opened={opened} onClose={onClose} />
{shouldMount && <AppConfigModal opened={opened} onClose={onClose} />}
</Suspense>
);
}
@@ -18,6 +18,7 @@ import {
} from "@app/contexts/NavigationContext";
import { useViewer } from "@app/contexts/ViewerContext";
import { useFileHandler } from "@app/hooks/useFileHandler";
import { useAuth } from "@app/auth/UseSession";
import {
useIndexedDB,
useIndexedDBRevision,
@@ -107,19 +108,39 @@ const FileSidebar = forwardRef<HTMLDivElement, FileSidebarProps>(
const { activeFileId, setActiveFileId } = useViewer();
const { addFiles } = useFileHandler();
const indexedDB = useIndexedDB();
const [displayName, setDisplayName] = useState<string>("Guest");
// Each auth layer derives its own displayName from its native user shape.
// Fall back to the proprietary REST endpoint only when the auth
// context yields nothing - then to "User" as a generic last resort.
const { displayName: authDisplayName } = useAuth();
const [accountUsername, setAccountUsername] = useState<string | null>(null);
const displayName =
authDisplayName ?? accountUsername ?? t("auth.displayName.user", "User");
useEffect(() => {
if (!config?.enableLogin) return;
if (!config?.enableLogin) {
setAccountUsername(null);
return;
}
if (authDisplayName) {
// The auth context has a name; don't bother hitting the REST
// endpoint, but clear any stale cached value from a prior call.
setAccountUsername(null);
return;
}
accountService
.getAccountData()
.then((data) => {
if (data?.username) setDisplayName(data.username);
// Always reflect the latest result - including clearing it on
// sign-out, when the endpoint returns no username (or 401s into
// the catch branch below). Without this, signing out would leave
// the old username on screen.
setAccountUsername(data?.username ?? null);
})
.catch(() => {
/* not logged in or security disabled */
setAccountUsername(null);
});
}, [config?.enableLogin]);
}, [config?.enableLogin, authDisplayName]);
// Leaf files = user-visible files (excludes intermediate tool outputs)
const [allFileStubs, setAllFileStubs] = useState<StirlingFileStub[]>([]);
@@ -31,7 +31,11 @@ const toneStyles: Record<
};
interface InfoBannerProps {
icon: string;
/**
* Either a LocalIcon name (string) for the standard sized icon slot, or a
* pre-rendered ReactNode (e.g. a logo image) which is dropped in as-is.
*/
icon?: string | ReactNode;
title?: ReactNode;
message: ReactNode;
buttonText?: string;
@@ -48,6 +52,8 @@ interface InfoBannerProps {
iconColor?: string;
buttonColor?: string;
buttonVariant?: "light" | "filled" | "white" | "outline" | "subtle";
/** Override the button label colour (for dark/custom theme variants). */
buttonTextColor?: string;
minHeight?: number | string;
closeIconColor?: string;
compact?: boolean;
@@ -74,6 +80,7 @@ export const InfoBanner: React.FC<InfoBannerProps> = ({
iconColor,
buttonColor,
buttonVariant = "light",
buttonTextColor,
minHeight = 56,
closeIconColor,
compact = false,
@@ -120,12 +127,21 @@ export const InfoBanner: React.FC<InfoBannerProps> = ({
wrap="nowrap"
style={{ flex: 1, minWidth: 0 }}
>
<LocalIcon
icon={icon}
width={iconSize}
height={iconSize}
style={{ color: iconColor ?? toneStyle.icon, flexShrink: 0 }}
/>
{icon != null &&
(typeof icon === "string" ? (
<LocalIcon
icon={icon}
width={iconSize}
height={iconSize}
style={{ color: iconColor ?? toneStyle.icon, flexShrink: 0 }}
/>
) : (
<div
style={{ flexShrink: 0, display: "flex", alignItems: "center" }}
>
{icon}
</div>
))}
<Stack gap={compact ? 1 : 2} style={{ flex: 1, minWidth: 0 }}>
{title && (
<Text
@@ -161,6 +177,11 @@ export const InfoBanner: React.FC<InfoBannerProps> = ({
height={compact ? "0.75rem" : "0.9rem"}
/>
}
styles={
buttonTextColor
? { label: { color: buttonTextColor } }
: undefined
}
>
{buttonText}
</Button>