feat: stateless federated identity resolver + federation UX improvements
Add identity.ts with isSelf() and resolveDisplayIdentity() — pure stateless functions that detect replicated-self using the immutable (username, homeInstance) composite key. No store lookups, no data mutation. Fixes wrong avatar gradient and missing edit/delete on own messages in remote channels. Also includes: optimistic message dedup fix for cross-instance messages (content-only matching), federation toast notifications, Username component with @domain display, invite parser, and deploy script simplification.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
|
||||
const borderColors = {
|
||||
info: 'border-l-accent-sky',
|
||||
warning: 'border-l-accent-amber',
|
||||
success: 'border-l-accent-mint',
|
||||
} as const;
|
||||
|
||||
export function ToastContainer() {
|
||||
const toasts = useUIStore((s) => s.toasts);
|
||||
const removeToast = useUIStore((s) => s.removeToast);
|
||||
|
||||
if (toasts.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-6 right-6 z-[300] flex flex-col gap-2 pointer-events-none">
|
||||
{toasts.map((toast) => (
|
||||
<div
|
||||
key={toast.id}
|
||||
className={`glass-pill border-l-2 ${borderColors[toast.type]} rounded-[10px] px-4 py-2.5 max-w-[320px] animate-slide-up pointer-events-auto cursor-pointer`}
|
||||
onClick={() => removeToast(toast.id)}
|
||||
>
|
||||
<span className="text-sm text-txt-primary leading-snug">{toast.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { User } from '@backspace/shared';
|
||||
import { Avatar } from '../ui/Avatar';
|
||||
import { Username } from '../ui/Username';
|
||||
import { api } from '../../api/client';
|
||||
import { useServerStore } from '../../stores/serverStore';
|
||||
import { useUIStore } from '../../stores/uiStore';
|
||||
@@ -77,12 +78,15 @@ export function UserProfilePopout({ user, onClose, position }: UserProfilePopout
|
||||
|
||||
{/* Name & info — flows naturally after avatar */}
|
||||
<div>
|
||||
<div className="text-[16px] font-semibold text-txt-primary leading-tight">
|
||||
{displayName}
|
||||
</div>
|
||||
<div className="text-[13px] text-txt-tertiary">
|
||||
@{user.username}
|
||||
</div>
|
||||
<Username
|
||||
username={displayName}
|
||||
className="text-[16px] font-semibold text-txt-primary leading-tight"
|
||||
/>
|
||||
{user.username.includes('@') ? (
|
||||
<Username username={user.username} className="text-[13px] text-txt-tertiary" />
|
||||
) : (
|
||||
<div className="text-[13px] text-txt-tertiary">@{user.username}</div>
|
||||
)}
|
||||
{user.customStatus && (
|
||||
<div className="text-[13px] text-txt-secondary italic mt-1">
|
||||
{user.customStatus}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from './Tooltip';
|
||||
|
||||
interface UsernameProps {
|
||||
username: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function Username({ username, className, style }: UsernameProps) {
|
||||
const atIndex = username.indexOf('@');
|
||||
if (atIndex === -1) {
|
||||
return <span className={className} style={style}>{username}</span>;
|
||||
}
|
||||
const name = username.slice(0, atIndex);
|
||||
const domain = username.slice(atIndex + 1);
|
||||
return (
|
||||
<Tooltip content={username} position="top">
|
||||
<span className={className} style={style}>
|
||||
{name}
|
||||
<span className="text-txt-tertiary text-[0.8em] ml-0.5 font-normal">@{domain}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user