Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ jobs:
run: |
npm ci
npm run lint
# About links to the copy on main, so that copy is the one people read, and it trails every bump.
# About carries both files inside the binary, so the tag ships whatever the tree holds.
- uses: ./.github/actions/toolchain
- name: The notices on main name what this build ships
if: startsWith(github.ref, 'refs/tags/v')
run: |
npm run notices
if ! git diff --exit-code THIRD-PARTY-BUNDLED.md; then
echo "::error::THIRD-PARTY-BUNDLED.md is behind the lockfiles; run npm run notices and land it before tagging"
if ! git diff --exit-code THIRD-PARTY-BUNDLED.md THIRD-PARTY-LICENSES.md; then
echo "::error::THIRD-PARTY-BUNDLED.md or THIRD-PARTY-LICENSES.md is behind the lockfiles; run npm run notices and land them before tagging"
exit 1
fi

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/rules.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ jobs:
echo "touched=true" >> "$GITHUB_OUTPUT"
exit 0
fi
watched='^(scripts/third-party\.mjs|THIRD-PARTY-BUNDLED\.md|Cargo\.(lock|toml)|.*/Cargo\.toml|app/package(-lock)?\.json|\.github/workflows/rules\.yml)$'
watched='^(scripts/third-party\.mjs|THIRD-PARTY-(BUNDLED|LICENSES)\.md|scripts/licences/.*|Cargo\.(lock|toml)|.*/Cargo\.toml|app/package(-lock)?\.json|\.github/workflows/rules\.yml)$'
from=$(git merge-base "$BASE" HEAD)
if git diff --name-only --diff-filter=d "$from" HEAD | grep -qE "$watched"; then
echo "touched=true" >> "$GITHUB_OUTPUT"
Expand All @@ -137,7 +137,7 @@ jobs:
run: |
(cd app && npm ci)
npm run notices
if ! git diff --exit-code THIRD-PARTY-BUNDLED.md; then
echo "::error::THIRD-PARTY-BUNDLED.md is behind the lockfiles; run npm run notices and commit it"
if ! git diff --exit-code THIRD-PARTY-BUNDLED.md THIRD-PARTY-LICENSES.md; then
echo "::error::THIRD-PARTY-BUNDLED.md or THIRD-PARTY-LICENSES.md is behind the lockfiles; run npm run notices and commit them"
exit 1
fi
2 changes: 1 addition & 1 deletion .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"globs": ["**/*.md", "!node_modules", "!target", "!app/node_modules", "!THIRD-PARTY-BUNDLED.md"],
"globs": ["**/*.md", "!node_modules", "!target", "!app/node_modules", "!THIRD-PARTY-BUNDLED.md", "!THIRD-PARTY-LICENSES.md"],
"config": {
"default": true,
"MD013": false,
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,11 @@ work.

What ships inside the binaries, each under its own licence, is listed in
[THIRD-PARTY-BUNDLED.md](THIRD-PARTY-BUNDLED.md); `npm run notices` writes it
from the lockfiles. The application carries it inside its own binary, under
**About → Third-party notices**, wherever it was installed from and with no
network.
from the lockfiles, together with
[THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md), the licence text of every
crate. The application carries both inside its own binary, under **About →
Third-party notices** and **About → Licence texts**, wherever it was installed
from and with no network.

---

Expand Down
4 changes: 3 additions & 1 deletion THIRD-PARTY-BUNDLED.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
<!-- Written by `npm run notices`. Do not edit by hand. -->

LinkUnbound is GPL-3.0-only. The binary carries the work below, each under its own
licence. Nothing of it was copied into LinkUnbound's own source.
licence. Nothing of it was copied into LinkUnbound's own source. The licence text
of every crate is in [THIRD-PARTY-LICENSES.md](https://github.com/rgdevment/LinkUnbound/blob/main/THIRD-PARTY-LICENSES.md),
also under About → Licence texts.

## In the window (12 packages)

Expand Down
13,799 changes: 13,799 additions & 0 deletions THIRD-PARTY-LICENSES.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions app/src-tauri/src/about.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use crate::{HERE, store, update};

const NOTICES: &str = include_str!("../../../THIRD-PARTY-BUNDLED.md");
const LICENCES: &str = include_str!("../../../THIRD-PARTY-LICENSES.md");

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -30,6 +31,6 @@
}

#[tauri::command]
pub fn notices() -> &'static str {
NOTICES
pub fn notices(licences: bool) -> &'static str {
if licences { LICENCES } else { NOTICES }

Check warning on line 35 in app/src-tauri/src/about.rs

View workflow job for this annotation

GitHub Actions / settings / the lines this branch changed

Missed mutant

replace notices -> &'static str with "xyzzy"

Check warning on line 35 in app/src-tauri/src/about.rs

View workflow job for this annotation

GitHub Actions / settings / the lines this branch changed

Missed mutant

replace notices -> &'static str with ""
}
4 changes: 4 additions & 0 deletions app/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ const ES = {
aboutPrivacyLink: "Privacidad",
aboutNotices: "Avisos de terceros",
noticesRefused: "No se pudieron leer los avisos de terceros",
aboutLicences: "Textos de las licencias",
licencesRefused: "No se pudieron leer los textos de las licencias",
aboutIssueNote: "Errores, ideas y navegadores que no detecta",
aboutSponsor: "Invítame un café",
aboutSponsorNote:
Expand Down Expand Up @@ -527,6 +529,8 @@ const EN: Record<Key, string> = {
aboutPrivacyLink: "Privacy",
aboutNotices: "Third-party notices",
noticesRefused: "The third-party notices could not be read",
aboutLicences: "Licence texts",
licencesRefused: "The licence texts could not be read",
aboutIssueNote: "Bugs, ideas and browsers it fails to detect",
aboutSponsor: "Buy me a coffee",
aboutSponsorNote:
Expand Down
17 changes: 17 additions & 0 deletions app/src/markdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import bundled from "../../THIRD-PARTY-BUNDLED.md?raw";
import licences from "../../THIRD-PARTY-LICENSES.md?raw";
import { composed } from "./markdown";

describe("the notices the binary carries", () => {
for (const [name, text] of [
["THIRD-PARTY-BUNDLED.md", bundled],
["THIRD-PARTY-LICENSES.md", licences],
]) {
it(`${name} links only to places outside the window`, () => {
const page = new DOMParser().parseFromString(composed(text), "text/html");
const links = [...page.querySelectorAll("a")].map((one) => one.getAttribute("href") ?? "");
expect(links.filter((href) => !/^(https?:\/\/|mailto:)/.test(href))).toEqual([]);
});
}
});
69 changes: 66 additions & 3 deletions app/src/settings/About.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ function showAsking() {
render(<Host from={null} step={null} starring />);
}

function answers(overrides: Record<string, () => Promise<unknown>> = {}) {
invoke.mockImplementation((cmd: string) => {
if (cmd in overrides) return overrides[cmd]();
function answers(overrides: Record<string, (args?: unknown) => Promise<unknown>> = {}) {
invoke.mockImplementation((cmd: string, args?: unknown) => {
if (cmd in overrides) return overrides[cmd](args);
if (cmd === "about") return Promise.resolve(BUILD);
return Promise.resolve(null);
});
Expand Down Expand Up @@ -486,3 +486,66 @@ describe("third-party notices", () => {
expect(screen.queryByRole("region", { name: "Avisos de terceros" })).not.toBeInTheDocument();
});
});

describe("licence texts", () => {
beforeEach(() => {
opened.length = 0;
invoke.mockReset();
});

function shelvesSay(fail = false) {
answers({
notices: (args) => {
if ((args as { licences: boolean }).licences) {
return fail ? Promise.reject(new Error("no")) : Promise.resolve("Apache License 2.0");
}
return Promise.resolve("MIT License");
},
});
}

it("are asked for on their own button, and only when it is pressed", async () => {
shelvesSay();
show();

const texts = await screen.findByRole("button", { name: "Textos de las licencias" });
expect(invoke.mock.calls.some(([cmd]) => cmd === "notices")).toBe(false);

await userEvent.click(texts);
const shelf = await screen.findByRole("region", { name: "Textos de las licencias" });
expect(shelf).toHaveTextContent("Apache License 2.0");
expect(invoke).toHaveBeenCalledWith("notices", { licences: true });
expect(texts).toHaveAttribute("aria-expanded", "true");
expect(screen.getByRole("button", { name: "Avisos de terceros" })).toHaveAttribute(
"aria-expanded",
"false",
);
});

it("give way to the notices when those are opened instead", async () => {
shelvesSay();
show();

await userEvent.click(await screen.findByRole("button", { name: "Textos de las licencias" }));
await screen.findByRole("region", { name: "Textos de las licencias" });
await userEvent.click(screen.getByRole("button", { name: "Avisos de terceros" }));

const shelf = await screen.findByRole("region", { name: "Avisos de terceros" });
expect(shelf).toHaveTextContent("MIT License");
expect(invoke).toHaveBeenCalledWith("notices", { licences: false });
expect(
screen.queryByRole("region", { name: "Textos de las licencias" }),
).not.toBeInTheDocument();
});

it("say so when they cannot be read", async () => {
shelvesSay(true);
show();

await userEvent.click(await screen.findByRole("button", { name: "Textos de las licencias" }));

expect(await screen.findByRole("alert")).toHaveTextContent(
"No se pudieron leer los textos de las licencias",
);
});
});
52 changes: 35 additions & 17 deletions app/src/settings/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ function External({ href, children }: { href: string; children: string }) {
);
}

type Shelf = "notices" | "licences";

function Notices({ text, label }: { text: string; label: string }) {
const box = useRef<HTMLElement>(null);

Expand Down Expand Up @@ -297,19 +299,19 @@ export default function About({
const [trouble, setTrouble] = useState<string | null>(null);
const [problem, setProblem] = useState<string | null>(null);
const [saved, setSaved] = useState<string | null>(null);
const [notices, setNotices] = useState<string | null>(null);
const [readingNotices, setReadingNotices] = useState(false);
const [noticesRefused, setNoticesRefused] = useState(false);

const toggleNotices = () => {
if (readingNotices) return;
if (notices !== null) return setNotices(null);
setNoticesRefused(false);
setReadingNotices(true);
invoke<string>("notices")
.then(setNotices)
.catch(() => setNoticesRefused(true))
.finally(() => setReadingNotices(false));
const [shown, setShown] = useState<{ which: Shelf; text: string } | null>(null);
const [reading, setReading] = useState(false);
const [refused, setRefused] = useState<Shelf | null>(null);

const toggle = (which: Shelf) => {
if (reading) return;
if (shown?.which === which) return setShown(null);
setRefused(null);
setReading(true);
invoke<string>("notices", { licences: which === "licences" })
.then((text) => setShown({ which, text }))
.catch(() => setRefused(which))
.finally(() => setReading(false));
};

const report = () => {
Expand Down Expand Up @@ -487,16 +489,32 @@ export default function About({
<External href={build?.repository ?? REPO}>{t("aboutRepo")}</External>
<External href={ALTERNATIVETO}>AlternativeTo</External>
<External href={`${REPO}/blob/main/PRIVACY.md`}>{t("aboutPrivacyLink")}</External>
<button type="button" aria-expanded={notices !== null} onClick={toggleNotices}>
<button
type="button"
aria-expanded={shown?.which === "notices"}
onClick={() => toggle("notices")}
>
{t("aboutNotices")}
</button>
<button
type="button"
aria-expanded={shown?.which === "licences"}
onClick={() => toggle("licences")}
>
{t("aboutLicences")}
</button>
</div>
{noticesRefused && (
{refused && (
<p role="alert" className="alarm">
{t("noticesRefused")}
{t(refused === "licences" ? "licencesRefused" : "noticesRefused")}
</p>
)}
{notices !== null && <Notices text={notices} label={t("aboutNotices")} />}
{shown && (
<Notices
text={shown.text}
label={t(shown.which === "licences" ? "aboutLicences" : "aboutNotices")}
/>
)}
</>
);
}
Expand Down
Loading
Loading