feat: format and display full federated user handles across the application.

This commit is contained in:
Christopher
2026-01-22 19:11:00 -08:00
parent f08ac13138
commit 66cd763c30
8 changed files with 37 additions and 10 deletions
+3 -2
View File
@@ -8,6 +8,7 @@ import { PostCard } from '@/components/PostCard';
import { User, Post } from '@/lib/types';
import AutoTextarea from '@/components/AutoTextarea';
import { Rocket } from 'lucide-react';
import { formatFullHandle } from '@/lib/utils/handle';
interface UserSummary {
id: string;
@@ -29,7 +30,7 @@ function UserRow({ user }: { user: UserSummary }) {
</div>
<div className="user-row-content">
<div style={{ fontWeight: 600 }}>{user.displayName || user.handle}</div>
<div style={{ color: 'var(--foreground-tertiary)', fontSize: '13px' }}>@{user.handle}</div>
<div style={{ color: 'var(--foreground-tertiary)', fontSize: '13px' }}>{formatFullHandle(user.handle)}</div>
{user.bio && (
<div className="user-row-bio">{user.bio}</div>
)}
@@ -340,7 +341,7 @@ export default function ProfilePage() {
{/* User Info */}
<div style={{ padding: '12px 0' }}>
<h2 style={{ fontSize: '20px', fontWeight: 700 }}>{user.displayName || user.handle}</h2>
<p style={{ color: 'var(--foreground-tertiary)' }}>@{user.handle}</p>
<p style={{ color: 'var(--foreground-tertiary)' }}>{formatFullHandle(user.handle)}</p>
{user.bio && (
<p style={{ marginTop: '12px', lineHeight: 1.5 }}>{user.bio}</p>
+1 -1
View File
@@ -25,7 +25,7 @@ const createPostSchema = z.object({
title: z.string().optional(),
description: z.string().optional(),
image: z.string().url().optional().nullable(),
}).optional(),
}).optional().nullable(),
});
// Create a new post
+2 -1
View File
@@ -5,6 +5,7 @@ import Link from 'next/link';
import { SearchIcon, TrendingIcon, UsersIcon } from '@/components/Icons';
import { PostCard } from '@/components/PostCard';
import { Post } from '@/lib/types';
import { formatFullHandle } from '@/lib/utils/handle';
interface User {
id: string;
@@ -28,7 +29,7 @@ function UserCard({ user }: { user: User }) {
</div>
<div className="user-card-info">
<div className="user-card-name">{user.displayName || user.handle}</div>
<div className="user-card-handle">@{user.handle}</div>
<div className="user-card-handle">{formatFullHandle(user.handle)}</div>
{user.bio && <div className="user-card-bio">{user.bio}</div>}
</div>
</Link>
+3 -2
View File
@@ -3,6 +3,7 @@
import { useState, useEffect, useCallback } from 'react';
import Link from 'next/link';
import { useSearchParams, useRouter } from 'next/navigation';
import { formatFullHandle } from '@/lib/utils/handle';
interface User {
id: string;
@@ -97,7 +98,7 @@ function UserCard({ user }: { user: User }) {
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600 }}>{user.displayName || user.handle}</div>
<div style={{ color: 'var(--foreground-tertiary)', fontSize: '14px' }}>@{user.handle}</div>
<div style={{ color: 'var(--foreground-tertiary)', fontSize: '14px' }}>{formatFullHandle(user.handle)}</div>
{user.bio && (
<div style={{
color: 'var(--foreground-secondary)',
@@ -148,7 +149,7 @@ function PostCard({ post }: { post: Post }) {
<Link href={`/@${post.author?.handle}`} className="post-handle">
{post.author?.displayName || post.author?.handle}
</Link>
<span className="post-time">@{post.author?.handle} · {formatTime(post.createdAt)}</span>
<span className="post-time">{formatFullHandle(post.author?.handle || '')} · {formatTime(post.createdAt)}</span>
</div>
</div>
<div className="post-content">{post.content}</div>
+2 -1
View File
@@ -5,6 +5,7 @@ import AutoTextarea from '@/components/AutoTextarea';
import { Post, Attachment } from '@/lib/types';
import { ImageIcon } from 'lucide-react';
import { VideoEmbed } from '@/components/VideoEmbed';
import { formatFullHandle } from '@/lib/utils/handle';
interface ComposeProps {
onPost: (content: string, mediaIds: string[], linkPreview?: any, replyToId?: string) => void;
@@ -121,7 +122,7 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
{replyingTo && !isReply && (
<div className="compose-reply-target">
<div className="compose-reply-info">
Replying to <span className="compose-reply-handle">@{replyingTo.author.handle}</span>
Replying to <span className="compose-reply-handle">{formatFullHandle(replyingTo.author.handle)}</span>
</div>
<button type="button" className="compose-reply-cancel" onClick={onCancelReply}>
Cancel
+3 -2
View File
@@ -6,6 +6,7 @@ import { HeartIcon, RepeatIcon, MessageIcon, FlagIcon, TrashIcon } from '@/compo
import { Post } from '@/lib/types';
import { useAuth } from '@/lib/contexts/AuthContext';
import { VideoEmbed } from '@/components/VideoEmbed';
import { formatFullHandle } from '@/lib/utils/handle';
interface PostCardProps {
post: Post;
@@ -159,13 +160,13 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, isDetail
<Link href={`/${post.author.handle}`} className="post-handle" onClick={(e) => e.stopPropagation()}>
{post.author.displayName || post.author.handle}
</Link>
<span className="post-time">@{post.author.handle} · {formatTime(post.createdAt)}</span>
<span className="post-time">{formatFullHandle(post.author.handle)} · {formatTime(post.createdAt)}</span>
</div>
</div>
{post.replyTo && (
<div className="post-reply-to">
Replied to <Link href={`/${post.replyTo.author.handle}`} onClick={(e) => e.stopPropagation()}>@{post.replyTo.author.handle}</Link>
Replied to <Link href={`/${post.replyTo.author.handle}`} onClick={(e) => e.stopPropagation()}>{formatFullHandle(post.replyTo.author.handle)}</Link>
</div>
)}
+2 -1
View File
@@ -4,6 +4,7 @@ import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useAuth } from '@/lib/contexts/AuthContext';
import { HomeIcon, SearchIcon, BellIcon, UserIcon, ShieldIcon, SynapsisLogo, BookOpenIcon, SettingsIcon } from './Icons';
import { formatFullHandle } from '@/lib/utils/handle';
export function Sidebar() {
const { user, isAdmin } = useAuth();
@@ -71,7 +72,7 @@ export function Sidebar() {
</div>
<div>
<div style={{ fontWeight: 600, fontSize: '14px' }}>{user.displayName}</div>
<div style={{ color: 'var(--foreground-tertiary)', fontSize: '13px' }}>@{user.handle}</div>
<div style={{ color: 'var(--foreground-tertiary)', fontSize: '13px' }}>{formatFullHandle(user.handle)}</div>
</div>
</div>
</div>
+21
View File
@@ -0,0 +1,21 @@
export const NODE_DOMAIN = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
/**
* Formats a handle into its full federated form: @user@domain
* If the handle already contains a domain (e.g. user@other.com), it returns it as @user@other.com
* If it's a local handle (e.g. user), it appends the local node domain: @user@localnode.com
*/
export function formatFullHandle(handle: string): string {
if (!handle) return '';
// Remove leading @ if present for processing
const cleanHandle = handle.startsWith('@') ? handle.slice(1) : handle;
// Check if it already has a domain (contains @)
if (cleanHandle.includes('@')) {
return `@${cleanHandle}`;
}
// Append local node domain
return `@${cleanHandle}@${NODE_DOMAIN}`;
}