From 3ca26316b0c2a0b6d745cc83741fba67eeb5325d Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Wed, 1 Apr 2026 16:03:34 -0700 Subject: [PATCH 1/5] feat(web): replace placeholder avatars with minidenticon-based UserAvatar component Adds the minidenticons library and a new UserAvatar component that generates deterministic avatar icons from email addresses. Replaces all placeholder avatar usage across chat, settings, and redeem pages with this unified component. Co-Authored-By: Claude Opus 4.6 --- packages/web/package.json | 1 + .../[domain]/chat/[id]/opengraph-image.tsx | 6 +++- .../shareChatPopover/ee/invitePanel.tsx | 23 ++++-------- .../shareChatPopover/shareSettings.tsx | 31 ++++++---------- .../components/meControlDropdownMenu.tsx | 27 ++++++-------- .../members/components/invitesList.tsx | 7 ++-- .../members/components/membersList.tsx | 10 +++--- .../members/components/requestsList.tsx | 7 ++-- .../redeem/components/acceptInviteCard.tsx | 19 +++++----- .../redeem/components/inviteNotFoundCard.tsx | 11 +++--- packages/web/src/components/userAvatar.tsx | 35 +++++++++++++++++++ .../components/chatThread/messageAvatar.tsx | 23 +++++++----- yarn.lock | 8 +++++ 13 files changed, 118 insertions(+), 90 deletions(-) create mode 100644 packages/web/src/components/userAvatar.tsx diff --git a/packages/web/package.json b/packages/web/package.json index ceee7d2a8..07ce4adff 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -151,6 +151,7 @@ "linguist-languages": "^9.3.1", "lucide-react": "^0.517.0", "micromatch": "^4.0.8", + "minidenticons": "^4.2.1", "next": "16.1.6", "next-auth": "^5.0.0-beta.30", "next-navigation-guard": "^0.2.0", diff --git a/packages/web/src/app/[domain]/chat/[id]/opengraph-image.tsx b/packages/web/src/app/[domain]/chat/[id]/opengraph-image.tsx index 4d3176416..cfde021db 100644 --- a/packages/web/src/app/[domain]/chat/[id]/opengraph-image.tsx +++ b/packages/web/src/app/[domain]/chat/[id]/opengraph-image.tsx @@ -4,6 +4,7 @@ import { prisma } from '@/prisma'; import { getOrgFromDomain } from '@/data/org'; import { ChatVisibility } from '@sourcebot/db'; import { env } from "@sourcebot/shared"; +import { minidenticon } from 'minidenticons'; export const runtime = 'nodejs'; export const alt = 'Sourcebot Chat'; @@ -37,6 +38,7 @@ export default async function Image({ params }: ImageProps) { createdBy: { select: { name: true, + email: true, image: true, }, }, @@ -53,7 +55,9 @@ export default async function Image({ params }: ImageProps) { const chatName = rawChatName.length > MAX_CHAT_NAME_LENGTH ? rawChatName.substring(0, MAX_CHAT_NAME_LENGTH).trim() + '...' : rawChatName; - const creatorImage = chat.createdBy?.image; + const creatorEmail = chat.createdBy?.email; + const creatorImage = chat.createdBy?.image + ?? (creatorEmail ? 'data:image/svg+xml;utf8,' + encodeURIComponent(minidenticon(creatorEmail, 50, 50)) : undefined); return new ImageResponse( ( diff --git a/packages/web/src/app/[domain]/chat/components/shareChatPopover/ee/invitePanel.tsx b/packages/web/src/app/[domain]/chat/components/shareChatPopover/ee/invitePanel.tsx index 8e9db0888..146a209b0 100644 --- a/packages/web/src/app/[domain]/chat/components/shareChatPopover/ee/invitePanel.tsx +++ b/packages/web/src/app/[domain]/chat/components/shareChatPopover/ee/invitePanel.tsx @@ -3,13 +3,12 @@ import { searchChatShareableMembers } from "@/app/api/(client)/client"; import { SearchChatShareableMembersResponse } from "@/app/api/(server)/ee/chat/[chatId]/searchMembers/route"; import { SessionUser } from "@/auth"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { LoadingButton } from "@/components/ui/loading-button"; import { Separator } from "@/components/ui/separator"; import { unwrapServiceError } from "@/lib/utils"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; import { useQuery } from "@tanstack/react-query"; import { useDebounce } from "@uidotdev/usehooks"; import { ChevronLeft, Circle, CircleCheck, Loader2, X } from "lucide-react"; @@ -33,17 +32,6 @@ export const InvitePanel = ({ const resultsRef = useRef(null); const inputRef = useRef(null); - const getInitials = (name?: string, email?: string) => { - if (name) { - return name.split(' ').map((n) => n[0]).join('').toUpperCase().slice(0, 2); - } - if (email) { - return email[0].toUpperCase(); - } - return '?'; - }; - - const debouncedSearchQuery = useDebounce(searchQuery, 100); const { data: searchResults, isPending, isError } = useQuery({ @@ -157,10 +145,11 @@ export const InvitePanel = ({ ) : ( )} - - - {getInitials(user.name ?? undefined, user.email ?? undefined)} - +
{user.name || user.email} {user.name && ( diff --git a/packages/web/src/app/[domain]/chat/components/shareChatPopover/shareSettings.tsx b/packages/web/src/app/[domain]/chat/components/shareChatPopover/shareSettings.tsx index bf1335f82..9dd0a6668 100644 --- a/packages/web/src/app/[domain]/chat/components/shareChatPopover/shareSettings.tsx +++ b/packages/web/src/app/[domain]/chat/components/shareChatPopover/shareSettings.tsx @@ -2,7 +2,6 @@ import { SessionUser } from "@/auth"; import { useToast } from "@/components/hooks/use-toast"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Select, @@ -13,7 +12,7 @@ import { } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; import { ChatVisibility } from "@sourcebot/db"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Info, Link2Icon, Loader2, Lock, X } from "lucide-react"; @@ -69,16 +68,6 @@ export const ShareSettings = ({ } }, [chatId, visibility, toast]); - const getInitials = (name?: string | null, email?: string | null) => { - if (name) { - return name.split(' ').map((n) => n[0]).join('').toUpperCase().slice(0, 2); - } - if (email) { - return email[0].toUpperCase(); - } - return '?'; - }; - return (

Share

@@ -113,10 +102,11 @@ export const ShareSettings = ({ {currentUser && (
- - - {getInitials(currentUser.name, currentUser.email)} - +
{currentUser.name || currentUser.email} @@ -134,10 +124,11 @@ export const ShareSettings = ({ {sharedWithUsers.map((user) => (
- - - {getInitials(user.name, user.email)} - +
{user.name || user.email} {user.name && ( diff --git a/packages/web/src/app/[domain]/components/meControlDropdownMenu.tsx b/packages/web/src/app/[domain]/components/meControlDropdownMenu.tsx index ae3263cb5..d9e4ee782 100644 --- a/packages/web/src/app/[domain]/components/meControlDropdownMenu.tsx +++ b/packages/web/src/app/[domain]/components/meControlDropdownMenu.tsx @@ -13,13 +13,12 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { cn } from "@/lib/utils" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { signOut } from "next-auth/react" import posthog from "posthog-js"; import { useDomain } from "@/hooks/useDomain"; import { Session } from "next-auth"; import { AppearanceDropdownMenuGroup } from "./appearanceDropdownMenuGroup"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; interface MeControlDropdownMenuProps { menuButtonClassName?: string; @@ -35,24 +34,20 @@ export const MeControlDropdownMenu = ({ return ( - - - - {session.user.name && session.user.name.length > 0 ? session.user.name[0].toUpperCase() : 'U'} - - +
- - - - {session.user.name && session.user.name.length > 0 ? session.user.name[0].toUpperCase() : 'U'} - - +

{session.user.name ?? "User"}

{session.user.email && ( diff --git a/packages/web/src/app/[domain]/settings/members/components/invitesList.tsx b/packages/web/src/app/[domain]/settings/members/components/invitesList.tsx index 66d096652..935893abd 100644 --- a/packages/web/src/app/[domain]/settings/members/components/invitesList.tsx +++ b/packages/web/src/app/[domain]/settings/members/components/invitesList.tsx @@ -3,13 +3,12 @@ import { OrgRole } from "@sourcebot/db"; import { useToast } from "@/components/hooks/use-toast"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; -import { Avatar, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { createPathWithQueryParams, isServiceError } from "@/lib/utils"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; import { Copy, MoreVertical, Search } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { cancelInvite } from "@/actions"; @@ -109,9 +108,7 @@ export const InvitesList = ({ invites, currentUserRole }: InviteListProps) => { filteredInvites.map((invite) => (
- - - +
{invite.email}
diff --git a/packages/web/src/app/[domain]/settings/members/components/membersList.tsx b/packages/web/src/app/[domain]/settings/members/components/membersList.tsx index 8b197bfc9..63810ce73 100644 --- a/packages/web/src/app/[domain]/settings/members/components/membersList.tsx +++ b/packages/web/src/app/[domain]/settings/members/components/membersList.tsx @@ -3,12 +3,11 @@ import { Input } from "@/components/ui/input"; import { Search, MoreVertical } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Avatar, AvatarImage } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; import { useCallback, useMemo, useState } from "react"; import { OrgRole } from "@prisma/client"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { promoteToOwner, demoteToMember } from "@/ee/features/userManagement/actions"; import { leaveOrg, removeMemberFromOrg } from "@/features/userManagement/actions"; @@ -200,9 +199,10 @@ export const MembersList = ({ members, currentUserId, currentUserRole, orgName, filteredMembers.map((member) => (
- - - +
{member.name}
{member.email}
diff --git a/packages/web/src/app/[domain]/settings/members/components/requestsList.tsx b/packages/web/src/app/[domain]/settings/members/components/requestsList.tsx index f3ae55f31..be6507aea 100644 --- a/packages/web/src/app/[domain]/settings/members/components/requestsList.tsx +++ b/packages/web/src/app/[domain]/settings/members/components/requestsList.tsx @@ -3,12 +3,11 @@ import { OrgRole } from "@sourcebot/db"; import { useToast } from "@/components/hooks/use-toast"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; -import { Avatar, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { isServiceError } from "@/lib/utils"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; import { CheckCircle, Search, XCircle } from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { approveAccountRequest, rejectAccountRequest } from "@/actions"; @@ -132,9 +131,7 @@ export const RequestsList = ({ requests, currentUserRole }: RequestsListProps) = filteredRequests.map((request) => (
- - - +
{request.name || request.email}
{request.email}
diff --git a/packages/web/src/app/redeem/components/acceptInviteCard.tsx b/packages/web/src/app/redeem/components/acceptInviteCard.tsx index d8f32e997..e7cd5243f 100644 --- a/packages/web/src/app/redeem/components/acceptInviteCard.tsx +++ b/packages/web/src/app/redeem/components/acceptInviteCard.tsx @@ -3,8 +3,7 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { SourcebotLogo } from "@/app/components/sourcebotLogo"; import Link from "next/link"; -import { Avatar, AvatarImage } from "@/components/ui/avatar"; -import placeholderAvatar from "@/public/placeholder_avatar.png"; +import { UserAvatar } from "@/components/userAvatar"; import { ArrowRight, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useCallback, useState } from "react"; @@ -74,13 +73,17 @@ export const AcceptInviteCard = ({ inviteId, orgName, orgDomain, orgImageUrl, ho invited you to join the {orgName} organization.

- - - + - - - +