diff --git a/src/clients.ts b/src/clients.ts index 9e1e6e14..94ffd7a6 100644 --- a/src/clients.ts +++ b/src/clients.ts @@ -20,9 +20,13 @@ export const getClientColors = (clientId: string, appState: AppState) => { }; }; -export const getClientInitials = (userName?: string | null) => { - if (!userName?.trim()) { - return "?"; - } - return userName.trim()[0].toUpperCase(); +/** + * returns first char, capitalized + */ +export const getNameInitial = (name?: string | null) => { + // first char can be a surrogate pair, hence using codePointAt + const firstCodePoint = name?.trim()?.codePointAt(0); + return ( + firstCodePoint ? String.fromCodePoint(firstCodePoint) : "?" + ).toUpperCase(); }; diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 57a7eec2..20dc7b9f 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,7 +1,7 @@ import "./Avatar.scss"; import React, { useState } from "react"; -import { getClientInitials } from "../clients"; +import { getNameInitial } from "../clients"; type AvatarProps = { onClick: (e: React.MouseEvent) => void; @@ -12,7 +12,7 @@ type AvatarProps = { }; export const Avatar = ({ color, onClick, name, src }: AvatarProps) => { - const shortName = getClientInitials(name); + const shortName = getNameInitial(name); const [error, setError] = useState(false); const loadImg = !error && src; const style = loadImg ? undefined : { background: color }; diff --git a/src/tests/clients.test.ts b/src/tests/clients.test.ts index a6a6901b..e78cf186 100644 --- a/src/tests/clients.test.ts +++ b/src/tests/clients.test.ts @@ -1,44 +1,39 @@ -import { getClientInitials } from "../clients"; +import { getNameInitial } from "../clients"; describe("getClientInitials", () => { it("returns substring if one name provided", () => { - const result = getClientInitials("Alan"); - expect(result).toBe("A"); + expect(getNameInitial("Alan")).toBe("A"); }); it("returns initials", () => { - const result = getClientInitials("John Doe"); - expect(result).toBe("J"); + expect(getNameInitial("John Doe")).toBe("J"); }); it("returns correct initials if many names provided", () => { - const result = getClientInitials("John Alan Doe"); - expect(result).toBe("J"); + expect(getNameInitial("John Alan Doe")).toBe("J"); }); it("returns single initial if 1 letter provided", () => { - const result = getClientInitials("z"); - expect(result).toBe("Z"); + expect(getNameInitial("z")).toBe("Z"); }); it("trims trailing whitespace", () => { - const result = getClientInitials(" q "); - expect(result).toBe("Q"); + expect(getNameInitial(" q ")).toBe("Q"); }); it('returns "?" if falsey value provided', () => { - let result = getClientInitials(""); - expect(result).toBe("?"); - - result = getClientInitials(undefined); - expect(result).toBe("?"); - - result = getClientInitials(null); - expect(result).toBe("?"); + expect(getNameInitial("")).toBe("?"); + expect(getNameInitial(undefined)).toBe("?"); + expect(getNameInitial(null)).toBe("?"); }); it('returns "?" when value is blank', () => { - const result = getClientInitials(" "); - expect(result).toBe("?"); + expect(getNameInitial(" ")).toBe("?"); + }); + + it("works with multibyte strings", () => { + expect(getNameInitial("😀")).toBe("😀"); + // but doesn't work with emoji ZWJ sequences + expect(getNameInitial("👨‍👩‍👦")).toBe("👨"); }); });