feat: overhaul remote instance indicators in space sidebar
Replace truncated text labels with globe pill dividers and add federation badges to remote space icons. Remote spaces now show rich Tooltips with "SpaceName · host" instead of native title attrs. Disconnected instances display amber warning indicators throughout.
This commit is contained in:
@@ -4,6 +4,7 @@ import { useSpaceStore } from '../../stores/spaceStore';
|
|||||||
import { useChatStore } from '../../stores/chatStore';
|
import { useChatStore } from '../../stores/chatStore';
|
||||||
import { useUIStore } from '../../stores/uiStore';
|
import { useUIStore } from '../../stores/uiStore';
|
||||||
import { useInstanceStore } from '../../stores/instanceStore';
|
import { useInstanceStore } from '../../stores/instanceStore';
|
||||||
|
import { Tooltip } from '../ui/Tooltip';
|
||||||
|
|
||||||
import { getSpaceGradient, HOME_GRADIENT } from '../../utils/gradients';
|
import { getSpaceGradient, HOME_GRADIENT } from '../../utils/gradients';
|
||||||
|
|
||||||
@@ -17,9 +18,12 @@ interface SidebarItemProps {
|
|||||||
actionType?: 'add' | 'join' | 'explore';
|
actionType?: 'add' | 'join' | 'explore';
|
||||||
hasUnread?: boolean;
|
hasUnread?: boolean;
|
||||||
dimmed?: boolean;
|
dimmed?: boolean;
|
||||||
|
federationBadge?: boolean;
|
||||||
|
federationDisconnected?: boolean;
|
||||||
|
tooltipText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SidebarItem({ id, name, icon, active, onClick, type = 'space', actionType, hasUnread, dimmed }: SidebarItemProps) {
|
function SidebarItem({ id, name, icon, active, onClick, type = 'space', actionType, hasUnread, dimmed, federationBadge, federationDisconnected, tooltipText }: SidebarItemProps) {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const firstLetter = name.charAt(0).toUpperCase();
|
const firstLetter = name.charAt(0).toUpperCase();
|
||||||
|
|
||||||
@@ -66,22 +70,8 @@ function SidebarItem({ id, name, icon, active, onClick, type = 'space', actionTy
|
|||||||
return `${base} text-white ${active ? 'rounded-[13px]' : 'rounded-[20px] hover:rounded-[13px]'}`;
|
return `${base} text-white ${active ? 'rounded-[13px]' : 'rounded-[20px] hover:rounded-[13px]'}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const buttonContent = (
|
||||||
<div
|
<button onClick={onClick} className={`${getButtonClasses()} ${dimmed ? 'opacity-40 saturate-50' : ''}`} style={backgroundStyle} title={tooltipText ? undefined : name}>
|
||||||
className="relative flex items-center mb-1.5 w-full justify-center"
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
|
||||||
>
|
|
||||||
{/* Pill Indicator */}
|
|
||||||
{(type === 'space' || type === 'dm') && (
|
|
||||||
<div className="absolute -left-0 w-2 h-10 flex items-center">
|
|
||||||
<div
|
|
||||||
className={`bg-white rounded-r-full transition-all duration-200 origin-left ${getPillHeight()} w-1`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button onClick={onClick} className={`${getButtonClasses()} ${dimmed ? 'opacity-40 saturate-50' : ''}`} style={backgroundStyle} title={name}>
|
|
||||||
{type === 'dm' ? (
|
{type === 'dm' ? (
|
||||||
<span className="text-[17px] font-bold">B</span>
|
<span className="text-[17px] font-bold">B</span>
|
||||||
) : type === 'action' ? (
|
) : type === 'action' ? (
|
||||||
@@ -108,7 +98,65 @@ function SidebarItem({ id, name, icon, active, onClick, type = 'space', actionTy
|
|||||||
<span className="text-[15px] font-bold">{firstLetter}</span>
|
<span className="text-[15px] font-bold">{firstLetter}</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const innerContent = (
|
||||||
|
<div className="relative">
|
||||||
|
{buttonContent}
|
||||||
|
{federationBadge && (
|
||||||
|
<div className="absolute -bottom-0.5 -right-0.5 w-[14px] h-[14px] rounded-full bg-surface-base flex items-center justify-center">
|
||||||
|
{federationDisconnected ? (
|
||||||
|
<div className="w-[8px] h-[8px] rounded-full bg-accent-amber" />
|
||||||
|
) : (
|
||||||
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" className="text-txt-tertiary/80">
|
||||||
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative flex items-center mb-1.5 w-full justify-center"
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
{/* Pill Indicator */}
|
||||||
|
{(type === 'space' || type === 'dm') && (
|
||||||
|
<div className="absolute -left-0 w-2 h-10 flex items-center">
|
||||||
|
<div
|
||||||
|
className={`bg-white rounded-r-full transition-all duration-200 origin-left ${getPillHeight()} w-1`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tooltipText ? (
|
||||||
|
<Tooltip content={tooltipText} position="right" delay={300}>
|
||||||
|
{innerContent}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
innerContent
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InstanceDivider({ label, disconnected }: { label: string; disconnected: boolean }) {
|
||||||
|
return (
|
||||||
|
<Tooltip content={label} position="right" delay={300}>
|
||||||
|
<div className="relative flex items-center justify-center w-full my-2">
|
||||||
|
<div className="absolute inset-x-5 h-[1px] bg-interactive-muted/30 rounded-full" />
|
||||||
|
<div className={`relative z-[1] w-5 h-5 rounded-full bg-surface-base flex items-center justify-center ${
|
||||||
|
disconnected ? 'text-accent-amber' : 'text-txt-tertiary'
|
||||||
|
}`}>
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" className="opacity-60">
|
||||||
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,14 +274,15 @@ export function SpaceSidebar() {
|
|||||||
{/* Remote instance groups */}
|
{/* Remote instance groups */}
|
||||||
{groupedSpaces.remoteGroups.map(([origin, groupSpaces]) => {
|
{groupedSpaces.remoteGroups.map(([origin, groupSpaces]) => {
|
||||||
const inst = instances.find(i => i.origin === origin);
|
const inst = instances.find(i => i.origin === origin);
|
||||||
const label = inst?.label || (() => { try { return new URL(origin).host; } catch { return '?'; } })();
|
const hostLabel = (() => { try { return new URL(origin).host; } catch { return '?'; } })();
|
||||||
|
const label = inst?.label || hostLabel;
|
||||||
const isDimmed = disconnectedOrigins.has(origin);
|
const isDimmed = disconnectedOrigins.has(origin);
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={origin}>
|
<React.Fragment key={origin}>
|
||||||
<div className="w-8 h-[2px] bg-interactive-muted/50 rounded-full my-1" />
|
<InstanceDivider
|
||||||
<div className="text-[9px] text-txt-tertiary font-medium uppercase tracking-wider mb-1 truncate max-w-[52px] text-center" title={label}>
|
label={isDimmed ? `${label} (disconnected)` : label}
|
||||||
{label}
|
disconnected={isDimmed}
|
||||||
</div>
|
/>
|
||||||
{groupSpaces.map((space) => (
|
{groupSpaces.map((space) => (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
key={space.id}
|
key={space.id}
|
||||||
@@ -244,6 +293,9 @@ export function SpaceSidebar() {
|
|||||||
onClick={() => handleSpaceClick(space.id)}
|
onClick={() => handleSpaceClick(space.id)}
|
||||||
hasUnread={unreadSpaceIds.has(space.id)}
|
hasUnread={unreadSpaceIds.has(space.id)}
|
||||||
dimmed={isDimmed}
|
dimmed={isDimmed}
|
||||||
|
federationBadge
|
||||||
|
federationDisconnected={isDimmed}
|
||||||
|
tooltipText={`${space.name} \u00b7 ${hostLabel}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|||||||
Reference in New Issue
Block a user