diff --git a/docs/api-change.md b/docs/api-change.md index 28087428dc..f630552080 100644 --- a/docs/api-change.md +++ b/docs/api-change.md @@ -4,6 +4,11 @@ Breaking changes are indicated by the :warning: icon. ## v1.0.5 (unreleased) +### dev21 + +- `admin/update-meta` can now take `moreUrls` parameter, and response of `admin/meta` now includes `moreUrls` + - These URLs are used for the help menu ([related merge request](https://git.joinfirefish.org/firefish/firefish/-/merge_requests/10640)) + ### dev18 - :warning: response of `meta` no longer includes the following: diff --git a/locales/en-US.yml b/locales/en-US.yml index d12ca8a753..64cce811ed 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -2161,3 +2161,5 @@ _iconSets: regular: "Regular" fill: "Filled" duotone: "Duotone" +moreUrls: "Pinned pages" +moreUrlsDescription: "Enter the pages you want to pin to the help menu in the lower left corner using this notation:\n\"Display name\": https://example.com/" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 53d2ca692b..0a6740b340 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2003,3 +2003,5 @@ _iconSets: regular: "標準" fill: "塗りつぶし" duotone: "2色" +moreUrls: "固定するページ" +moreUrlsDescription: "左下のヘルプメニューに固定したいページを以下の形式で、改行区切りで入力してください:\n\"表示名\": https://example.com/" diff --git a/packages/backend/migration/1699305365258-more-urls.js b/packages/backend/migration/1699305365258-more-urls.js new file mode 100644 index 0000000000..6ef1dcd2b3 --- /dev/null +++ b/packages/backend/migration/1699305365258-more-urls.js @@ -0,0 +1,13 @@ +export class MoreUrls1699305365258 { + name = "MoreUrls1699305365258"; + + async up(queryRunner) { + queryRunner.query( + `ALTER TABLE "meta" ADD "moreUrls" jsonb NOT NULL DEFAULT '[]'`, + ); + } + + async down(queryRunner) { + queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "moreUrls"`); + } +} diff --git a/packages/backend/native-utils/src/model/entity/meta.rs b/packages/backend/native-utils/src/model/entity/meta.rs index 3d203a015e..79ff8477a7 100644 --- a/packages/backend/native-utils/src/model/entity/meta.rs +++ b/packages/backend/native-utils/src/model/entity/meta.rs @@ -75,6 +75,8 @@ pub struct Model { pub pinned_users: StringVec, #[sea_orm(column_name = "ToSUrl")] pub to_s_url: Option, + #[sea_orm(column_name = "moreUrls", column_type = "JsonBinary")] + pub more_urls: Json, #[sea_orm(column_name = "repositoryUrl")] pub repository_url: String, #[sea_orm(column_name = "feedbackUrl")] diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts index 90dff4066e..c1e3be58f4 100644 --- a/packages/backend/src/models/entities/meta.ts +++ b/packages/backend/src/models/entities/meta.ts @@ -383,6 +383,12 @@ export class Meta { }) public ToSUrl: string | null; + @Column("jsonb", { + default: [], + nullable: false, + }) + public moreUrls: [string, string][]; + @Column("varchar", { length: 512, default: "https://git.joinfirefish.org/firefish/firefish", diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 37625a0c6f..6f23ef181b 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -472,6 +472,7 @@ export default define(meta, paramDef, async (ps, me) => { description: instance.description, langs: instance.langs, tosUrl: instance.ToSUrl, + moreUrls: instance.moreUrls, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, disableRegistration: instance.disableRegistration, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 70b3d2709e..422cf49f74 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -143,6 +143,17 @@ export const paramDef = { swPublicKey: { type: "string", nullable: true }, swPrivateKey: { type: "string", nullable: true }, tosUrl: { type: "string", nullable: true }, + moreUrls: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + url: { type: "string" }, + }, + }, + nullable: true, + }, repositoryUrl: { type: "string" }, feedbackUrl: { type: "string" }, useObjectStorage: { type: "boolean" }, @@ -174,6 +185,18 @@ export const paramDef = { required: [], } as const; +function isValidHttpUrl(src: string) { + let url; + + try { + url = new URL(src); + } catch (_) { + return false; + } + + return url.protocol === "http:" || url.protocol === "https:"; +} + export default define(meta, paramDef, async (ps, me) => { const set = {} as Partial; @@ -434,6 +457,14 @@ export default define(meta, paramDef, async (ps, me) => { set.ToSUrl = ps.tosUrl; } + if (ps.moreUrls !== undefined) { + const areUrlsVaild = ps.moreUrls.every( + (obj: { name: string; url: string }) => isValidHttpUrl(String(obj.url)), + ); + if (!areUrlsVaild) throw new Error("invalid URL"); + set.moreUrls = ps.moreUrls; + } + if (ps.repositoryUrl !== undefined) { set.repositoryUrl = ps.repositoryUrl; } diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 03cf0fb631..9dffad780f 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -64,6 +64,11 @@ export const meta = { optional: false, nullable: true, }, + moreUrls: { + type: "object", + optional: false, + nullable: false, + }, repositoryUrl: { type: "string", optional: false, @@ -416,6 +421,7 @@ export default define(meta, paramDef, async (ps, me) => { description: instance.description, langs: instance.langs, tosUrl: instance.ToSUrl, + moreUrls: instance.moreUrls, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue index 8de9f749e2..b54e5349b3 100644 --- a/packages/client/src/pages/admin/settings.vue +++ b/packages/client/src/pages/admin/settings.vue @@ -24,11 +24,18 @@ + + + + + (null); const description = ref(null); const tosUrl = ref(null); +const moreUrls = ref(null); const maintainerName = ref(null); const maintainerEmail = ref(null); const donationLink = ref(null); @@ -480,12 +488,44 @@ const defaultReactionCustom = ref(""); const enableServerMachineStats = ref(false); const enableIdenticonGeneration = ref(false); +function isValidHttpUrl(src: string) { + let url: URL; + try { + url = new URL(src); + } catch (_) { + return false; + } + return url.protocol === "http:" || url.protocol === "https:"; +} + +function parseMoreUrls(src: string): { name: string; url: string }[] { + const toReturn: { name: string; url: string }[] = []; + const pattern = /"(.+)"\s*:\s*(http.+)/; + src.trim() + .split("\n") + .forEach((line) => { + const match = pattern.exec(line); + if (match != null && isValidHttpUrl(match[2])) + toReturn.push({ name: match[1], url: match[2] }); + else console.error(`invalid syntax or invalid URL: ${line}`); + }); + return toReturn; +} + +function stringifyMoreUrls(src: { name: string; url: string }[]): string { + let toReturn = ""; + for (const { name, url } of src) + toReturn = toReturn.concat(`"${name}": ${url}`, "\n"); + return toReturn; +} + async function init() { const meta = await os.api("admin/meta"); if (!meta) throw new Error("No meta"); name.value = meta.name; description.value = meta.description; tosUrl.value = meta.tosUrl; + moreUrls.value = stringifyMoreUrls(meta.moreUrls); iconUrl.value = meta.iconUrl; bannerUrl.value = meta.bannerUrl; logoImageUrl.value = meta.logoImageUrl; @@ -535,6 +575,7 @@ function save() { name: name.value, description: description.value, tosUrl: tosUrl.value, + moreUrls: parseMoreUrls(moreUrls.value ?? ""), iconUrl: iconUrl.value, bannerUrl: bannerUrl.value, logoImageUrl: logoImageUrl.value, diff --git a/packages/client/src/scripts/helpMenu.ts b/packages/client/src/scripts/helpMenu.ts index a8544b5409..0bb3177e70 100644 --- a/packages/client/src/scripts/helpMenu.ts +++ b/packages/client/src/scripts/helpMenu.ts @@ -5,6 +5,31 @@ import { host } from "@/config"; import * as os from "@/os"; import { i18n } from "@/i18n"; import icon from "@/scripts/icon"; +import type { MenuItem } from "@/types/menu"; + +const instanceSpecificItems: MenuItem[] = []; + +if (instance.tosUrl != null) { + instanceSpecificItems.push({ + type: "button", + text: i18n.ts.tos, + icon: `${icon("ph-scroll")}`, + action: () => { + window.open(instance.tosUrl, "_blank"); + }, + }); +} + +for (const { name, url } of instance.moreUrls) { + instanceSpecificItems.push({ + type: "button", + text: name, + icon: `${icon("ph-link-simple")}`, + action: () => { + window.open(url, "_blank"); + }, + }); +} export function openHelpMenu_(ev: MouseEvent) { os.popupMenu( @@ -25,16 +50,9 @@ export function openHelpMenu_(ev: MouseEvent) { icon: `${icon("ph-lightbulb")}`, to: "/about-firefish", }, - instance.tosUrl - ? { - type: "button", - text: i18n.ts.tos, - icon: `${icon("ph-scroll")}`, - action: () => { - window.open(instance.tosUrl, "_blank"); - }, - } - : null, + ...(instanceSpecificItems.length >= 2 ? [null] : []), + ...instanceSpecificItems, + null, { type: "button", text: i18n.ts.apps,