From a7625c6b5eec8e81b4e08e0f1709d88127c4e6a7 Mon Sep 17 00:00:00 2001 From: eti Date: Fri, 25 Sep 2026 18:52:51 +0200 Subject: [PATCH] web/components: link @handle mentions in profile bios with ProfilePopover Signed-off-by: eti --- web/src/lib/components/comment/Comment.svelte | 4 +-- .../lib/components/profile/AvatarList.svelte | 2 +- .../lib/components/profile/ProfileBio.svelte | 28 ++++++++++++++++++ .../profile/ProfilePopover.stories.svelte | 29 ++++++++++++------- .../components/profile/ProfilePopover.svelte | 6 ++-- .../profile/UserProfileChrome.svelte | 8 +++-- web/src/lib/components/profile/bio.test.ts | 24 +++++++++++++++ web/src/lib/components/profile/bio.ts | 17 +++++++++++ .../organization/OrganizationChrome.svelte | 6 +++- .../repo/pulls/PullCommitRow.svelte | 2 +- .../repo/pulls/PullReviewComment.svelte | 2 +- .../repo/tickets/TicketInfoBar.svelte | 2 +- .../timeline/TimelineActorHeader.svelte | 2 +- 13 files changed, 108 insertions(+), 24 deletions(-) create mode 100644 web/src/lib/components/profile/ProfileBio.svelte create mode 100644 web/src/lib/components/profile/bio.test.ts create mode 100644 web/src/lib/components/profile/bio.ts diff --git a/web/src/lib/components/comment/Comment.svelte b/web/src/lib/components/comment/Comment.svelte index 523b542ae..94a46e5e5 100644 --- a/web/src/lib/components/comment/Comment.svelte +++ b/web/src/lib/components/comment/Comment.svelte @@ -51,12 +51,12 @@ class={`grid grid-cols-[auto_minmax(0,1fr)] gap-x-1.5 ${variant === "top" ? "border-b border-border-default bg-background-default p-md pb-lg" : "pt-sm pr-md pb-md"}`} >
- +
- + diff --git a/web/src/lib/components/profile/AvatarList.svelte b/web/src/lib/components/profile/AvatarList.svelte index c3231bf6d..e3a526ae6 100644 --- a/web/src/lib/components/profile/AvatarList.svelte +++ b/web/src/lib/components/profile/AvatarList.svelte @@ -39,7 +39,7 @@
{#if popover} - + {@render avatarLink(person)} {:else} diff --git a/web/src/lib/components/profile/ProfileBio.svelte b/web/src/lib/components/profile/ProfileBio.svelte new file mode 100644 index 000000000..deb9b1d7f --- /dev/null +++ b/web/src/lib/components/profile/ProfileBio.svelte @@ -0,0 +1,28 @@ + + +
+ {#each segments as segment, i (i)} + {#if segment.kind === "mention"} + + @{segment.handle} + + {:else} + {segment.text} + {/if} + {/each} +
diff --git a/web/src/lib/components/profile/ProfilePopover.stories.svelte b/web/src/lib/components/profile/ProfilePopover.stories.svelte index c07510b7c..bb9bf6ff8 100644 --- a/web/src/lib/components/profile/ProfilePopover.stories.svelte +++ b/web/src/lib/components/profile/ProfilePopover.stories.svelte @@ -51,7 +51,7 @@ component: ProfilePopover, tags: ["autodocs"], args: { - did: DID + identifier: DID } }); @@ -60,8 +60,11 @@ {#snippet template(args)}
- - + +
{/snippet} @@ -71,7 +74,10 @@ {#snippet template(args)}
- + {HANDLE}
@@ -82,7 +88,7 @@ {#snippet template()}
- +
@@ -93,8 +99,8 @@ {#snippet template(args)}
- - + +
{/snippet} @@ -104,8 +110,8 @@ {#snippet template(args)}
- - + +
{/snippet} @@ -115,7 +121,10 @@ {#snippet template()}
- +
diff --git a/web/src/lib/components/profile/ProfilePopover.svelte b/web/src/lib/components/profile/ProfilePopover.svelte index 2e80b3b2b..75c65efcc 100644 --- a/web/src/lib/components/profile/ProfilePopover.svelte +++ b/web/src/lib/components/profile/ProfilePopover.svelte @@ -16,7 +16,7 @@ import type { DirectVouch, ProfileCardData, NetworkVouch } from "$lib/components/profile/types"; interface Props { - did: string; + identifier: string; disabled?: boolean; openDelay?: number; class?: string; @@ -25,7 +25,7 @@ } let { - did, + identifier, disabled = false, openDelay = 200, class: className, @@ -67,7 +67,7 @@ try { const ctx = createBobbinClient({ serviceUrl: bobbinUrl }); - card = await fetchProfileCard(ctx, { identifier: did, viewerDid }); + card = await fetchProfileCard(ctx, { identifier, viewerDid }); status = "loaded"; } catch { status = "error"; diff --git a/web/src/lib/components/profile/UserProfileChrome.svelte b/web/src/lib/components/profile/UserProfileChrome.svelte index e9325da09..738c60d28 100644 --- a/web/src/lib/components/profile/UserProfileChrome.svelte +++ b/web/src/lib/components/profile/UserProfileChrome.svelte @@ -6,6 +6,7 @@ import FollowButton from "$lib/components/profile/FollowButton.svelte"; import NetworkVouches from "$lib/components/profile/NetworkVouches.svelte"; import ProfileEditForm from "$lib/components/profile/ProfileEditForm.svelte"; + import ProfileBio from "$lib/components/profile/ProfileBio.svelte"; import ProfileMoreMenu from "$lib/components/profile/ProfileMoreMenu.svelte"; import ProfileSocialLinks from "$lib/components/profile/ProfileSocialLinks.svelte"; import ProfileTabs from "$lib/components/profile/ProfileTabs.svelte"; @@ -117,9 +118,10 @@
{:else} {#if shown?.description} -

- {shown.description} -

+ {/if} {#if shown} { + it("splits mentions out of surrounding text", () => { + expect(bioSegments("working on @tangled.org with @Alice.bsky.social.")).toEqual([ + { kind: "text", text: "working on " }, + { kind: "mention", handle: "tangled.org" }, + { kind: "text", text: " with " }, + { kind: "mention", handle: "alice.bsky.social" }, + { kind: "text", text: "." } + ]); + }); + + it("leaves emails and dotless names as text", () => { + expect(bioSegments("mail me@example.com or @nobody")).toEqual([ + { kind: "text", text: "mail me@example.com or @nobody" } + ]); + }); + + it("keeps a mention at the very start", () => { + expect(bioSegments("@eti.tngl.sh")).toEqual([{ kind: "mention", handle: "eti.tngl.sh" }]); + }); +}); diff --git a/web/src/lib/components/profile/bio.ts b/web/src/lib/components/profile/bio.ts new file mode 100644 index 000000000..c970886fd --- /dev/null +++ b/web/src/lib/components/profile/bio.ts @@ -0,0 +1,17 @@ +export type BioSegment = { kind: "text"; text: string } | { kind: "mention"; handle: string }; + +const MENTION = + /(? { + const segments: BioSegment[] = []; + let last = 0; + for (const match of text.matchAll(MENTION)) { + if (match.index > last) + segments.push({ kind: "text", text: text.slice(last, match.index) }); + segments.push({ kind: "mention", handle: match[1].toLowerCase() }); + last = match.index + match[0].length; + } + if (last < text.length) segments.push({ kind: "text", text: text.slice(last) }); + return segments; +}; diff --git a/web/src/lib/components/profile/organization/OrganizationChrome.svelte b/web/src/lib/components/profile/organization/OrganizationChrome.svelte index f2be45376..b26608f69 100644 --- a/web/src/lib/components/profile/organization/OrganizationChrome.svelte +++ b/web/src/lib/components/profile/organization/OrganizationChrome.svelte @@ -8,6 +8,7 @@ import type { ProfileRecord } from "$lib/api/records"; import { getAuth } from "$lib/auth.svelte"; import FollowButton from "$lib/components/profile/FollowButton.svelte"; + import ProfileBio from "$lib/components/profile/ProfileBio.svelte"; import ProfileMoreMenu from "$lib/components/profile/ProfileMoreMenu.svelte"; import ProfileSocialLinks from "$lib/components/profile/ProfileSocialLinks.svelte"; import ProfileTabs from "$lib/components/profile/ProfileTabs.svelte"; @@ -104,7 +105,10 @@ {#if profile.description} -

{profile.description}

+ {/if}
diff --git a/web/src/lib/components/repo/pulls/PullCommitRow.svelte b/web/src/lib/components/repo/pulls/PullCommitRow.svelte index 875e208b7..bea1a86ec 100644 --- a/web/src/lib/components/repo/pulls/PullCommitRow.svelte +++ b/web/src/lib/components/repo/pulls/PullCommitRow.svelte @@ -102,7 +102,7 @@ > {#if showAvatar} {#if commit.authorHandle && commit.authorDid} - +
- + + {:else} diff --git a/web/src/lib/components/timeline/TimelineActorHeader.svelte b/web/src/lib/components/timeline/TimelineActorHeader.svelte index 903b59128..230d5ce3f 100644 --- a/web/src/lib/components/timeline/TimelineActorHeader.svelte +++ b/web/src/lib/components/timeline/TimelineActorHeader.svelte @@ -19,7 +19,7 @@ } = $props(); - + {verb} -- 2.51.2