feat(app): show instance branding in switchers and pickers (#3289)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-08 17:34:56 +02:00
1 parent 6dc698ff5e
commit d6e3254a16
63 files changed
+929 -587

No files matched your search

@@ -0,0 +1,60 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import FluxerLogoAsset from '@app/media/images/fluxer-logo-color.svg?react';
import {GlobeIcon} from '@phosphor-icons/react';
import type React from 'react';
import {useState} from 'react';
interface InstanceBrandMarkProps {
readonly isOfficial: boolean;
readonly iconUrl: string | null;
readonly size: number;
readonly globeWeight?: 'regular' | 'bold';
readonly 'data-flx'?: string;
}
function InstanceBrandImage({
url,
size,
fallback,
}: {
readonly url: string;
readonly size: number;
readonly fallback: React.ReactElement;
}): React.ReactElement {
const [failed, setFailed] = useState(false);
if (failed) {
return fallback;
}
return (
<img
src={url}
alt=""
width={size}
height={size}
draggable={false}
decoding="async"
referrerPolicy="no-referrer"
onError={() => setFailed(true)}
data-flx="auth.components.instance-brand-mark.instance-brand-image.image"
/>
);
}
export function InstanceBrandMark({
isOfficial,
iconUrl,
size,
globeWeight = 'regular',
'data-flx': dataFlx,
}: InstanceBrandMarkProps): React.ReactElement {
if (isOfficial) {
return <FluxerLogoAsset aria-hidden="true" data-flx={dataFlx} />;
}
const globe = <GlobeIcon size={remFromPx(size)} weight={globeWeight} aria-hidden="true" data-flx={dataFlx} />;
if (iconUrl == null) {
return globe;
}
return <InstanceBrandImage key={iconUrl} url={iconUrl} size={size} fallback={globe} />;
}
@@ -2,11 +2,10 @@
import styles from '@app/features/auth/components/accounts/account_switcher_modal/AccountProfilePicker.module.css';
import type {AccountInstanceBadgeInfo} from '@app/features/auth/components/accounts/account_switcher_modal/useAccountInstanceBadge';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {InstanceBrandMark} from '@app/features/auth/components/InstanceBrandMark';
import {instanceDomainHost} from '@app/features/auth/flow/instance_selector/InstanceDirectoryStorage';
import {VerifiedConnectionIcon} from '@app/features/ui/components/icons/VerifiedConnectionIcon';
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
import FluxerLogoAsset from '@app/media/images/fluxer-logo-color.svg?react';
import {GlobeIcon} from '@phosphor-icons/react';
import type React from 'react';
interface AccountInstanceBadgeProps {
@@ -22,11 +21,15 @@ export function AccountInstanceBadge({
}: AccountInstanceBadgeProps): React.ReactElement {
let domainLabel = fallbackLabel;
let isOfficial = false;
let iconUrl: string | null = null;
let instanceName: string | null = null;
if (badge != null) {
domainLabel = badge.title;
isOfficial = badge.isOfficial;
iconUrl = badge.iconUrl;
instanceName = badge.label !== instanceDomainHost(badge.title) ? badge.label : null;
}
const ariaLabels = [domainLabel];
const ariaLabels = instanceName == null ? [domainLabel] : [instanceName, domainLabel];
if (isOfficial) {
ariaLabels.push(officialLabel);
}
@@ -39,7 +42,7 @@ export function AccountInstanceBadge({
className={styles.instanceTooltipLabel}
data-flx="auth.accounts.account-switcher-modal.account-instance-badge.render-tooltip.instance-tooltip-label"
>
{domainLabel}
{instanceName == null ? domainLabel : `${instanceName} (${domainLabel})`}
</span>
{isOfficial && (
<span
@@ -69,15 +72,13 @@ export function AccountInstanceBadge({
aria-label={ariaLabels.join(', ')}
data-flx="auth.accounts.account-switcher-modal.account-instance-badge.instance-badge"
>
{isOfficial ? (
<FluxerLogoAsset data-flx="auth.accounts.account-switcher-modal.account-instance-badge.fluxer-logo-asset" />
) : (
<GlobeIcon
size={remFromPx(15)}
weight="bold"
data-flx="auth.accounts.account-switcher-modal.account-instance-badge.globe-icon"
/>
)}
<InstanceBrandMark
isOfficial={isOfficial}
iconUrl={iconUrl}
size={15}
globeWeight="bold"
data-flx="auth.accounts.account-switcher-modal.account-instance-badge.instance-brand-mark"
/>
</span>
</Tooltip>
);
@@ -281,14 +281,20 @@
box-shadow: 0 0.25rem 0.75rem rgb(0 0 0 / 0.28);
}
.instanceBadge svg,
.instanceBadge img {
.instanceBadge svg {
display: block;
width: 72%;
height: 72%;
object-fit: cover;
}
.instanceBadge img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.instanceTooltip {
display: inline-flex;
align-items: center;
@@ -6,6 +6,7 @@ import {
normalizeInstanceName,
resolveInstanceLabel,
} from '@app/features/auth/flow/instance_selector/InstanceDirectoryStorage';
import {resolveInstanceBrandIconUrl} from '@app/features/auth/InstanceBranding';
import type {Account} from '@app/features/platform/state/AuthSession';
import {isOfficialInstanceHost, OFFICIAL_INSTANCE_NAME} from '@fluxer/instance_bootstrap/src/OfficialInstance';
import {useMemo} from 'react';
@@ -14,6 +15,7 @@ export interface AccountInstanceBadgeInfo {
readonly label: string;
readonly title: string;
readonly isOfficial: boolean;
readonly iconUrl: string | null;
}
function findKnownInstance(instanceKey: string, knownInstances: ReadonlyArray<InstanceInfo>): InstanceInfo | null {
@@ -44,10 +46,15 @@ function getAccountInstanceBadgeInfo(
}
const isOfficial = isOfficialInstanceHost(instanceKey);
if (isOfficial) {
return {label: OFFICIAL_INSTANCE_NAME, title: domain, isOfficial};
return {label: OFFICIAL_INSTANCE_NAME, title: domain, isOfficial, iconUrl: null};
}
const knownInstance = findKnownInstance(instanceKey, knownInstances);
return {label: resolveInstanceName(account, knownInstance, domain), title: domain, isOfficial};
return {
label: resolveInstanceName(account, knownInstance, domain),
title: domain,
isOfficial,
iconUrl: resolveInstanceBrandIconUrl(account.instance),
};
}
export function useAccountInstanceBadge(
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
import {Endpoints} from '@app/features/app/constants/Endpoints';
import {getActiveInstanceProductName} from '@app/features/app/state/ActiveInstanceProductName';
import {useAuthorizeParams} from '@app/features/auth/components/pages/oauth_authorize_page/hooks/useAuthorizeParams';
import {
type BotInviteDestinationOption,
@@ -459,7 +459,8 @@ export function useAuthorizeFlow(options: UseAuthorizeFlowOptions = {}): Authori
}
}, [params, onCancelOverride, publicAppState.data, i18n]);
const sessionExpiredMessage = useMemo(
() => i18n._(YOUR_PRODUCT_SESSION_EXPIRED_SIGN_IN_AGAIN_TO_DESCRIPTOR, {productName: PRODUCT_NAME}),
() =>
i18n._(YOUR_PRODUCT_SESSION_EXPIRED_SIGN_IN_AGAIN_TO_DESCRIPTOR, {productName: getActiveInstanceProductName()}),
[i18n.locale],
);
return {