diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 874d2b245e..7058aafda5 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -118,6 +118,7 @@ "date-fns": "^4.1.0", "dompurify": "^3.3.1", "effect": "^3.19.16", + "facehash": "^0.0.7", "json5": "^2.2.3", "lucide-react": "^0.544.0", "motion": "^11.18.2", diff --git a/apps/desktop/src/auth/context.tsx b/apps/desktop/src/auth/context.tsx index 959a6cd89b..28d88df749 100644 --- a/apps/desktop/src/auth/context.tsx +++ b/apps/desktop/src/auth/context.tsx @@ -49,7 +49,7 @@ type AuthTokenHandlers = { type AuthUtils = { getHeaders: () => Record | null; - getAvatarUrl: () => Promise; + getAvatarUrl: () => Promise; }; export type AuthContextType = AuthState & @@ -282,7 +282,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const email = session?.user.email; if (!email) { - return "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect width='100' height='100' fill='%23e0e0e0'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='48' fill='%23666'%3E%3F%3C/text%3E%3C/svg%3E"; + return null; } const address = email.trim().toLowerCase(); @@ -292,7 +292,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const hashArray = Array.from(new Uint8Array(hashBuffer)); const hash = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); - return `https://gravatar.com/avatar/${hash}`; + return `https://gravatar.com/avatar/${hash}?d=404`; }, [session]); const value = useMemo( diff --git a/apps/desktop/src/components/main/body/contacts/details.tsx b/apps/desktop/src/components/main/body/contacts/details.tsx index fece43032e..e8d0067b02 100644 --- a/apps/desktop/src/components/main/body/contacts/details.tsx +++ b/apps/desktop/src/components/main/body/contacts/details.tsx @@ -1,3 +1,4 @@ +import { Facehash } from "facehash"; import { Building2, CircleMinus, @@ -17,7 +18,6 @@ import { import { Textarea } from "@hypr/ui/components/ui/textarea"; import * as main from "../../../../store/tinybase/store/main"; -import { getInitials } from "./shared"; export function DetailsColumn({ selectedHumanId, @@ -211,13 +211,16 @@ export function DetailsColumn({ <>
-
- - {getInitials( - selectedPersonData.name || selectedPersonData.email, - )} - -
+
@@ -264,11 +267,12 @@ export function DetailsColumn({ className="flex items-center justify-between p-2 bg-neutral-50 rounded-md border border-neutral-200" >
-
- - {getInitials(dup.name || dup.email)} - -
+
{dup.name || "Unnamed Contact"} diff --git a/apps/desktop/src/components/main/body/contacts/organization-details.tsx b/apps/desktop/src/components/main/body/contacts/organization-details.tsx index 84c94ea543..65e5a8e5f6 100644 --- a/apps/desktop/src/components/main/body/contacts/organization-details.tsx +++ b/apps/desktop/src/components/main/body/contacts/organization-details.tsx @@ -1,4 +1,5 @@ import { Icon } from "@iconify-icon/react"; +import { Facehash } from "facehash"; import { Building2, Mail } from "lucide-react"; import { commands as openerCommands } from "@hypr/plugin-opener2"; @@ -6,7 +7,6 @@ import { Button } from "@hypr/ui/components/ui/button"; import { Input } from "@hypr/ui/components/ui/input"; import * as main from "../../../../store/tinybase/store/main"; -import { getInitials } from "./shared"; export function OrganizationDetailsColumn({ selectedOrganizationId, @@ -77,14 +77,14 @@ export function OrganizationDetailsColumn({ onClick={() => onPersonClick?.(humanId)} >
-
- - {getInitials( - (human.name as string) || - (human.email as string), - )} - -
+
{human.name || human.email || "Unnamed"} diff --git a/apps/desktop/src/components/main/body/contacts/people.tsx b/apps/desktop/src/components/main/body/contacts/people.tsx index 4646066b95..c117aebc88 100644 --- a/apps/desktop/src/components/main/body/contacts/people.tsx +++ b/apps/desktop/src/components/main/body/contacts/people.tsx @@ -1,3 +1,4 @@ +import { Facehash } from "facehash"; import { CornerDownLeft, Pin } from "lucide-react"; import { Reorder } from "motion/react"; import React, { useCallback, useMemo, useState } from "react"; @@ -5,7 +6,7 @@ import React, { useCallback, useMemo, useState } from "react"; import { cn } from "@hypr/utils"; import * as main from "../../../../store/tinybase/store/main"; -import { ColumnHeader, getInitials, type SortOption } from "./shared"; +import { ColumnHeader, type SortOption } from "./shared"; export function PeopleColumn({ currentOrgId, @@ -251,11 +252,12 @@ function PersonItem({ active ? "border-neutral-500 bg-neutral-100" : "border-transparent", ])} > -
- - {getInitials(personName || personEmail)} - -
+
{personName || personEmail || "Unnamed"} diff --git a/apps/desktop/src/components/main/sidebar/profile/index.tsx b/apps/desktop/src/components/main/sidebar/profile/index.tsx index 5850b3f9e8..b8d1503f88 100644 --- a/apps/desktop/src/components/main/sidebar/profile/index.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/index.tsx @@ -1,4 +1,5 @@ import { useQuery } from "@tanstack/react-query"; +import { Facehash } from "facehash"; import { CalendarIcon, ChevronUpIcon, @@ -10,7 +11,7 @@ import { UsersIcon, } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useResizeObserver } from "usehooks-ts"; import { Kbd } from "@hypr/ui/components/ui/kbd"; @@ -288,6 +289,7 @@ function ProfileButton({ }) { const auth = useAuth(); const name = useMyName(auth?.session?.user.email); + const [imgError, setImgError] = useState(false); const profile = useQuery({ queryKey: ["profile"], @@ -297,6 +299,13 @@ function ProfileButton({ }, }); + const facehashName = useMemo( + () => auth?.session?.user.email || name || "user", + [auth?.session?.user.email, name], + ); + + const showFacehash = !profile.data || imgError; + return (
diff --git a/apps/desktop/src/components/main/sidebar/search/item.tsx b/apps/desktop/src/components/main/sidebar/search/item.tsx index 93729c6776..74e0916cac 100644 --- a/apps/desktop/src/components/main/sidebar/search/item.tsx +++ b/apps/desktop/src/components/main/sidebar/search/item.tsx @@ -1,4 +1,5 @@ import DOMPurify from "dompurify"; +import { Facehash } from "facehash"; import { useCallback, useMemo } from "react"; import { cn } from "@hypr/utils"; @@ -6,7 +7,6 @@ import { cn } from "@hypr/utils"; import { type SearchResult } from "../../../../contexts/search/ui"; import * as main from "../../../../store/tinybase/store/main"; import { type TabInput, useTabs } from "../../../../store/zustand/tabs"; -import { getInitials } from "../../body/contacts/shared"; export function SearchResultItem({ result, @@ -88,15 +88,12 @@ function HumanSearchResultItem({ isSelected && "bg-neutral-100", ])} > -
- - {getInitials(result.title)} - -
+
= 10.17.0'} hasBin: true + facehash@0.0.7: + resolution: {integrity: sha512-P4fw6z5DIGMbjtqEaOw7fYvYpQetSOSJOfqy3xuET7cDUI6f9CKlSX0UZIYNrtsPpCoz3LoPP5E8bNbpZBP30A==} + peerDependencies: + '@types/react': '' + next: '>=15' + react: '>=18 <20' + react-dom: '>=18 <20' + peerDependenciesMeta: + '@types/react': + optional: true + next: + optional: true + fast-check@3.23.2: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} @@ -26862,6 +26878,13 @@ snapshots: transitivePeerDependencies: - supports-color + facehash@0.0.7(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.13 + fast-check@3.23.2: dependencies: pure-rand: 6.1.0