diff --git a/src/app/bots/page.tsx b/src/app/bots/page.tsx index 9042ef5..0def9f4 100644 --- a/src/app/bots/page.tsx +++ b/src/app/bots/page.tsx @@ -13,6 +13,7 @@ import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { ArrowLeftIcon } from '@/components/Icons'; import { Bot, Plus, Sparkles } from 'lucide-react'; +import { AvatarImage } from '@/components/AvatarImage'; interface BotData { id: string; @@ -126,11 +127,7 @@ export default function BotsPage() { fontSize: '18px', }} > - {bot.avatarUrl ? ( - {bot.name} - ) : ( - bot.name.charAt(0).toUpperCase() - )} +
diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index a98b741..618d815 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -7,6 +7,7 @@ import { ArrowLeft, Send, Loader2, MessageCircle, Search, Plus, Trash2, MoreVert import Link from 'next/link'; import { getProfilePath, useFormattedHandle } from '@/lib/utils/handle'; import { useRouter, useSearchParams } from 'next/navigation'; +import { AvatarImage } from '@/components/AvatarImage'; interface Conversation { id: string; @@ -386,11 +387,11 @@ export default function ChatPage() {
- {selectedConversation.participant2.avatarUrl ? ( - - ) : ( - selectedConversation.participant2.displayName[0] || '?' - )} +
- {msg.isSentByMe ? ( - user.avatarUrl ? : user.displayName[0] - ) : ( - msg.senderAvatarUrl ? : msg.senderDisplayName?.[0] - )} +
@@ -595,7 +596,11 @@ export default function ChatPage() { style={{ cursor: 'pointer', display: 'flex', alignItems: 'flex-start', gap: '12px' }} >
- {conv.participant2.avatarUrl ? : conv.participant2.displayName?.[0] || '?'} +
diff --git a/src/app/explore/page.tsx b/src/app/explore/page.tsx index 7399239..c571a52 100644 --- a/src/app/explore/page.tsx +++ b/src/app/explore/page.tsx @@ -9,6 +9,7 @@ import { useFormattedHandle } from '@/lib/utils/handle'; import { Bot, Network, Server, EyeOff } from 'lucide-react'; import { useAuth } from '@/lib/contexts/AuthContext'; import { signedAPI } from '@/lib/api/signed-fetch'; +import { AvatarImage } from '@/components/AvatarImage'; interface User { id: string; @@ -26,11 +27,7 @@ function UserCard({ user }: { user: User }) { return (
- {user.avatarUrl ? ( - {user.displayName} - ) : ( - user.displayName?.charAt(0).toUpperCase() || user.handle.charAt(0).toUpperCase() - )} +
diff --git a/src/app/notifications/page.tsx b/src/app/notifications/page.tsx index 7ed071d..2583d82 100644 --- a/src/app/notifications/page.tsx +++ b/src/app/notifications/page.tsx @@ -5,6 +5,7 @@ import { BellIcon } from '@/components/Icons'; import Link from 'next/link'; import { useAuth } from '@/lib/contexts/AuthContext'; import { getProfilePath } from '@/lib/utils/handle'; +import { AvatarImage } from '@/components/AvatarImage'; interface NotificationActor { id: string; @@ -218,32 +219,9 @@ function NotificationItem({ }} > - {actor?.avatarUrl ? ( - {actor.displayName - ) : ( -
- {(actor?.displayName || actor?.handle || '?')[0].toUpperCase()} -
- )} +
+ +
diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index b9be09f..f1e3714 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -9,6 +9,7 @@ import { Post } from '@/lib/types'; import { Bot } from 'lucide-react'; import { useAuth } from '@/lib/contexts/AuthContext'; import { signedAPI } from '@/lib/api/signed-fetch'; +import { AvatarImage } from '@/components/AvatarImage'; interface User { id: string; @@ -82,11 +83,7 @@ function UserCard({ user }: { user: User }) { className="hover-bg" >
- {user.avatarUrl ? ( - {user.displayName} - ) : ( - (user.displayName || user.handle).charAt(0).toUpperCase() - )} +
diff --git a/src/app/settings/moderation/page.tsx b/src/app/settings/moderation/page.tsx index bad2f1a..590844c 100644 --- a/src/app/settings/moderation/page.tsx +++ b/src/app/settings/moderation/page.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import { getProfilePath } from '@/lib/utils/handle'; import { ArrowLeftIcon } from '@/components/Icons'; import { UserX, Globe, Trash2 } from 'lucide-react'; +import { AvatarImage } from '@/components/AvatarImage'; interface BlockedUser { id: string; @@ -189,16 +190,9 @@ export default function ModerationSettingsPage() { textDecoration: 'none', }} > - +
+ +
{user.displayName || user.handle} diff --git a/src/app/u/[handle]/page.tsx b/src/app/u/[handle]/page.tsx index 0f693f9..8e8e271 100644 --- a/src/app/u/[handle]/page.tsx +++ b/src/app/u/[handle]/page.tsx @@ -13,6 +13,7 @@ import { useFormattedHandle } from '@/lib/utils/handle'; import { Bot } from 'lucide-react'; import { useAuth } from '@/lib/contexts/AuthContext'; import { signedAPI } from '@/lib/api/signed-fetch'; +import { AvatarImage } from '@/components/AvatarImage'; interface BotOwner { id: string; @@ -41,11 +42,7 @@ function UserRow({ user }: { user: UserSummary }) { return (
- {user.avatarUrl ? ( - {user.displayName - ) : ( - (user.displayName || user.handle).charAt(0).toUpperCase() - )} +
@@ -568,11 +565,7 @@ export default function ProfilePage() { position: 'relative', }} > - {(isEditing ? profileForm.avatarUrl : user.avatarUrl) ? ( - {user.displayName - ) : ( - (user.displayName || user.handle).charAt(0).toUpperCase() - )} +
diff --git a/src/components/AvatarImage.test.ts b/src/components/AvatarImage.test.ts new file mode 100644 index 0000000..1c98a4e --- /dev/null +++ b/src/components/AvatarImage.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest'; +import { getDiceBearAvatarUrl } from './AvatarImage'; + +describe('getDiceBearAvatarUrl', () => { + it('uses the site-wide bottts-neutral style and a stable encoded handle seed', () => { + expect(getDiceBearAvatarUrl('cyph3r@node.example')).toBe( + 'https://api.dicebear.com/9.x/bottts-neutral/svg?seed=cyph3r%40node.example', + ); + }); +}); diff --git a/src/components/AvatarImage.tsx b/src/components/AvatarImage.tsx new file mode 100644 index 0000000..accca3d --- /dev/null +++ b/src/components/AvatarImage.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { useState, type ImgHTMLAttributes } from 'react'; + +export function getDiceBearAvatarUrl(seed: string): string { + return `https://api.dicebear.com/9.x/bottts-neutral/svg?seed=${encodeURIComponent(seed)}`; +} + +interface AvatarImageProps extends Omit, 'src'> { + avatarUrl?: string | null; + seed: string; +} + +export function AvatarImage({ avatarUrl, seed, alt = '', onError, ...props }: AvatarImageProps) { + const [failedAvatarUrl, setFailedAvatarUrl] = useState(null); + const customAvatar = avatarUrl?.trim(); + const src = customAvatar && failedAvatarUrl !== customAvatar ? customAvatar : getDiceBearAvatarUrl(seed); + + return ( + {alt} { + onError?.(event); + if (customAvatar && failedAvatarUrl !== customAvatar) setFailedAvatarUrl(customAvatar); + }} + /> + ); +} diff --git a/src/components/PostCard.tsx b/src/components/PostCard.tsx index d57ea23..38dc5fc 100644 --- a/src/components/PostCard.tsx +++ b/src/components/PostCard.tsx @@ -14,6 +14,7 @@ import { useFormattedHandle } from '@/lib/utils/handle'; import { useDomain } from '@/lib/contexts/ConfigContext'; import { signedAPI } from '@/lib/api/signed-fetch'; import type { LinkPreviewData } from '@/lib/media/linkPreview'; +import { AvatarImage } from '@/components/AvatarImage'; // Component for link preview image that hides on error function LinkPreviewImage({ src, alt }: { src: string; alt: string }) { @@ -638,11 +639,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide,
e.stopPropagation()}>
- {post.author.avatarUrl ? ( - {post.author.displayName - ) : ( - post.author.displayName?.charAt(0).toUpperCase() || post.author.handle.charAt(0).toUpperCase() - )} +
@@ -720,11 +717,7 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide,
e.stopPropagation()}>
- {post.author.avatarUrl ? ( - {post.author.displayName} - ) : ( - post.author.displayName?.charAt(0).toUpperCase() || post.author.handle.charAt(0).toUpperCase() - )} +
diff --git a/src/components/RightSidebar.tsx b/src/components/RightSidebar.tsx index ebc29fe..7b441a6 100644 --- a/src/components/RightSidebar.tsx +++ b/src/components/RightSidebar.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; +import { AvatarImage } from './AvatarImage'; interface Admin { handle: string; @@ -136,11 +137,7 @@ export function RightSidebar() { style={{ display: 'flex', alignItems: 'center', gap: '10px', textDecoration: 'none', color: 'inherit' }} >
- {admin.avatarUrl ? ( - {admin.displayName - ) : ( - (admin.displayName?.charAt(0) || admin.handle.charAt(0)).toUpperCase() - )} +
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index a4faae1..0348b79 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -10,6 +10,7 @@ import { HomeIcon, SearchIcon, BellIcon, UserIcon, ShieldIcon, SettingsIcon, Bot import { useFormattedHandle } from '@/lib/utils/handle'; import { Check, ChevronDown, LogOut, Plus, Settings2 } from 'lucide-react'; import { AuthScreen } from '@/app/login/page'; +import { AvatarImage } from './AvatarImage'; // import { IdentityUnlockPrompt } from './IdentityUnlockPrompt'; // Moved to LayoutWrapper function shortHandle(handle: string) { @@ -327,11 +328,7 @@ export function Sidebar() { }} >
- {account.avatarUrl ? ( - {account.displayName - ) : ( - (account.displayName?.charAt(0) || account.handle.charAt(0)).toUpperCase() - )} +
@@ -418,11 +415,7 @@ export function Sidebar() { }} >
- {user.avatarUrl ? ( - {user.displayName} - ) : ( - (user.displayName?.charAt(0) || user.handle.charAt(0)).toUpperCase() - )} +
{user.displayName}