From 3f691d5190675f15aa218d01be2bee496beceb8e Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Wed, 15 Apr 2026 10:36:28 -0500 Subject: [PATCH] feat: external link in browser * better moderation/labeler rendering (show names & handles with did) --- CHANGELOG.md | 4 ++++ src/components/feeds/embeds/ExternalEmbed.tsx | 20 ++++++++++++++++- .../feeds/tests/ExternalEmbed.test.tsx | 22 +++++++++++++++++++ .../settings/SettingsModeration.tsx | 12 +++++++++- .../tests/SettingsModeration.test.tsx | 11 ++++++++++ 5 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/components/feeds/tests/ExternalEmbed.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 05b8ba8..05dee9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## v0.1.0 - Unreleased +### 2025-04-15 + +- Open external links in your default browser + ### 2025-04-12 - Added labels to posts & profiles diff --git a/src/components/feeds/embeds/ExternalEmbed.tsx b/src/components/feeds/embeds/ExternalEmbed.tsx index 2f013f4..2195d35 100644 --- a/src/components/feeds/embeds/ExternalEmbed.tsx +++ b/src/components/feeds/embeds/ExternalEmbed.tsx @@ -1,13 +1,31 @@ +import { normalizeError } from "$/lib/utils/text"; +import * as logger from "@tauri-apps/plugin-log"; +import { openUrl } from "@tauri-apps/plugin-opener"; import { Show } from "solid-js"; export function ExternalEmbed(props: { description?: string; thumb?: string; title?: string; uri?: string }) { + function handleClick(event: MouseEvent) { + event.stopPropagation(); + + const uri = props.uri?.trim(); + if (!uri) { + event.preventDefault(); + return; + } + + event.preventDefault(); + void openUrl(uri).catch((error) => { + logger.warn("failed to open external embed URL", { keyValues: { error: normalizeError(error), uri } }); + }); + } + return ( event.stopPropagation()}> + onClick={handleClick}> {(thumb) => } diff --git a/src/components/feeds/tests/ExternalEmbed.test.tsx b/src/components/feeds/tests/ExternalEmbed.test.tsx new file mode 100644 index 0000000..ab960cc --- /dev/null +++ b/src/components/feeds/tests/ExternalEmbed.test.tsx @@ -0,0 +1,22 @@ +import { fireEvent, render, screen, waitFor } from "@solidjs/testing-library"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ExternalEmbed } from "../embeds/ExternalEmbed"; + +const openUrlMock = vi.hoisted(() => vi.fn()); + +vi.mock("@tauri-apps/plugin-opener", () => ({ openUrl: openUrlMock })); + +describe("ExternalEmbed", () => { + beforeEach(() => { + vi.resetAllMocks(); + openUrlMock.mockResolvedValue(void 0); + }); + + it("opens embed links with the system browser via the opener plugin", async () => { + render(() => ); + + fireEvent.click(screen.getByRole("link", { name: /external article/i })); + + await waitFor(() => expect(openUrlMock).toHaveBeenCalledWith("https://example.com/article")); + }); +}); diff --git a/src/components/settings/SettingsModeration.tsx b/src/components/settings/SettingsModeration.tsx index 6f8ee59..7003a94 100644 --- a/src/components/settings/SettingsModeration.tsx +++ b/src/components/settings/SettingsModeration.tsx @@ -263,6 +263,14 @@ export function SettingsModeration() { return null; } + function getLabelerSummary(did: string) { + const policy = policyByDid().get(did); + const displayName = policy?.labelerDisplayName?.trim() || policy?.labelerHandle?.trim() || "Unknown labeler"; + const handle = policy?.labelerHandle?.trim(); + const normalizedHandle = handle ? `@${handle.replace(/^@/, "")}` : "@unknown"; + return `${displayName} | ${normalizedHandle} | ${did}`; + } + function isMasBuild() { return distributionChannel() === "mac_app_store"; } @@ -377,7 +385,9 @@ export function SettingsModeration() { return (
- {did} + + {getLabelerSummary(did)} +
0} diff --git a/src/components/settings/tests/SettingsModeration.test.tsx b/src/components/settings/tests/SettingsModeration.test.tsx index 6a1b26d..6abf25f 100644 --- a/src/components/settings/tests/SettingsModeration.test.tsx +++ b/src/components/settings/tests/SettingsModeration.test.tsx @@ -42,9 +42,13 @@ describe("SettingsModeration", () => { unsubscribeLabelerMock.mockResolvedValue(void 0); getLabelerPolicyDefinitionsMock.mockResolvedValue([{ labelerDid: "did:plc:ar7c4by46qjdydhdevvrndac", + labelerHandle: "moderation.bsky.app", + labelerDisplayName: "Bluesky Moderation", definitions: [{ identifier: "graphic-media", adultOnly: false, severity: "alert", blurs: "media", locales: [] }], }, { labelerDid: "did:plc:custom-labeler", + labelerHandle: "custom-labeler.test", + labelerDisplayName: "Custom Labeler", definitions: [{ identifier: "porn", adultOnly: true, severity: "alert", blurs: "media", locales: [] }], }]); getDistributionChannelMock.mockResolvedValue("github"); @@ -86,6 +90,13 @@ describe("SettingsModeration", () => { await waitFor(() => expect(unsubscribeLabelerMock).toHaveBeenCalledWith("did:plc:custom-labeler")); }); + it("shows labeler identity metadata in label preference headings", async () => { + render(() => ); + + expect(await screen.findByText("Custom Labeler | @custom-labeler.test | did:plc:custom-labeler")) + .toBeInTheDocument(); + }); + it("adds a label visibility override", async () => { render(() => ); -- 2.51.2