forked from naskya/firefish
feat: ability to pin custom pages to the help menu
This commit is contained in:
parent
2911a07ac6
commit
6424b685e9
12 changed files with 138 additions and 13 deletions
|
@ -34,6 +34,7 @@
|
|||
|
||||
## 細かい変更点
|
||||
|
||||
- サーバーの管理者が左下のヘルプメニューに利用規約以外のページも固定できるように
|
||||
- 依存ライブラリのバージョンをアップデート
|
||||
- AiScript のバージョンも上がりました!
|
||||
- 絵文字ピッカーに表示されるカスタム絵文字の検索結果の件数を最大 100 件に変更(Misskey の変更を取り込み)
|
||||
|
|
|
@ -2188,3 +2188,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/"
|
||||
|
|
|
@ -2030,3 +2030,5 @@ _iconSets:
|
|||
regular: "標準"
|
||||
fill: "塗りつぶし"
|
||||
duotone: "2色"
|
||||
moreUrls: "固定するページ"
|
||||
moreUrlsDescription: "左下のヘルプメニューに固定したいページを以下の形式で、改行区切りで入力してください:\n\"表示名\": https://example.com/"
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
-- more-urls
|
||||
ALTER TABLE "meta" DROP COLUMN "moreUrls";
|
||||
|
||||
-- pgroonga
|
||||
DROP INDEX "public"."IDX_f27f5d88941e57442be75ba9c8";
|
||||
DROP INDEX "public"."IDX_065d4d8f3b5adb4a08841eae3c";
|
||||
|
|
13
packages/backend/migration-neko/1699305365258-more-urls.js
Normal file
13
packages/backend/migration-neko/1699305365258-more-urls.js
Normal file
|
@ -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"`);
|
||||
}
|
||||
}
|
|
@ -75,6 +75,8 @@ pub struct Model {
|
|||
pub pinned_users: StringVec,
|
||||
#[sea_orm(column_name = "ToSUrl")]
|
||||
pub to_s_url: Option<String>,
|
||||
#[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")]
|
||||
|
|
|
@ -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://code.naskya.net/naskya/firefish",
|
||||
|
|
|
@ -2,7 +2,6 @@ import config from "@/config/index.js";
|
|||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js";
|
||||
import define from "../../define.js";
|
||||
import { Exp } from "@tensorflow/tfjs";
|
||||
|
||||
export const meta = {
|
||||
tags: ["meta"],
|
||||
|
@ -473,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,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Meta } from "@/models/entities/meta.js";
|
||||
import { insertModerationLog } from "@/services/insert-moderation-log.js";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import define from "../../define.js";
|
||||
import define from "@/server/api/define.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["admin"],
|
||||
|
@ -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<Meta>;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -24,11 +24,18 @@
|
|||
|
||||
<FormInput v-model="tosUrl" class="_formBlock">
|
||||
<template #prefix
|
||||
><i :class="icon('ph-link-simple')"></i
|
||||
><i :class="icon('ph-scroll')"></i
|
||||
></template>
|
||||
<template #label>{{ i18n.ts.tosUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormTextarea v-model="moreUrls" class="_formBlock">
|
||||
<template #label>{{ i18n.ts.moreUrls }}</template>
|
||||
<template #caption>{{
|
||||
i18n.ts.moreUrlsDescription
|
||||
}}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormSplit :min-width="300">
|
||||
<FormInput
|
||||
v-model="maintainerName"
|
||||
|
@ -446,6 +453,7 @@ import icon from "@/scripts/icon";
|
|||
const name = ref<string | null>(null);
|
||||
const description = ref<string | null>(null);
|
||||
const tosUrl = ref<string | null>(null);
|
||||
const moreUrls = ref<string | null>(null);
|
||||
const maintainerName = ref<string | null>(null);
|
||||
const maintainerEmail = ref<string | null>(null);
|
||||
const donationLink = ref<string | null>(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,
|
||||
|
|
|
@ -6,6 +6,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(
|
||||
|
@ -26,16 +51,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,
|
||||
|
|
Loading…
Reference in a new issue