feat: format and display full federated user handles across the application.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user