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 { User, Post } from '@/lib/types';
|
||||||
import AutoTextarea from '@/components/AutoTextarea';
|
import AutoTextarea from '@/components/AutoTextarea';
|
||||||
import { Rocket } from 'lucide-react';
|
import { Rocket } from 'lucide-react';
|
||||||
|
import { formatFullHandle } from '@/lib/utils/handle';
|
||||||
|
|
||||||
interface UserSummary {
|
interface UserSummary {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -29,7 +30,7 @@ function UserRow({ user }: { user: UserSummary }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="user-row-content">
|
<div className="user-row-content">
|
||||||
<div style={{ fontWeight: 600 }}>{user.displayName || user.handle}</div>
|
<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 && (
|
{user.bio && (
|
||||||
<div className="user-row-bio">{user.bio}</div>
|
<div className="user-row-bio">{user.bio}</div>
|
||||||
)}
|
)}
|
||||||
@@ -340,7 +341,7 @@ export default function ProfilePage() {
|
|||||||
{/* User Info */}
|
{/* User Info */}
|
||||||
<div style={{ padding: '12px 0' }}>
|
<div style={{ padding: '12px 0' }}>
|
||||||
<h2 style={{ fontSize: '20px', fontWeight: 700 }}>{user.displayName || user.handle}</h2>
|
<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 && (
|
{user.bio && (
|
||||||
<p style={{ marginTop: '12px', lineHeight: 1.5 }}>{user.bio}</p>
|
<p style={{ marginTop: '12px', lineHeight: 1.5 }}>{user.bio}</p>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const createPostSchema = z.object({
|
|||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
image: z.string().url().optional().nullable(),
|
image: z.string().url().optional().nullable(),
|
||||||
}).optional(),
|
}).optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a new post
|
// Create a new post
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Link from 'next/link';
|
|||||||
import { SearchIcon, TrendingIcon, UsersIcon } from '@/components/Icons';
|
import { SearchIcon, TrendingIcon, UsersIcon } from '@/components/Icons';
|
||||||
import { PostCard } from '@/components/PostCard';
|
import { PostCard } from '@/components/PostCard';
|
||||||
import { Post } from '@/lib/types';
|
import { Post } from '@/lib/types';
|
||||||
|
import { formatFullHandle } from '@/lib/utils/handle';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -28,7 +29,7 @@ function UserCard({ user }: { user: User }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="user-card-info">
|
<div className="user-card-info">
|
||||||
<div className="user-card-name">{user.displayName || user.handle}</div>
|
<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>}
|
{user.bio && <div className="user-card-bio">{user.bio}</div>}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useSearchParams, useRouter } from 'next/navigation';
|
import { useSearchParams, useRouter } from 'next/navigation';
|
||||||
|
import { formatFullHandle } from '@/lib/utils/handle';
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -97,7 +98,7 @@ function UserCard({ user }: { user: User }) {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div style={{ fontWeight: 600 }}>{user.displayName || user.handle}</div>
|
<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 && (
|
{user.bio && (
|
||||||
<div style={{
|
<div style={{
|
||||||
color: 'var(--foreground-secondary)',
|
color: 'var(--foreground-secondary)',
|
||||||
@@ -148,7 +149,7 @@ function PostCard({ post }: { post: Post }) {
|
|||||||
<Link href={`/@${post.author?.handle}`} className="post-handle">
|
<Link href={`/@${post.author?.handle}`} className="post-handle">
|
||||||
{post.author?.displayName || post.author?.handle}
|
{post.author?.displayName || post.author?.handle}
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
<div className="post-content">{post.content}</div>
|
<div className="post-content">{post.content}</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import AutoTextarea from '@/components/AutoTextarea';
|
|||||||
import { Post, Attachment } from '@/lib/types';
|
import { Post, Attachment } from '@/lib/types';
|
||||||
import { ImageIcon } from 'lucide-react';
|
import { ImageIcon } from 'lucide-react';
|
||||||
import { VideoEmbed } from '@/components/VideoEmbed';
|
import { VideoEmbed } from '@/components/VideoEmbed';
|
||||||
|
import { formatFullHandle } from '@/lib/utils/handle';
|
||||||
|
|
||||||
interface ComposeProps {
|
interface ComposeProps {
|
||||||
onPost: (content: string, mediaIds: string[], linkPreview?: any, replyToId?: string) => void;
|
onPost: (content: string, mediaIds: string[], linkPreview?: any, replyToId?: string) => void;
|
||||||
@@ -121,7 +122,7 @@ export function Compose({ onPost, replyingTo, onCancelReply, placeholder = "What
|
|||||||
{replyingTo && !isReply && (
|
{replyingTo && !isReply && (
|
||||||
<div className="compose-reply-target">
|
<div className="compose-reply-target">
|
||||||
<div className="compose-reply-info">
|
<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>
|
</div>
|
||||||
<button type="button" className="compose-reply-cancel" onClick={onCancelReply}>
|
<button type="button" className="compose-reply-cancel" onClick={onCancelReply}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { HeartIcon, RepeatIcon, MessageIcon, FlagIcon, TrashIcon } from '@/compo
|
|||||||
import { Post } from '@/lib/types';
|
import { Post } from '@/lib/types';
|
||||||
import { useAuth } from '@/lib/contexts/AuthContext';
|
import { useAuth } from '@/lib/contexts/AuthContext';
|
||||||
import { VideoEmbed } from '@/components/VideoEmbed';
|
import { VideoEmbed } from '@/components/VideoEmbed';
|
||||||
|
import { formatFullHandle } from '@/lib/utils/handle';
|
||||||
|
|
||||||
interface PostCardProps {
|
interface PostCardProps {
|
||||||
post: Post;
|
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()}>
|
<Link href={`/${post.author.handle}`} className="post-handle" onClick={(e) => e.stopPropagation()}>
|
||||||
{post.author.displayName || post.author.handle}
|
{post.author.displayName || post.author.handle}
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
|
|
||||||
{post.replyTo && (
|
{post.replyTo && (
|
||||||
<div className="post-reply-to">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Link from 'next/link';
|
|||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useAuth } from '@/lib/contexts/AuthContext';
|
import { useAuth } from '@/lib/contexts/AuthContext';
|
||||||
import { HomeIcon, SearchIcon, BellIcon, UserIcon, ShieldIcon, SynapsisLogo, BookOpenIcon, SettingsIcon } from './Icons';
|
import { HomeIcon, SearchIcon, BellIcon, UserIcon, ShieldIcon, SynapsisLogo, BookOpenIcon, SettingsIcon } from './Icons';
|
||||||
|
import { formatFullHandle } from '@/lib/utils/handle';
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
const { user, isAdmin } = useAuth();
|
const { user, isAdmin } = useAuth();
|
||||||
@@ -71,7 +72,7 @@ export function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 600, fontSize: '14px' }}>{user.displayName}</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>
|
</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