Compare commits

..

20 Commits

Author SHA1 Message Date
github-actions[bot] fbf8db618c Bump version to 2025.8.0-alpha.4 2025-08-03 02:05:40 +00:00
syuilo 6f3cc2cdf7 コントロールパネルの検索 (#16343)
* Update settings.vue

* Update settings.vue

* Update settings.vue

* Update settings.vue

* Update settings.vue

* Update performance.vue

* Update performance.vue

* Update performance.vue

* Update external-services.vue

* wip

* wip

* Update security.vue

* Update settings.vue

* Update CHANGELOG.md

* wip

* Update moderation.vue

* wip

* Update branding.vue

* wip

* Update email-settings.vue

* Update system-webhook.vue

* Update MkSuperMenu.vue

* Update index.vue
2025-08-03 11:02:20 +09:00
syuilo 7c1f4c9037 perf(frontend): tweak css performance 2025-08-03 10:01:25 +09:00
かっこかり 2da20bf3e8 fix(misskey-js): fix misskey-js autogen (#16345) 2025-08-02 17:16:24 +09:00
renovate[bot] 6d54370f01 chore(deps): update [misskey-js] update dependencies (major) (#16177)
* chore(deps): update [misskey-js] update dependencies

* enhance(misskey-js): テストスイートをVitestに置き換え (#16340)

* enhance(misskey-js): テストスイートをVitestに置き換え

* fix: 変なものが混入

* fix test, lint

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
2025-08-02 16:20:08 +09:00
かっこかり 905d3c87f1 Update CHANGELOG.md 2025-08-02 14:01:57 +09:00
syuilo fc244067e0 enhnace(frontend): リロードのサジェストをダイアログではなくバナー表示に 2025-08-02 12:10:51 +09:00
syuilo 8449354887 fix e2e 2025-08-02 11:47:24 +09:00
syuilo 57e0f1b4ef fix e2e 2025-08-02 11:03:41 +09:00
github-actions[bot] a1e170e065 Bump version to 2025.8.0-alpha.3 2025-08-01 13:35:41 +00:00
poppingmoon 73de40b81e enhance: support emojis added in Unicode 16.0 or earlier (#16329)
* add missing emojis to emojilist.json

* reorder family emojis

* reorder harp

* reorder symbols

* correct emoji genders

* add missing emojis to unicode-emoji-indexes

* add Japanese annotations for Sark flag

* correct kana notations

* add square colors

* upgrade @twemoji/parser to 16.0.0

* correct emoji genders

* upgrade mfm.js to 0.25.0
2025-08-01 22:26:29 +09:00
syuilo 2c836e3c24 fix migration
Fix #16341
2025-08-01 21:55:56 +09:00
syuilo c2c5898221 refactor 2025-08-01 19:55:55 +09:00
果物リン 99adf12355 ノート検索のホスト指定オプションの速度改善 (#16339)
Co-authored-by: misskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>
2025-08-01 17:48:26 +09:00
github-actions[bot] 7df0a6c55f Bump version to 2025.8.0-alpha.2 2025-08-01 08:23:40 +00:00
かっこかり e092008dc5 feat(frontend): セーフモード (#16245)
* feat(frontend): セーフモード

* Update Changelog

* Update Changelog

* fix

* fix

* Update Changelog

* Update Changelog

* PWAのショートカット経由でもセーフモードで起動できるように

* Update ClientServerService.ts

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-08-01 17:20:40 +09:00
syuilo 0cfc910cdc enhance(frontend): いくつかの翻訳を調整 2025-08-01 16:18:47 +09:00
かっこかり 62f68de800 fix(frontend); Playのボタンがはみ出している問題を修正 (#16303) 2025-08-01 14:31:49 +09:00
syuilo 5bf13c4cc2 Update CHANGELOG.md 2025-08-01 13:44:06 +09:00
syuilo 16f47adcc6 Update CHANGELOG.md 2025-08-01 13:43:09 +09:00
94 changed files with 2438 additions and 2079 deletions
+14
View File
@@ -9,9 +9,23 @@
- 定期的に参照されていない古いリモートの投稿を削除する機能が実装されました(コントロールパネル→パフォーマンス→Remote Notes Cleaning)
- 既存のサーバーでは**デフォルトでオフ**、新規サーバーでは**デフォルトでオン**になります
- データベースの肥大化を防止することが可能です
- 既存のサーバーで当機能を有効化した場合は、処理量が多くなるため、一時的にストレージ使用量が増加する可能性があります。
- 増加量を抑えるには、最大処理継続時間をデフォルトより短くしてください。
- サーバーの初期設定が完了するまでは連合がオンにならないようになりました
- 日本語における公開範囲名称の「ダイレクト」が「指名」に改称されました
- mfm.jsをアップデートしました
- Enhance: Unicode 15.1 および 16.0 に収録されている絵文字に対応
- Enhance: acctに `.` が入っているユーザーのメンションに対応
- Fix: Unicode絵文字に隣接する異体字セレクタ(`U+FE0F`)が絵文字として認識される問題を修正
### Client
- Feat: セーフモード
- プラグイン・テーマ・カスタムCSSの使用でクライアントの起動に問題が発生した際に、これらを無効にして起動できます
- 以下の方法でセーフモードを起動できます
- `g` キーを連打する
- URLに`?safemode=true`を付ける
- PWAのショートカットで Safemode を選択して起動する
- Enhance: コントロールパネルを検索できるように
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
- Fix: テーマエディタが動作しない問題を修正
+27 -7
View File
@@ -315,11 +315,11 @@ export interface Locale extends ILocale {
*/
"mention": string;
/**
*
*
*/
"mentions": string;
/**
* 稿
*
*/
"directNotes": string;
/**
@@ -5501,6 +5501,22 @@ export interface Locale extends ILocale {
*
*/
"inDays": string;
/**
*
*/
"safeModeEnabled": string;
/**
*
*/
"pluginsAreDisabledBecauseSafeMode": string;
/**
* CSSは適用されていません
*/
"customCssIsDisabledBecauseSafeMode": string;
/**
* 使
*/
"themeIsDefaultBecauseSafeMode": string;
"_order": {
/**
*
@@ -6337,7 +6353,7 @@ export interface Locale extends ILocale {
*/
"followers": string;
/**
* 使
*
*/
"direct": string;
/**
@@ -6345,7 +6361,7 @@ export interface Locale extends ILocale {
*/
"doNotSendConfidencialOnDirect1": string;
/**
* 稿稿
* 稿
*/
"doNotSendConfidencialOnDirect2": string;
/**
@@ -9625,7 +9641,7 @@ export interface Locale extends ILocale {
*/
"followersDescription": string;
/**
*
*
*/
"specified": string;
/**
@@ -10514,11 +10530,11 @@ export interface Locale extends ILocale {
*/
"channel": string;
/**
*
*
*/
"mentions": string;
/**
*
*
*/
"direct": string;
/**
@@ -11839,6 +11855,10 @@ export interface Locale extends ILocale {
*
*/
"otherOption3": string;
/**
* Misskeyをセーフモードで起動
*/
"otherOption4": string;
};
"_search": {
/**
+12 -7
View File
@@ -74,8 +74,8 @@ youGotNewFollower: "フォローされました"
receiveFollowRequest: "フォローリクエストされました"
followRequestAccepted: "フォローが承認されました"
mention: "メンション"
mentions: "あなた宛て"
directNotes: "ダイレクト投稿"
mentions: "メンション"
directNotes: "指名"
importAndExport: "インポートとエクスポート"
import: "インポート"
export: "エクスポート"
@@ -1370,6 +1370,10 @@ defaultImageCompressionLevel: "デフォルトの画像圧縮度"
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
inMinutes: "分"
inDays: "日"
safeModeEnabled: "セーフモードが有効です"
pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プラグインはすべて無効化されています。"
customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。"
themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。"
_order:
newest: "新しい順"
@@ -1605,9 +1609,9 @@ _initialTutorial:
public: "すべてのユーザーに公開。"
home: "ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・リノートから、他のユーザーも見ることができます。"
followers: "フォロワーにのみ公開。本人以外がリノートすることはできず、またフォロワー以外は閲覧できません。"
direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。"
direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。"
doNotSendConfidencialOnDirect1: "機密情報は送信する際は注意してください。"
doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。"
doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーが含まれる限定公開のノートを作成する際は、機密情報の扱いに注意が必要です。"
localOnly: "他のサーバーに投稿を連合しません。上記の公開範囲に関わらず、他のサーバーのユーザーは、この設定がついたノートを直接閲覧することができなくなります。"
_cw:
title: "内容を隠す(CW"
@@ -2532,7 +2536,7 @@ _visibility:
homeDescription: "ホームタイムラインのみに公開"
followers: "フォロワー"
followersDescription: "自分のフォロワーのみに公開"
specified: "ダイレクト"
specified: "指名"
specifiedDescription: "指定したユーザーのみに公開"
disableFederation: "連合なし"
disableFederationDescription: "他サーバーへの配信を行いません"
@@ -2778,8 +2782,8 @@ _deck:
antenna: "アンテナ"
list: "リスト"
channel: "チャンネル"
mentions: "あなた宛て"
direct: "ダイレクト"
mentions: "メンション"
direct: "指名"
roleTimeline: "ロールタイムライン"
chat: "チャット"
@@ -3164,6 +3168,7 @@ _bootErrors:
otherOption1: "クライアント設定とキャッシュを削除"
otherOption2: "簡易クライアントを起動"
otherOption3: "修復ツールを起動"
otherOption4: "Misskeyをセーフモードで起動"
_search:
searchScopeAll: "全て"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.8.0-alpha.1",
"version": "2025.8.0-alpha.4",
"codename": "nasubi",
"repository": {
"type": "git",
@@ -7,7 +7,7 @@ export class RemoteNotesCleaning1753863104203 {
name = 'RemoteNotesCleaning1753863104203'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableRemoteNotesCleaning" boolean NOT NULL DEFAULT true`);
await queryRunner.query(`ALTER TABLE "meta" ADD "enableRemoteNotesCleaning" boolean NOT NULL DEFAULT false`);
await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningMaxProcessingDurationInMinutes" integer NOT NULL DEFAULT \'60\'');
await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningExpiryDaysForEachNotes" integer NOT NULL DEFAULT \'90\'');
}
@@ -8,11 +8,9 @@ export class TweakDefaultFederationSettings1754019326356 {
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'none'`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "enableRemoteNotesCleaning" SET DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "enableRemoteNotesCleaning" SET DEFAULT true`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'all'`);
}
}
+3 -3
View File
@@ -70,7 +70,7 @@
"dependencies": {
"@aws-sdk/client-s3": "3.826.0",
"@aws-sdk/lib-storage": "3.826.0",
"@discordapp/twemoji": "15.1.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.2",
"@fastify/cookie": "11.0.2",
"@fastify/cors": "10.1.0",
@@ -93,7 +93,7 @@
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.7.7",
"@swc/core": "1.12.0",
"@twemoji/parser": "15.1.1",
"@twemoji/parser": "16.0.0",
"@types/redis-info": "3.0.3",
"accepts": "1.3.8",
"ajv": "8.17.1",
@@ -135,7 +135,7 @@
"jsrsasign": "11.1.0",
"juice": "11.0.1",
"meilisearch": "0.51.0",
"mfm-js": "0.24.0",
"mfm-js": "0.25.0",
"microformats-parser": "2.0.3",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
+2 -2
View File
@@ -227,9 +227,9 @@ export class SearchService {
if (opts.host) {
if (opts.host === '.') {
query.andWhere('user.host IS NULL');
query.andWhere('note.userHost IS NULL');
} else {
query.andWhere('user.host = :host', { host: opts.host });
query.andWhere('note.userHost = :host', { host: opts.host });
}
}
File diff suppressed because one or more lines are too long
@@ -188,6 +188,10 @@ export class ClientServerService {
'url': 'url',
},
},
'shortcuts': [{
'name': 'Safemode',
'url': '/?safemode=true',
}],
};
manifest = {
+42 -16
View File
@@ -94,23 +94,37 @@
}
//#endregion
//#region Theme
const theme = localStorage.getItem('theme');
if (theme) {
for (const [k, v] of Object.entries(JSON.parse(theme))) {
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
let isSafeMode = (localStorage.getItem('isSafeMode') === 'true');
// HTMLの theme-color 適用
if (k === 'htmlThemeColor') {
for (const tag of document.head.children) {
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', v);
break;
if (!isSafeMode) {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('safemode') && urlParams.get('safemode') === 'true') {
localStorage.setItem('isSafeMode', 'true');
isSafeMode = true;
}
}
//#region Theme
if (!isSafeMode) {
const theme = localStorage.getItem('theme');
if (theme) {
for (const [k, v] of Object.entries(JSON.parse(theme))) {
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
// HTMLの theme-color 適用
if (k === 'htmlThemeColor') {
for (const tag of document.head.children) {
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', v);
break;
}
}
}
}
}
}
const colorScheme = localStorage.getItem('colorScheme');
if (colorScheme) {
document.documentElement.style.setProperty('color-scheme', colorScheme);
@@ -127,11 +141,13 @@
document.documentElement.classList.add('useSystemFont');
}
const customCss = localStorage.getItem('customCss');
if (customCss && customCss.length > 0) {
const style = document.createElement('style');
style.innerHTML = customCss;
document.head.appendChild(style);
if (!isSafeMode) {
const customCss = localStorage.getItem('customCss');
if (customCss && customCss.length > 0) {
const style = document.createElement('style');
style.innerHTML = customCss;
document.head.appendChild(style);
}
}
async function addStyle(styleText) {
@@ -159,9 +175,13 @@
otherOption1: 'Clear preferences and cache',
otherOption2: 'Start the simple client',
otherOption3: 'Start the repair tool',
otherOption4: 'Start Misskey in safe mode',
}, locale?._bootErrors || {});
const reload = locale?.reload || 'Reload';
const safeModeUrl = new URL(window.location.href);
safeModeUrl.searchParams.set('safemode', 'true');
let errorsElement = document.getElementById('errors');
if (!errorsElement) {
@@ -182,6 +202,12 @@
<p>${messages.solution4}</p>
<details style="color: #86b300;">
<summary>${messages.otherOption}</summary>
<a href="${safeModeUrl}">
<button class="button-small">
<span class="button-label-small">${messages.otherOption4}</span>
</button>
</a>
<br>
<a href="/flush">
<button class="button-small">
<span class="button-label-small">${messages.otherOption1}</span>
@@ -34,5 +34,11 @@
"text": "text",
"url": "url"
}
}
},
"shortcuts": [
{
"name": "Safemode",
"url": "/?safemode=true"
}
]
}
@@ -79,6 +79,9 @@ async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse
rateLimitFactor: 0 as never,
},
}, res.token);
await client.request('admin/update-meta', {
federation: 'all',
}, res.token);
return res;
}).catch(err => {
if (err.info.e.message === 'access denied') return undefined;
+1
View File
@@ -24,6 +24,7 @@ describe('Endpoints', () => {
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
dave = await signup({ username: 'dave' });
await api('admin/update-meta', { federation: 'all' }, alice as misskey.entities.SignupResponse);
}, 1000 * 60 * 2);
describe('signup', () => {
+2 -1
View File
@@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { channel, clip, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js';
import { api, channel, clip, galleryPost, page, play, post, signup, simpleGet, uploadFile } from '../utils.js';
import type { SimpleGetResponse } from '../utils.js';
import type * as misskey from 'misskey-js';
@@ -78,6 +78,7 @@ describe('Webリソース', () => {
beforeAll(async () => {
alice = await signup({ username: 'alice' });
await api('admin/update-meta', { federation: 'all' }, alice as misskey.entities.SignupResponse);
aliceUploadedFile = (await uploadFile(alice)).body;
alicesPost = await post(alice, {
text: 'test',
@@ -16,6 +16,7 @@ describe('FF visibility', () => {
beforeAll(async () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
await api('admin/update-meta', { federation: 'all' }, alice as misskey.entities.SignupResponse);
}, 1000 * 60 * 2);
test('followingVisibility, followersVisibility がともに public なユーザーのフォロー/フォロワーを誰でも見れる', async () => {
+2 -1
View File
@@ -6,7 +6,7 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { host, origin, relativeFetch, signup } from '../utils.js';
import { api, host, origin, relativeFetch, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('.well-known', () => {
@@ -14,6 +14,7 @@ describe('.well-known', () => {
beforeAll(async () => {
alice = await signup({ username: 'alice' });
await api('admin/update-meta', { federation: 'all' }, alice as misskey.entities.SignupResponse);
}, 1000 * 60 * 2);
test('nodeinfo', async () => {
+3 -3
View File
@@ -10,11 +10,11 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"dependencies": {
"@discordapp/twemoji": "15.1.0",
"@discordapp/twemoji": "16.0.1",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.2.0",
"@twemoji/parser": "15.1.1",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "5.2.4",
"@vue/compiler-sfc": "3.5.17",
"astring": "1.9.0",
@@ -23,7 +23,7 @@
"icons-subsetter": "workspace:*",
"frontend-shared": "workspace:*",
"json5": "2.2.3",
"mfm-js": "0.24.0",
"mfm-js": "0.25.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.45.1",
+1
View File
@@ -23,6 +23,7 @@ export const version = _VERSION_;
export const instanceName = (siteName === 'Misskey' || siteName == null) ? host : siteName;
export const ui = localStorage.getItem('ui');
export const debug = localStorage.getItem('debug') === 'true';
export const isSafeMode = localStorage.getItem('isSafeMode') === 'true';
export function updateLocale(newLocale: Locale): void {
locale = newLocale;
+136 -32
View File
@@ -44,6 +44,8 @@
["😑", "expressionless", 0],
["😒", "unamused", 0],
["🙄", "roll_eyes", 0],
["🙂‍↔️", "head_shaking_horizontally", 0],
["🙂‍↕️", "head_shaking_vertically", 0],
["🤔", "thinking", 0],
["🤥", "lying_face", 0],
["🤭", "hand_over_mouth", 0],
@@ -76,6 +78,7 @@
["😥", "disappointed_relieved", 0],
["🤤", "drooling_face", 0],
["😪", "sleepy", 0],
["🫩", "face_with_bags_under_eyes", 0],
["😓", "sweat", 0],
["🥵", "hot", 0],
["🥶", "cold", 0],
@@ -92,6 +95,7 @@
["🥴", "woozy", 0],
["🥱", "yawning", 0],
["😴", "sleeping", 0],
["👁️‍🗨️", "eye_in_speech_bubble", 0],
["💤", "zzz", 0],
["😶‍🌫️", "face_in_clouds", 0],
["😮‍💨", "face_exhaling", 0],
@@ -185,6 +189,7 @@
["👤", "bust_in_silhouette", 1],
["👥", "busts_in_silhouette", 1],
["🗣", "speaking_head", 1],
["🫆", "fingerprint", 1],
["👶", "baby", 1],
["🧒", "child", 1],
["👦", "boy", 1],
@@ -198,8 +203,9 @@
["🧑‍🦰", "red_hair", 1],
["👩‍🦰", "red_hair_woman", 1],
["👨‍🦰", "red_hair_man", 1],
["👱", "blonde_person", 1],
["👱‍♀️", "blonde_woman", 1],
["👱", "blonde_man", 1],
["👱‍♂️", "blonde_man", 1],
["🧑‍🦳", "white_hair", 1],
["👩‍🦳", "white_hair_woman", 1],
["👨‍🦳", "white_hair_man", 1],
@@ -207,21 +213,28 @@
["👩‍🦲", "bald_woman", 1],
["👨‍🦲", "bald_man", 1],
["🧔", "bearded_person", 1],
["🧔‍♀️", "beard_woman", 1],
["🧔‍♂️", "beard_man", 1],
["🧓", "older_adult", 1],
["👴", "older_man", 1],
["👵", "older_woman", 1],
["👲", "man_with_gua_pi_mao", 1],
["🧕", "woman_with_headscarf", 1],
["👳", "person_with_turban", 1],
["👳‍♀️", "woman_with_turban", 1],
["👳", "man_with_turban", 1],
["👳‍♂️", "man_with_turban", 1],
["👮", "police_officer", 1],
["👮‍♀️", "policewoman", 1],
["👮", "policeman", 1],
["👮‍♂️", "policeman", 1],
["👷", "construction_worker", 1],
["👷‍♀️", "construction_worker_woman", 1],
["👷", "construction_worker_man", 1],
["👷‍♂️", "construction_worker_man", 1],
["💂", "guard", 1],
["💂‍♀️", "guardswoman", 1],
["💂", "guardsman", 1],
["💂‍♂️", "guardsman", 1],
["🕵", "detective", 1],
["🕵️‍♀️", "female_detective", 1],
["🕵", "male_detective", 1],
["🕵‍♂️", "male_detective", 1],
["🧑‍⚕️", "health_worker", 1],
["👩‍⚕️", "woman_health_worker", 1],
["👨‍⚕️", "man_health_worker", 1],
@@ -270,26 +283,35 @@
["🧑‍⚖️", "judge", 1],
["👩‍⚖️", "woman_judge", 1],
["👨‍⚖️", "man_judge", 1],
["🦸", "superhero", 1],
["🦸‍♀️", "woman_superhero", 1],
["🦸‍♂️", "man_superhero", 1],
["🦹", "supervillain", 1],
["🦹‍♀️", "woman_supervillain", 1],
["🦹‍♂️", "man_supervillain", 1],
["🤶", "mrs_claus", 1],
["🧑‍🎄", "mx_claus", 1],
["🎅", "santa", 1],
["🥷", "ninja", 1],
["🧙", "mage", 1],
["🧙‍♀️", "sorceress", 1],
["🧙‍♂️", "wizard", 1],
["🧝", "elf", 1],
["🧝‍♀️", "woman_elf", 1],
["🧝‍♂️", "man_elf", 1],
["🧛", "vampire", 1],
["🧛‍♀️", "woman_vampire", 1],
["🧛‍♂️", "man_vampire", 1],
["🧟", "zombie", 1],
["🧟‍♀️", "woman_zombie", 1],
["🧟‍♂️", "man_zombie", 1],
["🧞", "genie", 1],
["🧞‍♀️", "woman_genie", 1],
["🧞‍♂️", "man_genie", 1],
["🧜", "merperson", 1],
["🧜‍♀️", "mermaid", 1],
["🧜‍♂️", "merman", 1],
["🧚", "fairy", 1],
["🧚‍♀️", "woman_fairy", 1],
["🧚‍♂️", "man_fairy", 1],
["👼", "angel", 1],
@@ -305,68 +327,108 @@
["👸", "princess", 1],
["🤴", "prince", 1],
["👰", "person_with_veil", 1],
["👰", "bride_with_veil", 1],
["👰‍♀️", "bride_with_veil", 1],
["👰‍♂️", "man_with_veil", 1],
["🤵", "person_in_tuxedo", 1],
["🤵", "man_in_tuxedo", 1],
["🤵‍♀️", "woman_in_tuxedo", 1],
["🤵‍♂️", "man_in_tuxedo", 1],
["🏃", "running_person", 1],
["🏃‍➡️", "running_person_facing_right", 1],
["🏃‍♀️", "running_woman", 1],
["🏃", "running_man", 1],
["🏃‍♀️‍➡️", "running_woman_facing_right", 1],
["🏃‍♂️", "running_man", 1],
["🏃‍♂️‍➡️", "running_man_facing_right", 1],
["🚶", "walking_person", 1],
["🚶‍➡️", "walking_person_facing_right", 1],
["🚶‍♀️", "walking_woman", 1],
["🚶", "walking_man", 1],
["🚶‍♀️‍➡️", "walking_woman_facing_right", 1],
["🚶‍♂️", "walking_man", 1],
["🚶‍♂️‍➡️", "walking_man_facing_right", 1],
["💃", "dancer", 1],
["🕺", "man_dancing", 1],
["👯", "dancing_women", 1],
["👯", "dancing_people", 1],
["👯‍♀️", "dancing_women", 1],
["👯‍♂️", "dancing_men", 1],
["👫", "couple", 1],
["🧑‍🤝‍🧑", "people_holding_hands", 1],
["👬", "two_men_holding_hands", 1],
["👭", "two_women_holding_hands", 1],
["🫂", "people_hugging", 1],
["🙇", "bowing_person", 1],
["🙇‍♀️", "bowing_woman", 1],
["🙇", "bowing_man", 1],
["🙇‍♂️", "bowing_man", 1],
["🤦", "person_facepalming", 1],
["🤦‍♂️", "man_facepalming", 1],
["🤦‍♀️", "woman_facepalming", 1],
["🤷", "woman_shrugging", 1],
["🤷", "person_shrugging", 1],
["🤷‍♀️", "woman_shrugging", 1],
["🤷‍♂️", "man_shrugging", 1],
["💁", "tipping_hand_woman", 1],
["💁", "tipping_hand_person", 1],
["💁‍♀️", "tipping_hand_woman", 1],
["💁‍♂️", "tipping_hand_man", 1],
["🙅", "no_good_woman", 1],
["🙅", "no_good_person", 1],
["🙅‍♀️", "no_good_woman", 1],
["🙅‍♂️", "no_good_man", 1],
["🙆", "ok_woman", 1],
["🙆", "ok_person", 1],
["🙆‍♀️", "ok_woman", 1],
["🙆‍♂️", "ok_man", 1],
["🙋", "raising_hand_woman", 1],
["🙋", "raising_hand_person", 1],
["🙋‍♀️", "raising_hand_woman", 1],
["🙋‍♂️", "raising_hand_man", 1],
["🙎", "pouting_woman", 1],
["🙎", "pouting_person", 1],
["🙎‍♀️", "pouting_woman", 1],
["🙎‍♂️", "pouting_man", 1],
["🙍", "frowning_woman", 1],
["🙍", "frowning_person", 1],
["🙍‍♀️", "frowning_woman", 1],
["🙍‍♂️", "frowning_man", 1],
["💇", "haircut_woman", 1],
["💇", "haircut_person", 1],
["💇‍♀️", "haircut_woman", 1],
["💇‍♂️", "haircut_man", 1],
["💆", "massage_woman", 1],
["💆", "massage_person", 1],
["💆‍♀️", "massage_woman", 1],
["💆‍♂️", "massage_man", 1],
["🧖", "person_in_steamy_room", 1],
["🧖‍♀️", "woman_in_steamy_room", 1],
["🧖‍♂️", "man_in_steamy_room", 1],
["🧏", "person_deaf", 1],
["🧏‍♀️", "woman_deaf", 1],
["🧏‍♂️", "man_deaf", 1],
["🧍", "person_standing", 1],
["🧍‍♀️", "woman_standing", 1],
["🧍‍♂️", "man_standing", 1],
["🧎", "person_kneeling", 1],
["🧎‍➡️", "person_kneeling_facing_right", 1],
["🧎‍♀️", "woman_kneeling", 1],
["🧎‍♀️‍➡️", "woman_kneeling_facing_right", 1],
["🧎‍♂️", "man_kneeling", 1],
["🧎‍♂️‍➡️", "man_kneeling_facing_right", 1],
["🧑‍🦯", "person_with_probing_cane", 1],
["🧑‍🦯‍➡️", "person_with_probing_cane_facing_right", 1],
["👩‍🦯", "woman_with_probing_cane", 1],
["👩‍🦯‍➡️", "woman_with_probing_cane_facing_right", 1],
["👨‍🦯", "man_with_probing_cane", 1],
["👨‍🦯‍➡️", "man_with_probing_cane_facing_right", 1],
["🧑‍🦼", "person_in_motorized_wheelchair", 1],
["🧑‍🦼‍➡️", "person_in_motorized_wheelchair_facing_right", 1],
["👩‍🦼", "woman_in_motorized_wheelchair", 1],
["👩‍🦼‍➡️", "woman_in_motorized_wheelchair_facing_right", 1],
["👨‍🦼", "man_in_motorized_wheelchair", 1],
["👨‍🦼‍➡️", "man_in_motorized_wheelchair_facing_right", 1],
["🧑‍🦽", "person_in_manual_wheelchair", 1],
["🧑‍🦽‍➡️", "person_in_manual_wheelchair_facing_right", 1],
["👩‍🦽", "woman_in_manual_wheelchair", 1],
["👩‍🦽‍➡️", "woman_in_manual_wheelchair_facing_right", 1],
["👨‍🦽", "man_in_manual_wheelchair", 1],
["💑", "couple_with_heart_woman_man", 1],
["👨‍🦽‍➡️", "man_in_manual_wheelchair_facing_right", 1],
["💑", "couple_with_heart", 1],
["👩‍❤️‍👨", "couple_with_heart_woman_man", 1],
["👩‍❤️‍👩", "couple_with_heart_woman_woman", 1],
["👨‍❤️‍👨", "couple_with_heart_man_man", 1],
["💏", "couplekiss_man_woman", 1],
["💏", "couplekiss", 1],
["👩‍❤️‍💋‍👨", "couplekiss_woman_man", 1],
["👩‍❤️‍💋‍👩", "couplekiss_woman_woman", 1],
["👨‍❤️‍💋‍👨", "couplekiss_man_man", 1],
["👪", "family_man_woman_boy", 1],
["👨‍👩‍👦", "family_man_woman_boy", 1],
["👨‍👩‍👧", "family_man_woman_girl", 1],
["👨‍👩‍👧‍👦", "family_man_woman_girl_boy", 1],
["👨‍👩‍👦‍👦", "family_man_woman_boy_boy", 1],
@@ -391,6 +453,11 @@
["👨‍👧‍👦", "family_man_girl_boy", 1],
["👨‍👦‍👦", "family_man_boy_boy", 1],
["👨‍👧‍👧", "family_man_girl_girl", 1],
["👪", "family", 1],
["🧑‍🧑‍🧒", "family_adult_adult_child", 1],
["🧑‍🧑‍🧒‍🧒", "family_adult_adult_child_child", 1],
["🧑‍🧒", "family_adult_child", 1],
["🧑‍🧒‍🧒", "family_adult_child_child", 1],
["🧶", "yarn", 1],
["🧵", "thread", 1],
["🧥", "coat", 1],
@@ -475,6 +542,7 @@
["🐦‍⬛", "black_bird", 2],
["🦅", "eagle", 2],
["🦉", "owl", 2],
["🐦‍🔥", "phoenix", 2],
["🦇", "bat", 2],
["🐺", "wolf", 2],
["🐗", "boar", 2],
@@ -575,6 +643,7 @@
["🌿", "herb", 2],
["☘", "shamrock", 2],
["🍀", "four_leaf_clover", 2],
["🪾", "leafless_tree", 2],
["🎍", "bamboo", 2],
["🎋", "tanabata_tree", 2],
["🍃", "leaves", 2],
@@ -648,6 +717,7 @@
["🪸", "coral", 2],
["🪹", "empty_nest", 2],
["🪺", "nest_with_eggs", 2],
["🍋‍🟩", "lime", 3],
["🍏", "green_apple", 3],
["🍎", "apple", 3],
["🍐", "pear", 3],
@@ -667,6 +737,8 @@
["🥑", "avocado", 3],
["🫛", "pea_pod", 3],
["🥦", "broccoli", 3],
["🍄‍🟫", "brown_mushroom", 3],
["🫜", "root_vegetable", 3],
["🍅", "tomato", 3],
["🍆", "eggplant", 3],
["🥒", "cucumber", 3],
@@ -786,8 +858,9 @@
["🥏", "flying_disc", 4],
["🎱", "8ball", 4],
["⛳", "golf", 4],
["🏌", "golfing_person", 4],
["🏌️‍♀️", "golfing_woman", 4],
["🏌", "golfing_man", 4],
["🏌‍♂️", "golfing_man", 4],
["🏓", "ping_pong", 4],
["🏸", "badminton", 4],
["🥅", "goal_net", 4],
@@ -799,10 +872,13 @@
["⛷", "skier", 4],
["🏂", "snowboarder", 4],
["🤺", "person_fencing", 4],
["🤼", "people_wrestling", 4],
["🤼‍♀️", "women_wrestling", 4],
["🤼‍♂️", "men_wrestling", 4],
["🤸", "person_cartwheeling", 4],
["🤸‍♀️", "woman_cartwheeling", 4],
["🤸‍♂️", "man_cartwheeling", 4],
["🤾", "person_playing_handball", 4],
["🤾‍♀️", "woman_playing_handball", 4],
["🤾‍♂️", "man_playing_handball", 4],
["⛸", "ice_skate", 4],
@@ -813,27 +889,37 @@
["🎣", "fishing_pole_and_fish", 4],
["🥊", "boxing_glove", 4],
["🥋", "martial_arts_uniform", 4],
["🚣", "rowing_person", 4],
["🚣‍♀️", "rowing_woman", 4],
["🚣", "rowing_man", 4],
["🚣‍♂️", "rowing_man", 4],
["🧗", "climbing_person", 4],
["🧗‍♀️", "climbing_woman", 4],
["🧗‍♂️", "climbing_man", 4],
["🏊", "swimming_person", 4],
["🏊‍♀️", "swimming_woman", 4],
["🏊", "swimming_man", 4],
["🏊‍♂️", "swimming_man", 4],
["🤽", "person_playing_water_polo", 4],
["🤽‍♀️", "woman_playing_water_polo", 4],
["🤽‍♂️", "man_playing_water_polo", 4],
["🧘", "person_in_lotus_position", 4],
["🧘‍♀️", "woman_in_lotus_position", 4],
["🧘‍♂️", "man_in_lotus_position", 4],
["🏄", "surfing_person", 4],
["🏄‍♀️", "surfing_woman", 4],
["🏄", "surfing_man", 4],
["🏄‍♂️", "surfing_man", 4],
["🛀", "bath", 4],
["⛹", "basketball_person", 4],
["⛹️‍♀️", "basketball_woman", 4],
["⛹", "basketball_man", 4],
["⛹‍♂️", "basketball_man", 4],
["🏋", "weight_lifting_person", 4],
["🏋️‍♀️", "weight_lifting_woman", 4],
["🏋", "weight_lifting_man", 4],
["🏋‍♂️", "weight_lifting_man", 4],
["🚴", "biking_person", 4],
["🚴‍♀️", "biking_woman", 4],
["🚴", "biking_man", 4],
["🚴‍♂️", "biking_man", 4],
["🚵", "mountain_biking_person", 4],
["🚵‍♀️", "mountain_biking_woman", 4],
["🚵", "mountain_biking_man", 4],
["🚵‍♂️", "mountain_biking_man", 4],
["🏇", "horse_racing", 4],
["🤿", "diving_mask", 4],
["🪀", "yo_yo", 4],
@@ -856,6 +942,7 @@
["🎭", "performing_arts", 4],
["🎨", "art", 4],
["🎪", "circus_tent", 4],
["🤹", "person_juggling", 4],
["🤹‍♀️", "woman_juggling", 4],
["🤹‍♂️", "man_juggling", 4],
["🎤", "microphone", 4],
@@ -872,6 +959,7 @@
["🪕", "banjo", 4],
["🪗", "accordion", 4],
["🪘", "long_drum", 4],
["🪉", "harp", 4],
["🎬", "clapper", 4],
["🎮", "video_game", 4],
["👾", "space_invader", 4],
@@ -1076,8 +1164,10 @@
["🪙", "coin", 6],
["💳", "credit_card", 6],
["🪪", "identification_card", 6],
["🥾", "hiking_boot", 6],
["💎", "gem", 6],
["⚖", "balance_scale", 6],
["⛓️‍💥", "broken_chain", 6],
["🧰", "toolbox", 6],
["🔧", "wrench", 6],
["🔨", "hammer", 6],
@@ -1093,6 +1183,7 @@
["🪛", "screwdriver", 6],
["🪝", "hook", 6],
["🪜", "ladder", 6],
["🪏", "shovel", 6],
["🧱", "brick", 6],
["⛓", "chains", 6],
["🧲", "magnet", 6],
@@ -1304,6 +1395,8 @@
["♓", "pisces", 7],
["🆔", "id", 7],
["⚛", "atom_symbol", 7],
["♀️", "female_sign", 7],
["♂️", "male_sign", 7],
["⚧️", "transgender_symbol", 7],
["🈳", "u7a7a", 7],
["🈹", "u5272", 7],
@@ -1463,9 +1556,11 @@
["♾", "infinity", 7],
["💲", "heavy_dollar_sign", 7],
["💱", "currency_exchange", 7],
["⚕️", "medical_symbol", 7],
["©️", "copyright", 7],
["®️", "registered", 7],
["™️", "tm", 7],
["🫟", "splatter", 7],
["🔚", "end", 7],
["🔙", "back", 7],
["🔛", "on", 7],
@@ -1576,6 +1671,7 @@
["🇧🇲", "bermuda", 8],
["🇧🇹", "bhutan", 8],
["🇧🇴", "bolivia", 8],
["🇧🇻", "bouvet_island", 8],
["🇧🇶", "caribbean_netherlands", 8],
["🇧🇦", "bosnia_herzegovina", 8],
["🇧🇼", "botswana", 8],
@@ -1593,10 +1689,12 @@
["🇮🇨", "canary_islands", 8],
["🇰🇾", "cayman_islands", 8],
["🇨🇫", "central_african_republic", 8],
["🇪🇦", "ceuta_melilla", 8],
["🇹🇩", "chad", 8],
["🇨🇱", "chile", 8],
["🇨🇳", "cn", 8],
["🇨🇽", "christmas_island", 8],
["🇨🇵", "clipperton_island", 8],
["🇨🇨", "cocos_islands", 8],
["🇨🇴", "colombia", 8],
["🇰🇲", "comoros", 8],
@@ -1610,6 +1708,7 @@
["🇨🇾", "cyprus", 8],
["🇨🇿", "czech_republic", 8],
["🇩🇰", "denmark", 8],
["🇩🇬", "diego_garcia", 8],
["🇩🇯", "djibouti", 8],
["🇩🇲", "dominica", 8],
["🇩🇴", "dominican_republic", 8],
@@ -1646,6 +1745,7 @@
["🇬🇼", "guinea_bissau", 8],
["🇬🇾", "guyana", 8],
["🇭🇹", "haiti", 8],
["🇭🇲", "heard_mcdonald_islands", 8],
["🇭🇳", "honduras", 8],
["🇭🇰", "hong_kong", 8],
["🇭🇺", "hungary", 8],
@@ -1733,10 +1833,12 @@
["🇷🇴", "romania", 8],
["🇷🇺", "ru", 8],
["🇷🇼", "rwanda", 8],
["🇨🇶", "sark", 8],
["🇧🇱", "st_barthelemy", 8],
["🇸🇭", "st_helena", 8],
["🇰🇳", "st_kitts_nevis", 8],
["🇱🇨", "st_lucia", 8],
["🇲🇫", "st_martin", 8],
["🇵🇲", "st_pierre_miquelon", 8],
["🇻🇨", "st_vincent_grenadines", 8],
["🇼🇸", "samoa", 8],
@@ -1762,6 +1864,7 @@
["🇸🇩", "sudan", 8],
["🇸🇷", "suriname", 8],
["🇸🇿", "swaziland", 8],
["🇸🇯", "svalbard_jan_mayen", 8],
["🇸🇪", "sweden", 8],
["🇨🇭", "switzerland", 8],
["🇸🇾", "syria", 8],
@@ -1788,6 +1891,7 @@
["🏴󠁧󠁢󠁳󠁣󠁴󠁿", "scotland", 8],
["🏴󠁧󠁢󠁷󠁬󠁳󠁿", "wales", 8],
["🇺🇸", "us", 8],
["🇺🇲", "us_outlying_islands", 8],
["🇻🇮", "us_virgin_islands", 8],
["🇺🇾", "uruguay", 8],
["🇺🇿", "uzbekistan", 8],
+1 -1
View File
@@ -34,7 +34,7 @@ export const commonHandlers = [
}),
http.get('/twemoji/:codepoints.svg', async ({ params }) => {
const { codepoints } = params;
const value = await fetch(`https://unpkg.com/@discordapp/twemoji@15.0.2/dist/svg/${codepoints}.svg`).then((response) => response.blob());
const value = await fetch(`https://unpkg.com/@discordapp/twemoji@16.0.1/dist/svg/${codepoints}.svg`).then((response) => response.blob());
return new HttpResponse(value, {
headers: {
'Content-Type': 'image/svg+xml',
@@ -39,6 +39,7 @@ export interface SearchIndexItem {
path?: string;
label: string;
keywords: string[];
texts: string[];
icon?: string;
inlining?: string[];
}
@@ -227,14 +228,14 @@ function extractElementText2Inner(node: TemplateChildNode, processingNodeName: s
// region extractUsageInfoFromTemplateAst
/**
* SearchLabel/SearchKeyword/SearchIconを探して抽出する関数
* SearchLabel/SearchText/SearchIconを探して抽出する関数
*/
function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: string | null, keywords: string[], icon: string | null } {
function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: string | null; texts: string[]; icon: string | null; } {
let label: string | null | undefined = undefined;
let icon: string | null | undefined = undefined;
const keywords: string[] = [];
const texts: string[] = [];
logger.info(`Extracting labels and keywords from ${nodes.length} nodes`);
logger.info(`Extracting labels and texts from ${nodes.length} nodes`);
walkVueElements(nodes, null, (node) => {
switch (node.tag) {
@@ -248,10 +249,10 @@ function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: stri
label = extractElementText(node, id);
return;
case 'SearchKeyword':
case 'SearchText':
const content = extractElementText(node, id);
if (content) {
keywords.push(content);
texts.push(content);
}
return;
case 'SearchIcon':
@@ -278,8 +279,8 @@ function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: stri
});
// デバッグ情報
logger.info(`Extraction completed: label=${label}, keywords=[${keywords.join(', ')}, icon=${icon}]`);
return { label: label ?? null, keywords, icon: icon ?? null };
logger.info(`Extraction completed: label=${label}, text=[${texts.join(', ')}, icon=${icon}]`);
return { label: label ?? null, texts, icon: icon ?? null };
}
function getStringProp(attr: AttributeNode | DirectiveNode | null, id: string): string | null {
@@ -351,33 +352,36 @@ function extractUsageInfoFromTemplateAst(
parentId: parentId ?? undefined,
label: '', // デフォルト値
keywords: [],
texts: [],
};
// バインドプロパティを取得
const path = getStringProp(findAttribute(node.props, 'path'), id)
const icon = getStringProp(findAttribute(node.props, 'icon'), id)
const label = getStringProp(findAttribute(node.props, 'label'), id)
const inlining = getStringArrayProp(findAttribute(node.props, 'inlining'), id)
const keywords = getStringArrayProp(findAttribute(node.props, 'keywords'), id)
const path = getStringProp(findAttribute(node.props, 'path'), id);
const icon = getStringProp(findAttribute(node.props, 'icon'), id);
const label = getStringProp(findAttribute(node.props, 'label'), id);
const inlining = getStringArrayProp(findAttribute(node.props, 'inlining'), id);
const keywords = getStringArrayProp(findAttribute(node.props, 'keywords'), id);
const texts = getStringArrayProp(findAttribute(node.props, 'texts'), id);
if (path) markerInfo.path = path;
if (icon) markerInfo.icon = icon;
if (label) markerInfo.label = label;
if (inlining) markerInfo.inlining = inlining;
if (keywords) markerInfo.keywords = keywords;
if (texts) markerInfo.texts = texts;
//pathがない場合はファイルパスを設定
// pathがない場合はファイルパスを設定
if (markerInfo.path == null && parentId == null) {
markerInfo.path = id.match(/.*(\/(admin|settings)\/[^\/]+)\.vue$/)?.[1];
}
// SearchLabelとSearchKeywordを抽出 (AST全体を探索)
// SearchLabelとSearchTextを抽出 (AST全体を探索)
{
const extracted = extractSugarTags(node.children, id);
if (extracted.label && markerInfo.label) logger.warn(`Duplicate label found for ${markerId} at ${id}:${node.loc.start.line}`);
if (extracted.icon && markerInfo.icon) logger.warn(`Duplicate icon found for ${markerId} at ${id}:${node.loc.start.line}`);
markerInfo.label = extracted.label ?? markerInfo.label ?? '';
markerInfo.keywords = [...extracted.keywords, ...markerInfo.keywords];
markerInfo.texts = [...extracted.texts, ...markerInfo.texts];
markerInfo.icon = extracted.icon ?? markerInfo.icon ?? undefined;
}
+3 -3
View File
@@ -17,7 +17,7 @@
},
"dependencies": {
"@analytics/google-analytics": "1.1.0",
"@discordapp/twemoji": "15.1.0",
"@discordapp/twemoji": "16.0.1",
"@github/webauthn-json": "2.1.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@misskey-dev/browser-image-resizer": "2024.1.0",
@@ -26,7 +26,7 @@
"@rollup/pluginutils": "5.2.0",
"@sentry/vue": "9.39.0",
"@syuilo/aiscript": "0.19.0",
"@twemoji/parser": "15.1.1",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "5.2.4",
"@vue/compiler-sfc": "3.5.17",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
@@ -54,7 +54,7 @@
"json5": "2.2.3",
"magic-string": "0.30.17",
"matter-js": "0.20.0",
"mfm-js": "0.24.0",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
+37 -28
View File
@@ -5,7 +5,7 @@
import { computed, watch, version as vueVersion } from 'vue';
import { compareVersions } from 'compare-versions';
import { version, lang, updateLocale, locale, apiUrl } from '@@/js/config.js';
import { version, lang, updateLocale, locale, apiUrl, isSafeMode } from '@@/js/config.js';
import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
import type { App } from 'vue';
@@ -168,28 +168,35 @@ export async function common(createVue: () => Promise<App<Element>>) {
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
watch(store.r.darkMode, (darkMode) => {
applyTheme(darkMode
? (prefer.s.darkTheme ?? defaultDarkTheme)
: (prefer.s.lightTheme ?? defaultLightTheme),
);
}, { immediate: miLocalStorage.getItem('theme') == null });
const theme = (() => {
if (darkMode) {
return isSafeMode ? defaultDarkTheme : (prefer.s.darkTheme ?? defaultDarkTheme);
} else {
return isSafeMode ? defaultLightTheme : (prefer.s.lightTheme ?? defaultLightTheme);
}
})();
applyTheme(theme);
}, { immediate: isSafeMode || miLocalStorage.getItem('theme') == null });
window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
const darkTheme = prefer.model('darkTheme');
const lightTheme = prefer.model('lightTheme');
if (!isSafeMode) {
const darkTheme = prefer.model('darkTheme');
const lightTheme = prefer.model('lightTheme');
watch(darkTheme, (theme) => {
if (store.s.darkMode) {
applyTheme(theme ?? defaultDarkTheme);
}
});
watch(darkTheme, (theme) => {
if (store.s.darkMode) {
applyTheme(theme ?? defaultDarkTheme);
}
});
watch(lightTheme, (theme) => {
if (!store.s.darkMode) {
applyTheme(theme ?? defaultLightTheme);
}
});
watch(lightTheme, (theme) => {
if (!store.s.darkMode) {
applyTheme(theme ?? defaultLightTheme);
}
});
}
//#region Sync dark mode
if (prefer.s.syncDeviceDarkMode) {
@@ -203,17 +210,19 @@ export async function common(createVue: () => Promise<App<Element>>) {
});
//#endregion
if (prefer.s.darkTheme && store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
} else if (prefer.s.lightTheme && !store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
}
if (!isSafeMode) {
if (prefer.s.darkTheme && store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
} else if (prefer.s.lightTheme && !store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
}
fetchInstanceMetaPromise.then(() => {
// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
});
fetchInstanceMetaPromise.then(() => {
// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
});
}
watch(prefer.r.overridedDeviceKind, (kind) => {
updateDeviceKind(kind);
+21 -1
View File
@@ -28,8 +28,8 @@ import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom
import { prefer } from '@/preferences.js';
import { launchPlugins } from '@/plugin.js';
import { updateCurrentAccountPartial } from '@/accounts.js';
import { signout } from '@/signout.js';
import { migrateOldSettings } from '@/pref-migrate.js';
import { unisonReload } from '@/utility/unison-reload.js';
export async function mainBoot() {
const { isClientUpdated, lastVersion } = await common(async () => {
@@ -391,6 +391,8 @@ export async function mainBoot() {
}
// shortcut
let safemodeRequestCount = 0;
let safemodeRequestTimer: number | null = null;
const keymap = {
'p|n': () => {
if ($i == null) return;
@@ -402,6 +404,24 @@ export async function mainBoot() {
's': () => {
mainRouter.push('/search');
},
'g': {
callback: () => {
// mを5回押すとセーフモードに入る
safemodeRequestCount++;
if (safemodeRequestCount >= 5) {
miLocalStorage.setItem('isSafeMode', 'true');
unisonReload();
} else {
if (safemodeRequestTimer != null) {
window.clearTimeout(safemodeRequestTimer);
}
safemodeRequestTimer = window.setTimeout(() => {
safemodeRequestCount = 0;
}, 300);
}
},
allowRepeat: true,
}
} as const satisfies Keymap;
window.document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@contextmenu.stop
@keydown.stop
>
<button v-if="hide" :class="$style.hidden" @click="show">
<button v-if="hide" :class="$style.hidden" @click="reveal">
<div :class="$style.hiddenTextWrapper">
<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
<b v-else style="display: block;"><i class="ti ti-music"></i> {{ prefer.s.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
@@ -157,7 +157,7 @@ const audioEl = useTemplateRef('audioEl');
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore'));
async function show() {
async function reveal() {
if (props.audio.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
const { canceled } = await os.confirm({
type: 'question',
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root">
<MkMediaAudio v-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/>
<div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="show">
<div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="reveal">
<span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
<b>{{ i18n.ts.sensitive }}</b>
<span>{{ i18n.ts.clickToShow }}</span>
@@ -37,7 +37,7 @@ const props = defineProps<{
const hide = ref(true);
async function show() {
async function reveal() {
if (props.media.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
const { canceled } = await os.confirm({
type: 'question',
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="onclick">
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="reveal">
<component
:is="disableImageLink ? 'div' : 'a'"
v-bind="disableImageLink ? {
@@ -96,10 +96,10 @@ const url = computed(() => (props.raw || prefer.s.loadRawImages)
? props.image.url
: prefer.s.disableShowingAnimatedImages
? getStaticImageUrl(props.image.url)
: props.image.thumbnailUrl,
: props.image.thumbnailUrl!,
);
async function onclick(ev: MouseEvent) {
async function reveal(ev: MouseEvent) {
if (!props.controls) {
return;
}
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@contextmenu.stop
@keydown.stop
>
<button v-if="hide" :class="$style.hidden" @click="show">
<button v-if="hide" :class="$style.hidden" @click="reveal">
<div :class="$style.hiddenTextWrapper">
<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ prefer.s.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
@@ -178,7 +178,7 @@ function hasFocus() {
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore'));
async function show() {
async function reveal() {
if (props.video.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
const { canceled } = await os.confirm({
type: 'question',
@@ -52,9 +52,9 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ item.label }}
</template>
<template v-else>
<span style="opacity: 0.7; font-size: 90%;">{{ item.parentLabels.join(' > ') }}</span>
<span style="opacity: 0.7; font-size: 90%; word-break: break-word;">{{ item.parentLabels.join(' > ') }}</span>
<br>
<span>{{ item.label }}</span>
<span style="word-break: break-word;">{{ item.label }}</span>
</template>
</span>
</MkA>
@@ -95,7 +95,7 @@ export type SuperMenuDef = {
<script lang="ts" setup>
import { useTemplateRef, ref, watch, nextTick, computed } from 'vue';
import { getScrollContainer } from '@@/js/scroll.js';
import type { SearchIndexItem } from '@/utility/settings-search-index.js';
import type { SearchIndexItem } from '@/utility/inapp-search.js';
import MkInput from '@/components/MkInput.vue';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
@@ -165,12 +165,28 @@ watch(rawSearchQuery, (value) => {
});
};
for (const item of searchIndexItemById.values()) {
if (
compareStringIncludes(item.label, value) ||
item.keywords.some((x) => compareStringIncludes(x, value))
) {
// label, keywords, texts
let items = Array.from(searchIndexItemById.values());
for (const item of items) {
if (compareStringIncludes(item.label, value)) {
addSearchResult(item);
items = items.filter(i => i.id !== item.id);
}
}
for (const item of items) {
if (item.keywords.some((x) => compareStringIncludes(x, value))) {
addSearchResult(item);
items = items.filter(i => i.id !== item.id);
}
}
for (const item of items) {
if (item.texts.some((x) => compareStringIncludes(x, value))) {
addSearchResult(item);
items = items.filter(i => i.id !== item.id);
}
}
}
@@ -0,0 +1,14 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<slot></slot>
</template>
<script lang="ts" setup>
</script>
<style lang="scss" module>
</style>
+3 -3
View File
@@ -31,7 +31,7 @@ import PageWithHeader from './global/PageWithHeader.vue';
import PageWithAnimBg from './global/PageWithAnimBg.vue';
import SearchMarker from './global/SearchMarker.vue';
import SearchLabel from './global/SearchLabel.vue';
import SearchKeyword from './global/SearchKeyword.vue';
import SearchText from './global/SearchText.vue';
import SearchIcon from './global/SearchIcon.vue';
import type { App } from 'vue';
@@ -71,7 +71,7 @@ export const components = {
PageWithAnimBg: PageWithAnimBg,
SearchMarker: SearchMarker,
SearchLabel: SearchLabel,
SearchKeyword: SearchKeyword,
SearchText: SearchText,
SearchIcon: SearchIcon,
};
@@ -105,7 +105,7 @@ declare module '@vue/runtime-core' {
PageWithAnimBg: typeof PageWithAnimBg;
SearchMarker: typeof SearchMarker;
SearchLabel: typeof SearchLabel;
SearchKeyword: typeof SearchKeyword;
SearchText: typeof SearchText;
SearchIcon: typeof SearchIcon;
}
}
+1
View File
@@ -33,6 +33,7 @@ export type Keys = (
'preferences' |
'latestPreferencesUpdate' |
'hidePreferencesRestoreSuggestion' |
'isSafeMode' |
`miux:${string}` |
`ui:folder:${string}` |
`themes:${string}` | // DEPRECATED
@@ -4,158 +4,161 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkFolder>
<template #icon><i class="ti ti-shield"></i></template>
<template #label>{{ i18n.ts.botProtection }}</template>
<template v-if="botProtectionForm.savedState.provider === 'hcaptcha'" #suffix>hCaptcha</template>
<template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template>
<template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template>
<template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template>
<template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
<template #footer>
<MkFormFooter :canSaving="canSaving" :form="botProtectionForm"/>
</template>
<SearchMarker markerId="botProtection" :keywords="['bot', 'protection', 'captcha', 'hcaptcha', 'mcaptcha', 'recaptcha', 'turnstile']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-shield"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.botProtection }}</SearchLabel></template>
<template v-if="botProtectionForm.savedState.provider === 'hcaptcha'" #suffix>hCaptcha</template>
<template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template>
<template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template>
<template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template>
<template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
<template #footer>
<MkFormFooter :canSaving="canSaving" :form="botProtectionForm"/>
</template>
<div class="_gaps_m">
<MkRadios v-model="botProtectionForm.state.provider">
<option value="none">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
<option value="hcaptcha">hCaptcha</option>
<option value="mcaptcha">mCaptcha</option>
<option value="recaptcha">reCAPTCHA</option>
<option value="turnstile">Turnstile</option>
<option value="testcaptcha">testCaptcha</option>
</MkRadios>
<div class="_gaps_m">
<MkRadios v-model="botProtectionForm.state.provider">
<option value="none">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
<option value="hcaptcha">hCaptcha</option>
<option value="mcaptcha">mCaptcha</option>
<option value="recaptcha">reCAPTCHA</option>
<option value="turnstile">Turnstile</option>
<option value="testcaptcha">testCaptcha</option>
</MkRadios>
<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
<MkInput v-model="botProtectionForm.state.hcaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.hcaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.hcaptchaSiteKey">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="hcaptcha"
:sitekey="botProtectionForm.state.hcaptchaSiteKey"
:secretKey="botProtectionForm.state.hcaptchaSecretKey"
/>
</FormSlot>
<MkInfo>
<div :class="$style.captchaInfoMsg">
<div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
<div>
<span>ref: </span><a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha Developer Guide</a>
<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
<MkInput v-model="botProtectionForm.state.hcaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.hcaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.hcaptchaSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.hcaptchaSiteKey">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="hcaptcha"
:sitekey="botProtectionForm.state.hcaptchaSiteKey"
:secretKey="botProtectionForm.state.hcaptchaSecretKey"
/>
</FormSlot>
<MkInfo>
<div :class="$style.captchaInfoMsg">
<div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
<div>
<span>ref: </span><a href="https://docs.hcaptcha.com/#integration-testing-test-keys" target="_blank">hCaptcha Developer Guide</a>
</div>
</div>
</div>
</MkInfo>
</template>
</MkInfo>
</template>
<template v-else-if="botProtectionForm.state.provider === 'mcaptcha'">
<MkInput v-model="botProtectionForm.state.mcaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.mcaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl" debounce>
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="mcaptcha"
:sitekey="botProtectionForm.state.mcaptchaSiteKey"
:secretKey="botProtectionForm.state.mcaptchaSecretKey"
:instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"
/>
</FormSlot>
</template>
<template v-else-if="botProtectionForm.state.provider === 'mcaptcha'">
<MkInput v-model="botProtectionForm.state.mcaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.mcaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.mcaptchaInstanceUrl" debounce>
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.mcaptchaSiteKey && botProtectionForm.state.mcaptchaInstanceUrl">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="mcaptcha"
:sitekey="botProtectionForm.state.mcaptchaSiteKey"
:secretKey="botProtectionForm.state.mcaptchaSecretKey"
:instanceUrl="botProtectionForm.state.mcaptchaInstanceUrl"
/>
</FormSlot>
</template>
<template v-else-if="botProtectionForm.state.provider === 'recaptcha'">
<MkInput v-model="botProtectionForm.state.recaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.recaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.recaptchaSiteKey">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="recaptcha"
:sitekey="botProtectionForm.state.recaptchaSiteKey"
:secretKey="botProtectionForm.state.recaptchaSecretKey"
/>
</FormSlot>
<MkInfo>
<div :class="$style.captchaInfoMsg">
<div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
<div>
<span>ref: </span>
<a
href="https://developers.google.com/recaptcha/docs/faq?hl=ja#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do"
target="_blank"
>reCAPTCHA FAQ</a>
<template v-else-if="botProtectionForm.state.provider === 'recaptcha'">
<MkInput v-model="botProtectionForm.state.recaptchaSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.recaptchaSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.recaptchaSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.recaptchaSiteKey">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="recaptcha"
:sitekey="botProtectionForm.state.recaptchaSiteKey"
:secretKey="botProtectionForm.state.recaptchaSecretKey"
/>
</FormSlot>
<MkInfo>
<div :class="$style.captchaInfoMsg">
<div>{{ i18n.ts._captcha.testSiteKeyMessage }}</div>
<div>
<span>ref: </span>
<a
href="https://developers.google.com/recaptcha/docs/faq?hl=ja#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do"
target="_blank"
>reCAPTCHA FAQ</a>
</div>
</div>
</div>
</MkInfo>
</template>
</MkInfo>
</template>
<template v-else-if="botProtectionForm.state.provider === 'turnstile'">
<MkInput v-model="botProtectionForm.state.turnstileSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.turnstileSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.turnstileSiteKey">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="turnstile"
:sitekey="botProtectionForm.state.turnstileSiteKey"
:secretKey="botProtectionForm.state.turnstileSecretKey"
/>
</FormSlot>
<MkInfo>
<div :class="$style.captchaInfoMsg">
<div>
{{ i18n.ts._captcha.testSiteKeyMessage }}
<template v-else-if="botProtectionForm.state.provider === 'turnstile'">
<MkInput v-model="botProtectionForm.state.turnstileSiteKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSiteKey }}</template>
</MkInput>
<MkInput v-model="botProtectionForm.state.turnstileSecretKey" debounce>
<template #prefix><i class="ti ti-key"></i></template>
<template #label>{{ i18n.ts.turnstileSecretKey }}</template>
</MkInput>
<FormSlot v-if="botProtectionForm.state.turnstileSiteKey">
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha
v-model="captchaResult"
provider="turnstile"
:sitekey="botProtectionForm.state.turnstileSiteKey"
:secretKey="botProtectionForm.state.turnstileSecretKey"
/>
</FormSlot>
<MkInfo>
<div :class="$style.captchaInfoMsg">
<div>
{{ i18n.ts._captcha.testSiteKeyMessage }}
</div>
<div>
<span>ref: </span><a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare Docs</a>
</div>
</div>
<div>
<span>ref: </span><a href="https://developers.cloudflare.com/turnstile/troubleshooting/testing/" target="_blank">Cloudflare Docs</a>
</div>
</div>
</MkInfo>
</template>
</MkInfo>
</template>
<template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
<MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
<FormSlot>
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha v-model="captchaResult" provider="testcaptcha" :sitekey="null"/>
</FormSlot>
</template>
</div>
</MkFolder>
<template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
<MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
<FormSlot>
<template #label>{{ i18n.ts._captcha.verify }}</template>
<MkCaptcha v-model="captchaResult" provider="testcaptcha" :sitekey="null"/>
</FormSlot>
</template>
</div>
</MkFolder>
</SearchMarker>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import type { ApiWithDialogCustomErrors } from '@/os.js';
import MkRadios from '@/components/MkRadios.vue';
import MkInput from '@/components/MkInput.vue';
import FormSlot from '@/components/form/slot.vue';
@@ -167,7 +170,6 @@ import { useForm } from '@/composables/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue';
import type { ApiWithDialogCustomErrors } from '@/os.js';
const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
+111 -100
View File
@@ -6,89 +6,117 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense :p="init">
<SearchMarker path="/admin/branding" :label="i18n.ts.branding" :keywords="['branding']" icon="ti ti-paint">
<div class="_gaps_m">
<MkInput v-model="iconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
</MkInput>
<SearchMarker :keywords="['icon', 'image']">
<MkInput v-model="iconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.iconUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="app192IconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
<template #caption>
<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '192x192px' }) }}</strong></div>
</template>
</MkInput>
<SearchMarker :keywords="['icon', 'image']">
<MkInput v-model="app192IconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</SearchLabel></template>
<template #caption>
<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '192x192px' }) }}</strong></div>
</template>
</MkInput>
</SearchMarker>
<MkInput v-model="app512IconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
<template #caption>
<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '512x512px' }) }}</strong></div>
</template>
</MkInput>
<SearchMarker :keywords="['icon', 'image']">
<MkInput v-model="app512IconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</SearchLabel></template>
<template #caption>
<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '512x512px' }) }}</strong></div>
</template>
</MkInput>
</SearchMarker>
<MkInput v-model="bannerUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.bannerUrl }}</template>
</MkInput>
<SearchMarker :keywords="['banner', 'image']">
<MkInput v-model="bannerUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.bannerUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="backgroundImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
</MkInput>
<SearchMarker :keywords="['background', 'image']">
<MkInput v-model="backgroundImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.backgroundImageUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="notFoundImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.notFoundDescription }}</template>
</MkInput>
<SearchMarker :keywords="['image']">
<MkInput v-model="notFoundImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.notFoundDescription }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="infoImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.nothing }}</template>
</MkInput>
<SearchMarker :keywords="['image']">
<MkInput v-model="infoImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.nothing }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="serverErrorImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.somethingHappened }}</template>
</MkInput>
<SearchMarker :keywords="['image']">
<MkInput v-model="serverErrorImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.somethingHappened }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkColorInput v-model="themeColor">
<template #label>{{ i18n.ts.themeColor }}</template>
</MkColorInput>
<SearchMarker :keywords="['theme', 'color']">
<MkColorInput v-model="themeColor">
<template #label><SearchLabel>{{ i18n.ts.themeColor }}</SearchLabel></template>
</MkColorInput>
</SearchMarker>
<MkTextarea v-model="defaultLightTheme">
<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
<SearchMarker :keywords="['theme', 'default', 'light']">
<MkTextarea v-model="defaultLightTheme">
<template #label><SearchLabel>{{ i18n.ts.instanceDefaultLightTheme }}</SearchLabel></template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
</SearchMarker>
<MkTextarea v-model="defaultDarkTheme">
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
<SearchMarker :keywords="['theme', 'default', 'dark']">
<MkTextarea v-model="defaultDarkTheme">
<template #label><SearchLabel>{{ i18n.ts.instanceDefaultDarkTheme }}</SearchLabel></template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
</SearchMarker>
<MkInput v-model="repositoryUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.repositoryUrl }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="repositoryUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.repositoryUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="feedbackUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.feedbackUrl }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="feedbackUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label><SearchLabel>{{ i18n.ts.feedbackUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkTextarea v-model="manifestJsonOverride">
<template #label>{{ i18n.ts._serverSettings.manifestJsonOverride }}</template>
</MkTextarea>
<SearchMarker>
<MkTextarea v-model="manifestJsonOverride">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.manifestJsonOverride }}</SearchLabel></template>
</MkTextarea>
</SearchMarker>
</div>
</FormSuspense>
</SearchMarker>
</div>
<template #footer>
<div :class="$style.footer">
@@ -106,7 +134,6 @@ import JSON5 from 'json5';
import { host } from '@@/js/config.js';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { instance, fetchInstance } from '@/instance.js';
@@ -115,38 +142,22 @@ import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
import MkColorInput from '@/components/MkColorInput.vue';
const iconUrl = ref<string | null>(null);
const app192IconUrl = ref<string | null>(null);
const app512IconUrl = ref<string | null>(null);
const bannerUrl = ref<string | null>(null);
const backgroundImageUrl = ref<string | null>(null);
const themeColor = ref<string | null>(null);
const defaultLightTheme = ref<string | null>(null);
const defaultDarkTheme = ref<string | null>(null);
const serverErrorImageUrl = ref<string | null>(null);
const infoImageUrl = ref<string | null>(null);
const notFoundImageUrl = ref<string | null>(null);
const repositoryUrl = ref<string | null>(null);
const feedbackUrl = ref<string | null>(null);
const manifestJsonOverride = ref<string>('{}');
const meta = await misskeyApi('admin/meta');
async function init() {
const meta = await misskeyApi('admin/meta');
iconUrl.value = meta.iconUrl;
app192IconUrl.value = meta.app192IconUrl;
app512IconUrl.value = meta.app512IconUrl;
bannerUrl.value = meta.bannerUrl;
backgroundImageUrl.value = meta.backgroundImageUrl;
themeColor.value = meta.themeColor;
defaultLightTheme.value = meta.defaultLightTheme;
defaultDarkTheme.value = meta.defaultDarkTheme;
serverErrorImageUrl.value = meta.serverErrorImageUrl;
infoImageUrl.value = meta.infoImageUrl;
notFoundImageUrl.value = meta.notFoundImageUrl;
repositoryUrl.value = meta.repositoryUrl;
feedbackUrl.value = meta.feedbackUrl;
manifestJsonOverride.value = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t');
}
const iconUrl = ref(meta.iconUrl);
const app192IconUrl = ref(meta.app192IconUrl);
const app512IconUrl = ref(meta.app512IconUrl);
const bannerUrl = ref(meta.bannerUrl);
const backgroundImageUrl = ref(meta.backgroundImageUrl);
const themeColor = ref(meta.themeColor);
const defaultLightTheme = ref(meta.defaultLightTheme);
const defaultDarkTheme = ref(meta.defaultDarkTheme);
const serverErrorImageUrl = ref(meta.serverErrorImageUrl);
const infoImageUrl = ref(meta.infoImageUrl);
const notFoundImageUrl = ref(meta.notFoundImageUrl);
const repositoryUrl = ref(meta.repositoryUrl);
const feedbackUrl = ref(meta.feedbackUrl);
const manifestJsonOverride = ref(meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t'));
function save() {
os.apiWithDialog('admin/update-meta', {
@@ -6,48 +6,67 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense :p="init">
<SearchMarker path="/admin/email-settings" :label="i18n.ts.emailServer" :keywords="['email']" icon="ti ti-mail">
<div class="_gaps_m">
<MkSwitch v-model="enableEmail">
<template #label>{{ i18n.ts.enableEmail }} ({{ i18n.ts.recommended }})</template>
<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
</MkSwitch>
<SearchMarker>
<MkSwitch v-model="enableEmail">
<template #label><SearchLabel>{{ i18n.ts.enableEmail }}</SearchLabel> ({{ i18n.ts.recommended }})</template>
<template #caption><SearchText>{{ i18n.ts.emailConfigInfo }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<template v-if="enableEmail">
<MkInput v-model="email" type="email">
<template #label>{{ i18n.ts.emailAddress }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="email" type="email">
<template #label><SearchLabel>{{ i18n.ts.emailAddress }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<FormSection>
<template #label>{{ i18n.ts.smtpConfig }}</template>
<SearchMarker>
<FormSection>
<template #label><SearchLabel>{{ i18n.ts.smtpConfig }}</SearchLabel></template>
<div class="_gaps_m">
<FormSplit :minWidth="280">
<MkInput v-model="smtpHost">
<template #label>{{ i18n.ts.smtpHost }}</template>
</MkInput>
<MkInput v-model="smtpPort" type="number">
<template #label>{{ i18n.ts.smtpPort }}</template>
</MkInput>
</FormSplit>
<FormSplit :minWidth="280">
<MkInput v-model="smtpUser">
<template #label>{{ i18n.ts.smtpUser }}</template>
</MkInput>
<MkInput v-model="smtpPass" type="password">
<template #label>{{ i18n.ts.smtpPass }}</template>
</MkInput>
</FormSplit>
<FormInfo>{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
<MkSwitch v-model="smtpSecure">
<template #label>{{ i18n.ts.smtpSecure }}</template>
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
</MkSwitch>
</div>
</FormSection>
<div class="_gaps_m">
<FormSplit :minWidth="280">
<SearchMarker>
<MkInput v-model="smtpHost">
<template #label><SearchLabel>{{ i18n.ts.smtpHost }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="smtpPort" type="number">
<template #label><SearchLabel>{{ i18n.ts.smtpPort }}</SearchLabel></template>
</MkInput>
</SearchMarker>
</FormSplit>
<FormSplit :minWidth="280">
<SearchMarker>
<MkInput v-model="smtpUser">
<template #label><SearchLabel>{{ i18n.ts.smtpUser }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="smtpPass" type="password">
<template #label><SearchLabel>{{ i18n.ts.smtpPass }}</SearchLabel></template>
</MkInput>
</SearchMarker>
</FormSplit>
<FormInfo>{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
<SearchMarker>
<MkSwitch v-model="smtpSecure">
<template #label><SearchLabel>{{ i18n.ts.smtpSecure }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.smtpSecureInfo }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</div>
</FormSection>
</SearchMarker>
</template>
</div>
</FormSuspense>
</SearchMarker>
</div>
<template #footer>
<div :class="$style.footer">
@@ -67,7 +86,6 @@ import { ref, computed } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import FormInfo from '@/components/MkInfo.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
@@ -77,24 +95,15 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
const enableEmail = ref<boolean>(false);
const email = ref<string | null>(null);
const smtpSecure = ref<boolean>(false);
const smtpHost = ref<string>('');
const smtpPort = ref<number>(0);
const smtpUser = ref<string>('');
const smtpPass = ref<string>('');
const meta = await misskeyApi('admin/meta');
async function init() {
const meta = await misskeyApi('admin/meta');
enableEmail.value = meta.enableEmail;
email.value = meta.email;
smtpSecure.value = meta.smtpSecure;
smtpHost.value = meta.smtpHost;
smtpPort.value = meta.smtpPort;
smtpUser.value = meta.smtpUser;
smtpPass.value = meta.smtpPass;
}
const enableEmail = ref(meta.enableEmail);
const email = ref(meta.email);
const smtpSecure = ref(meta.smtpSecure);
const smtpHost = ref(meta.smtpHost);
const smtpPort = ref(meta.smtpPort);
const smtpUser = ref(meta.smtpUser);
const smtpPass = ref(meta.smtpPass);
async function testEmail() {
const { canceled, result: destination } = await os.inputText({
@@ -6,36 +6,49 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense :p="init">
<SearchMarker path="/admin/external-services" :label="i18n.ts.externalServices" :keywords="['external', 'services', 'thirdparty']" icon="ti ti-link">
<div class="_gaps_m">
<MkFolder>
<template #label>Google Analytics<span class="_beta">{{ i18n.ts.beta }}</span></template>
<SearchMarker v-slot="slotProps">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>Google Analytics</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<div class="_gaps_m">
<MkInput v-model="googleAnalyticsMeasurementId">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Measurement ID</template>
</MkInput>
<MkButton primary @click="save_googleAnalytics">Save</MkButton>
</div>
</MkFolder>
<div class="_gaps_m">
<SearchMarker>
<MkInput v-model="googleAnalyticsMeasurementId">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>Measurement ID</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkFolder>
<template #label>DeepL Translation</template>
<MkButton primary @click="save_googleAnalytics">Save</MkButton>
</div>
</MkFolder>
</SearchMarker>
<div class="_gaps_m">
<MkInput v-model="deeplAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>DeepL Auth Key</template>
</MkInput>
<MkSwitch v-model="deeplIsPro">
<template #label>Pro account</template>
</MkSwitch>
<MkButton primary @click="save_deepl">Save</MkButton>
</div>
</MkFolder>
<SearchMarker v-slot="slotProps">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>DeepL Translation</SearchLabel></template>
<div class="_gaps_m">
<SearchMarker>
<MkInput v-model="deeplAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>Auth Key</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkSwitch v-model="deeplIsPro">
<template #label><SearchLabel>Pro account</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<MkButton primary @click="save_deepl">Save</MkButton>
</div>
</MkFolder>
</SearchMarker>
</div>
</FormSuspense>
</SearchMarker>
</div>
</PageWithHeader>
</template>
@@ -45,7 +58,6 @@ import { ref, computed } from 'vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { fetchInstance } from '@/instance.js';
@@ -53,17 +65,11 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkFolder from '@/components/MkFolder.vue';
const deeplAuthKey = ref<string>('');
const deeplIsPro = ref<boolean>(false);
const meta = await misskeyApi('admin/meta');
const googleAnalyticsMeasurementId = ref<string>('');
async function init() {
const meta = await misskeyApi('admin/meta');
deeplAuthKey.value = meta.deeplAuthKey ?? '';
deeplIsPro.value = meta.deeplIsPro;
googleAnalyticsMeasurementId.value = meta.googleAnalyticsMeasurementId ?? '';
}
const deeplAuthKey = ref(meta.deeplAuthKey ?? '');
const deeplIsPro = ref(meta.deeplIsPro);
const googleAnalyticsMeasurementId = ref(meta.googleAnalyticsMeasurementId ?? '');
function save_deepl() {
os.apiWithDialog('admin/update-meta', {
+4 -7
View File
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="noEmailServer" warn>{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
</div>
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
<MkSuperMenu :def="menuDef" :searchIndex="searchIndex" :grid="narrow"></MkSuperMenu>
</div>
</div>
</div>
@@ -44,6 +44,9 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { lookupUser, lookupUserByEmail, lookupFile } from '@/utility/admin-lookup.js';
import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import { useRouter } from '@/router.js';
import { genSearchIndexes } from '@/utility/inapp-search.js';
const searchIndex = await import('search-index:admin').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
const isEmpty = (x: string | null) => x == null || x === '';
@@ -324,12 +327,6 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePage(() => INFO.value);
defineExpose({
header: {
title: i18n.ts.controlPanel,
},
});
</script>
<style lang="scss" scoped>
+138 -129
View File
@@ -6,140 +6,162 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense :p="init">
<SearchMarker path="/admin/moderation" :label="i18n.ts.moderation" :keywords="['moderation']" icon="ti ti-shield" :inlining="['serverRules']">
<div class="_gaps_m">
<MkSwitch :modelValue="enableRegistration" @update:modelValue="onChange_enableRegistration">
<template #label>{{ i18n.ts._serverSettings.openRegistration }}</template>
<template #caption>
<div>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._serverSettings.openRegistrationWarning }}</div>
</template>
</MkSwitch>
<SearchMarker :keywords="['open', 'registration']">
<MkSwitch :modelValue="enableRegistration" @update:modelValue="onChange_enableRegistration">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.openRegistration }}</SearchLabel></template>
<template #caption>
<div><SearchText>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</SearchText></div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> <SearchText>{{ i18n.ts._serverSettings.openRegistrationWarning }}</SearchText></div>
</template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
<template #label>{{ i18n.ts.emailRequiredForSignup }} ({{ i18n.ts.recommended }})</template>
</MkSwitch>
<SearchMarker :keywords="['email', 'required', 'signup']">
<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
<template #label><SearchLabel>{{ i18n.ts.emailRequiredForSignup }}</SearchLabel> ({{ i18n.ts.recommended }})</template>
</MkSwitch>
</SearchMarker>
<MkSelect v-model="ugcVisibilityForVisitor" @update:modelValue="onChange_ugcVisibilityForVisitor">
<template #label>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor }}</template>
<option value="all">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.all }}</option>
<option value="local">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.localOnly }} ({{ i18n.ts.recommended }})</option>
<option value="none">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.none }}</option>
<template #caption>
<div>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description2 }}</div>
</template>
</MkSelect>
<SearchMarker :keywords="['ugc', 'content', 'visibility', 'visitor', 'guest']">
<MkSelect v-model="ugcVisibilityForVisitor" @update:modelValue="onChange_ugcVisibilityForVisitor">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor }}</SearchLabel></template>
<option value="all">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.all }}</option>
<option value="local">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.localOnly }} ({{ i18n.ts.recommended }})</option>
<option value="none">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.none }}</option>
<template #caption>
<div><SearchText>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description }}</SearchText></div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> <SearchText>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description2 }}</SearchText></div>
</template>
</MkSelect>
</SearchMarker>
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
<XServerRules/>
<MkFolder>
<template #icon><i class="ti ti-lock-star"></i></template>
<template #label>{{ i18n.ts.preservedUsernames }}</template>
<SearchMarker :keywords="['preserved', 'usernames']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-lock-star"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.preservedUsernames }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="preservedUsernames">
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_preservedUsernames">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<div class="_gaps">
<MkTextarea v-model="preservedUsernames">
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_preservedUsernames">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-message-exclamation"></i></template>
<template #label>{{ i18n.ts.sensitiveWords }}</template>
<SearchMarker :keywords="['sensitive', 'words']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-message-exclamation"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.sensitiveWords }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="sensitiveWords">
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_sensitiveWords">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<div class="_gaps">
<MkTextarea v-model="sensitiveWords">
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_sensitiveWords">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-message-x"></i></template>
<template #label>{{ i18n.ts.prohibitedWords }}</template>
<SearchMarker :keywords="['prohibited', 'words']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-message-x"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.prohibitedWords }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="prohibitedWords">
<template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_prohibitedWords">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<div class="_gaps">
<MkTextarea v-model="prohibitedWords">
<template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_prohibitedWords">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-user-x"></i></template>
<template #label>{{ i18n.ts.prohibitedWordsForNameOfUser }}</template>
<SearchMarker :keywords="['prohibited', 'name', 'user']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-user-x"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.prohibitedWordsForNameOfUser }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="prohibitedWordsForNameOfUser">
<template #caption>{{ i18n.ts.prohibitedWordsForNameOfUserDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_prohibitedWordsForNameOfUser">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<div class="_gaps">
<MkTextarea v-model="prohibitedWordsForNameOfUser">
<template #caption>{{ i18n.ts.prohibitedWordsForNameOfUserDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>
<MkButton primary @click="save_prohibitedWordsForNameOfUser">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.hiddenTags }}</template>
<SearchMarker :keywords="['hidden', 'tags', 'hashtags']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.hiddenTags }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="hiddenTags">
<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_hiddenTags">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<div class="_gaps">
<MkTextarea v-model="hiddenTags">
<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_hiddenTags">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.silencedInstances }}</template>
<SearchMarker :keywords="['silenced', 'servers', 'hosts']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.silencedInstances }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="silencedHosts">
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_silencedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<div class="_gaps">
<MkTextarea v-model="silencedHosts">
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_silencedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.mediaSilencedInstances }}</template>
<SearchMarker :keywords="['media', 'silenced', 'servers', 'hosts']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.mediaSilencedInstances }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="mediaSilencedHosts">
<template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_mediaSilencedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<div class="_gaps">
<MkTextarea v-model="mediaSilencedHosts">
<template #caption>{{ i18n.ts.mediaSilencedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_mediaSilencedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-ban"></i></template>
<template #label>{{ i18n.ts.blockedInstances }}</template>
<SearchMarker :keywords="['blocked', 'servers', 'hosts']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-ban"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.blockedInstances }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="blockedHosts">
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_blockedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<div class="_gaps">
<MkTextarea v-model="blockedHosts">
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save_blockedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
</div>
</FormSuspense>
</SearchMarker>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import XServerRules from './server-rules.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { fetchInstance } from '@/instance.js';
@@ -150,32 +172,19 @@ import FormLink from '@/components/form/link.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
const enableRegistration = ref<boolean>(false);
const emailRequiredForSignup = ref<boolean>(false);
const ugcVisibilityForVisitor = ref<string>('all');
const sensitiveWords = ref<string>('');
const prohibitedWords = ref<string>('');
const prohibitedWordsForNameOfUser = ref<string>('');
const hiddenTags = ref<string>('');
const preservedUsernames = ref<string>('');
const blockedHosts = ref<string>('');
const silencedHosts = ref<string>('');
const mediaSilencedHosts = ref<string>('');
const meta = await misskeyApi('admin/meta');
async function init() {
const meta = await misskeyApi('admin/meta');
enableRegistration.value = !meta.disableRegistration;
emailRequiredForSignup.value = meta.emailRequiredForSignup;
ugcVisibilityForVisitor.value = meta.ugcVisibilityForVisitor;
sensitiveWords.value = meta.sensitiveWords.join('\n');
prohibitedWords.value = meta.prohibitedWords.join('\n');
prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n');
hiddenTags.value = meta.hiddenTags.join('\n');
preservedUsernames.value = meta.preservedUsernames.join('\n');
blockedHosts.value = meta.blockedHosts.join('\n');
silencedHosts.value = meta.silencedHosts?.join('\n') ?? '';
mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
}
const enableRegistration = ref(!meta.disableRegistration);
const emailRequiredForSignup = ref(meta.emailRequiredForSignup);
const ugcVisibilityForVisitor = ref(meta.ugcVisibilityForVisitor);
const sensitiveWords = ref(meta.sensitiveWords.join('\n'));
const prohibitedWords = ref(meta.prohibitedWords.join('\n'));
const prohibitedWordsForNameOfUser = ref(meta.prohibitedWordsForNameOfUser.join('\n'));
const hiddenTags = ref(meta.hiddenTags.join('\n'));
const preservedUsernames = ref(meta.preservedUsernames.join('\n'));
const blockedHosts = ref(meta.blockedHosts.join('\n'));
const silencedHosts = ref(meta.silencedHosts?.join('\n') ?? '');
const mediaSilencedHosts = ref(meta.mediaSilencedHosts.join('\n'));
async function onChange_enableRegistration(value: boolean) {
if (value) {
@@ -6,70 +6,94 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense :p="init">
<SearchMarker path="/admin/object-storage" :label="i18n.ts.objectStorage" :keywords="['objectStorage']" icon="ti ti-cloud">
<div class="_gaps_m">
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
<SearchMarker>
<MkSwitch v-model="useObjectStorage"><SearchLabel>{{ i18n.ts.useObjectStorage }}</SearchLabel></MkSwitch>
</SearchMarker>
<template v-if="useObjectStorage">
<MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'" type="url">
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'" type="url">
<template #label><SearchLabel>{{ i18n.ts.objectStorageBaseUrl }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageBaseUrlDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<MkInput v-model="objectStorageBucket">
<template #label>{{ i18n.ts.objectStorageBucket }}</template>
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="objectStorageBucket">
<template #label><SearchLabel>{{ i18n.ts.objectStorageBucket }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageBucketDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<MkInput v-model="objectStoragePrefix">
<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="objectStoragePrefix">
<template #label><SearchLabel>{{ i18n.ts.objectStoragePrefix }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStoragePrefixDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<MkInput v-model="objectStorageEndpoint" :placeholder="'example.com'">
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
<template #prefix>https://</template>
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="objectStorageEndpoint" :placeholder="'example.com'">
<template #label><SearchLabel>{{ i18n.ts.objectStorageEndpoint }}</SearchLabel></template>
<template #prefix>https://</template>
<template #caption><SearchText>{{ i18n.ts.objectStorageEndpointDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<MkInput v-model="objectStorageRegion">
<template #label>{{ i18n.ts.objectStorageRegion }}</template>
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="objectStorageRegion">
<template #label><SearchLabel>{{ i18n.ts.objectStorageRegion }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageRegionDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<FormSplit :minWidth="280">
<MkInput v-model="objectStorageAccessKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Access key</template>
</MkInput>
<SearchMarker>
<MkInput v-model="objectStorageAccessKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>Access key</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkInput v-model="objectStorageSecretKey" type="password">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Secret key</template>
</MkInput>
<SearchMarker>
<MkInput v-model="objectStorageSecretKey" type="password">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>Secret key</SearchLabel></template>
</MkInput>
</SearchMarker>
</FormSplit>
<MkSwitch v-model="objectStorageUseSSL">
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
</MkSwitch>
<SearchMarker>
<MkSwitch v-model="objectStorageUseSSL">
<template #label><SearchLabel>{{ i18n.ts.objectStorageUseSSL }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageUseSSLDesc }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="objectStorageUseProxy">
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
</MkSwitch>
<SearchMarker>
<MkSwitch v-model="objectStorageUseProxy">
<template #label><SearchLabel>{{ i18n.ts.objectStorageUseProxy }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageUseProxyDesc }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="objectStorageSetPublicRead">
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
</MkSwitch>
<SearchMarker>
<MkSwitch v-model="objectStorageSetPublicRead">
<template #label><SearchLabel>{{ i18n.ts.objectStorageSetPublicRead }}</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="objectStorageS3ForcePathStyle">
<template #label>s3ForcePathStyle</template>
<template #caption>{{ i18n.ts.s3ForcePathStyleDesc }}</template>
</MkSwitch>
<SearchMarker>
<MkSwitch v-model="objectStorageS3ForcePathStyle">
<template #label><SearchLabel>s3ForcePathStyle</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.s3ForcePathStyleDesc }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</template>
</div>
</FormSuspense>
</SearchMarker>
</div>
<template #footer>
<div :class="$style.footer">
@@ -94,36 +118,21 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
const useObjectStorage = ref<boolean>(false);
const objectStorageBaseUrl = ref<string | null>(null);
const objectStorageBucket = ref<string | null>(null);
const objectStoragePrefix = ref<string | null>(null);
const objectStorageEndpoint = ref<string | null>(null);
const objectStorageRegion = ref<string | null>(null);
const objectStoragePort = ref<number | null>(null);
const objectStorageAccessKey = ref<string | null>(null);
const objectStorageSecretKey = ref<string | null>(null);
const objectStorageUseSSL = ref<boolean>(false);
const objectStorageUseProxy = ref<boolean>(false);
const objectStorageSetPublicRead = ref<boolean>(false);
const objectStorageS3ForcePathStyle = ref<boolean>(true);
const meta = await misskeyApi('admin/meta');
async function init() {
const meta = await misskeyApi('admin/meta');
useObjectStorage.value = meta.useObjectStorage;
objectStorageBaseUrl.value = meta.objectStorageBaseUrl;
objectStorageBucket.value = meta.objectStorageBucket;
objectStoragePrefix.value = meta.objectStoragePrefix;
objectStorageEndpoint.value = meta.objectStorageEndpoint;
objectStorageRegion.value = meta.objectStorageRegion;
objectStoragePort.value = meta.objectStoragePort;
objectStorageAccessKey.value = meta.objectStorageAccessKey;
objectStorageSecretKey.value = meta.objectStorageSecretKey;
objectStorageUseSSL.value = meta.objectStorageUseSSL;
objectStorageUseProxy.value = meta.objectStorageUseProxy;
objectStorageSetPublicRead.value = meta.objectStorageSetPublicRead;
objectStorageS3ForcePathStyle.value = meta.objectStorageS3ForcePathStyle;
}
const useObjectStorage = ref(meta.useObjectStorage);
const objectStorageBaseUrl = ref(meta.objectStorageBaseUrl);
const objectStorageBucket = ref(meta.objectStorageBucket);
const objectStoragePrefix = ref(meta.objectStoragePrefix);
const objectStorageEndpoint = ref(meta.objectStorageEndpoint);
const objectStorageRegion = ref(meta.objectStorageRegion);
const objectStoragePort = ref(meta.objectStoragePort);
const objectStorageAccessKey = ref(meta.objectStorageAccessKey);
const objectStorageSecretKey = ref(meta.objectStorageSecretKey);
const objectStorageUseSSL = ref(meta.objectStorageUseSSL);
const objectStorageUseProxy = ref(meta.objectStorageUseProxy);
const objectStorageSetPublicRead = ref(meta.objectStorageSetPublicRead);
const objectStorageS3ForcePathStyle = ref(meta.objectStorageS3ForcePathStyle);
function save() {
os.apiWithDialog('admin/update-meta', {
+148 -116
View File
@@ -6,131 +6,163 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<div class="_gaps">
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableServerMachineStats" @change="onChange_enableServerMachineStats">
<template #label>{{ i18n.ts.enableServerMachineStats }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableIdenticonGeneration" @change="onChange_enableIdenticonGeneration">
<template #label>{{ i18n.ts.enableIdenticonGeneration }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForRemoteUser" @change="onChange_enableChartsForRemoteUser">
<template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableStatsForFederatedInstances" @change="onChange_enableStatsForFederatedInstances">
<template #label>{{ i18n.ts.enableStatsForFederatedInstances }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances">
<template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-bolt"></i></template>
<template #label>Misskey® Fan-out Timeline Technology (FTT)</template>
<template v-if="fttForm.savedState.enableFanoutTimeline" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="fttForm.modified.value" #footer>
<MkFormFooter :form="fttForm"/>
</template>
<div class="_gaps">
<MkSwitch v-model="fttForm.state.enableFanoutTimeline">
<template #label>{{ i18n.ts.enable }}<span v-if="fttForm.modifiedStates.enableFanoutTimeline" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>
<div>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</div>
<div><MkLink target="_blank" url="https://misskey-hub.net/docs/for-admin/features/ftt/">{{ i18n.ts.details }}</MkLink></div>
</template>
</MkSwitch>
<template v-if="fttForm.state.enableFanoutTimeline">
<MkSwitch v-model="fttForm.state.enableFanoutTimelineDbFallback">
<template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}<span v-if="fttForm.modifiedStates.enableFanoutTimelineDbFallback" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template>
<SearchMarker path="/admin/performance" :label="i18n.ts.performance" :keywords="['performance']" icon="ti ti-bolt">
<div class="_gaps">
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableServerMachineStats" @change="onChange_enableServerMachineStats">
<template #label><SearchLabel>{{ i18n.ts.enableServerMachineStats }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<MkInput v-model="fttForm.state.perLocalUserUserTimelineCacheMax" type="number">
<template #label>perLocalUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perLocalUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableIdenticonGeneration" @change="onChange_enableIdenticonGeneration">
<template #label><SearchLabel>{{ i18n.ts.enableIdenticonGeneration }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<MkInput v-model="fttForm.state.perRemoteUserUserTimelineCacheMax" type="number">
<template #label>perRemoteUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perRemoteUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForRemoteUser" @change="onChange_enableChartsForRemoteUser">
<template #label><SearchLabel>{{ i18n.ts.enableChartsForRemoteUser }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<MkInput v-model="fttForm.state.perUserHomeTimelineCacheMax" type="number">
<template #label>perUserHomeTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserHomeTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableStatsForFederatedInstances" @change="onChange_enableStatsForFederatedInstances">
<template #label><SearchLabel>{{ i18n.ts.enableStatsForFederatedInstances }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<MkInput v-model="fttForm.state.perUserListTimelineCacheMax" type="number">
<template #label>perUserListTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserListTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</template>
</div>
</MkFolder>
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances">
<template #label><SearchLabel>{{ i18n.ts.enableChartsForFederatedInstances }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-bolt"></i></template>
<template #label>Misskey® Reactions Boost Technology (RBT)<span class="_beta">{{ i18n.ts.beta }}</span></template>
<template v-if="rbtForm.savedState.enableReactionsBuffering" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="rbtForm.modified.value" #footer>
<MkFormFooter :form="rbtForm"/>
</template>
<SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><SearchIcon><i class="ti ti-bolt"></i></SearchIcon></template>
<template #label><SearchLabel>Misskey® Fan-out Timeline Technology (FTT)</SearchLabel></template>
<template v-if="fttForm.savedState.enableFanoutTimeline" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="fttForm.modified.value" #footer>
<MkFormFooter :form="fttForm"/>
</template>
<div class="_gaps_m">
<MkSwitch v-model="rbtForm.state.enableReactionsBuffering">
<template #label>{{ i18n.ts.enable }}<span v-if="rbtForm.modifiedStates.enableReactionsBuffering" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.reactionsBufferingDescription }}</template>
</MkSwitch>
</div>
</MkFolder>
<div class="_gaps">
<SearchMarker>
<MkSwitch v-model="fttForm.state.enableFanoutTimeline">
<template #label><SearchLabel>{{ i18n.ts.enable }}</SearchLabel><span v-if="fttForm.modifiedStates.enableFanoutTimeline" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>
<div><SearchText>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</SearchText></div>
<div><MkLink target="_blank" url="https://misskey-hub.net/docs/for-admin/features/ftt/">{{ i18n.ts.details }}</MkLink></div>
</template>
</MkSwitch>
</SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-recycle"></i></template>
<template #label>Remote Notes Cleaning ()</template>
<template v-if="remoteNotesCleaningForm.savedState.enableRemoteNotesCleaning" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="remoteNotesCleaningForm.modified.value" #footer>
<MkFormFooter :form="remoteNotesCleaningForm"/>
</template>
<template v-if="fttForm.state.enableFanoutTimeline">
<SearchMarker :keywords="['db', 'database', 'fallback']">
<MkSwitch v-model="fttForm.state.enableFanoutTimelineDbFallback">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}</SearchLabel><span v-if="fttForm.modifiedStates.enableFanoutTimelineDbFallback" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<div class="_gaps_m">
<MkSwitch v-model="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
<template #label>{{ i18n.ts.enable }}<span v-if="remoteNotesCleaningForm.modifiedStates.enableRemoteNotesCleaning" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.remoteNotesCleaning_description }}</template>
</MkSwitch>
<SearchMarker>
<MkInput v-model="fttForm.state.perLocalUserUserTimelineCacheMax" type="number">
<template #label><SearchLabel>perLocalUserUserTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perLocalUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<template v-if="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
<MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningExpiryDaysForEachNotes" type="number">
<template #label>{{ i18n.ts._serverSettings.remoteNotesCleaningExpiryDaysForEachNotes }} ({{ i18n.ts.inDays }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningExpiryDaysForEachNotes" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #suffix>{{ i18n.ts._time.day }}</template>
</MkInput>
<SearchMarker>
<MkInput v-model="fttForm.state.perRemoteUserUserTimelineCacheMax" type="number">
<template #label><SearchLabel>perRemoteUserUserTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perRemoteUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningMaxProcessingDurationInMinutes" type="number">
<template #label>{{ i18n.ts._serverSettings.remoteNotesCleaningMaxProcessingDuration }} ({{ i18n.ts.inMinutes }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningMaxProcessingDurationInMinutes" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #suffix>{{ i18n.ts._time.minute }}</template>
</MkInput>
</template>
</div>
</MkFolder>
</div>
<SearchMarker>
<MkInput v-model="fttForm.state.perUserHomeTimelineCacheMax" type="number">
<template #label><SearchLabel>perUserHomeTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perUserHomeTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="fttForm.state.perUserListTimelineCacheMax" type="number">
<template #label><SearchLabel>perUserListTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perUserListTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
</template>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><SearchIcon><i class="ti ti-bolt"></i></SearchIcon></template>
<template #label><SearchLabel>Misskey® Reactions Boost Technology (RBT)</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<template v-if="rbtForm.savedState.enableReactionsBuffering" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="rbtForm.modified.value" #footer>
<MkFormFooter :form="rbtForm"/>
</template>
<div class="_gaps_m">
<SearchMarker>
<MkSwitch v-model="rbtForm.state.enableReactionsBuffering">
<template #label><SearchLabel>{{ i18n.ts.enable }}</SearchLabel><span v-if="rbtForm.modifiedStates.enableReactionsBuffering" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.reactionsBufferingDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><SearchIcon><i class="ti ti-recycle"></i></SearchIcon></template>
<template #label><SearchLabel>Remote Notes Cleaning ()</SearchLabel></template>
<template v-if="remoteNotesCleaningForm.savedState.enableRemoteNotesCleaning" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="remoteNotesCleaningForm.modified.value" #footer>
<MkFormFooter :form="remoteNotesCleaningForm"/>
</template>
<div class="_gaps_m">
<MkSwitch v-model="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
<template #label><SearchLabel>{{ i18n.ts.enable }}</SearchLabel><span v-if="remoteNotesCleaningForm.modifiedStates.enableRemoteNotesCleaning" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.remoteNotesCleaning_description }}</SearchText></template>
</MkSwitch>
<template v-if="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
<MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningExpiryDaysForEachNotes" type="number">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.remoteNotesCleaningExpiryDaysForEachNotes }}</SearchLabel> ({{ i18n.ts.inDays }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningExpiryDaysForEachNotes" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #suffix>{{ i18n.ts._time.day }}</template>
</MkInput>
<MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningMaxProcessingDurationInMinutes" type="number">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.remoteNotesCleaningMaxProcessingDuration }}</SearchLabel> ({{ i18n.ts.inMinutes }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningMaxProcessingDurationInMinutes" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #suffix>{{ i18n.ts._time.minute }}</template>
</MkInput>
</template>
</div>
</MkFolder>
</SearchMarker>
</div>
</SearchMarker>
</div>
</PageWithHeader>
</template>
@@ -243,7 +275,7 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePage(() => ({
title: i18n.ts.other,
icon: 'ti ti-adjustments',
title: i18n.ts.performance,
icon: 'ti ti-bolt',
}));
</script>
+12 -10
View File
@@ -6,18 +6,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 800px;">
<div class="_gaps">
<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;">
<div>{{ relay.inbox }}</div>
<div style="margin: 8px 0;">
<i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--MI_THEME-success);"></i>
<i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--MI_THEME-error);"></i>
<i v-else class="ti ti-clock" :class="$style.icon"></i>
<span>{{ i18n.ts._relayStatus[relay.status] }}</span>
<SearchMarker path="/admin/relays" :label="i18n.ts.relays" :keywords="['relays']" icon="ti ti-planet">
<div class="_gaps">
<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;">
<div>{{ relay.inbox }}</div>
<div style="margin: 8px 0;">
<i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--MI_THEME-success);"></i>
<i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--MI_THEME-error);"></i>
<i v-else class="ti ti-clock" :class="$style.icon"></i>
<span>{{ i18n.ts._relayStatus[relay.status] }}</span>
</div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
</div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</div>
</SearchMarker>
</div>
</PageWithHeader>
</template>
+130 -92
View File
@@ -6,115 +6,153 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<div class="_gaps_m">
<XBotProtection/>
<SearchMarker path="/admin/security" :label="i18n.ts.security" :keywords="['security']" icon="ti ti-lock" :inlining="['botProtection']">
<div class="_gaps_m">
<XBotProtection/>
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.sensitiveMediaDetection }}</template>
<template v-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
<template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'local'" #suffix>{{ i18n.ts.localOnly }}</template>
<template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'remote'" #suffix>{{ i18n.ts.remoteOnly }}</template>
<template v-else #suffix>{{ i18n.ts.none }}</template>
<template v-if="sensitiveMediaDetectionForm.modified.value" #footer>
<MkFormFooter :form="sensitiveMediaDetectionForm"/>
</template>
<SearchMarker v-slot="slotProps" :keywords="['sensitive', 'media', 'detection']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.sensitiveMediaDetection }}</SearchLabel></template>
<template v-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
<template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'local'" #suffix>{{ i18n.ts.localOnly }}</template>
<template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'remote'" #suffix>{{ i18n.ts.remoteOnly }}</template>
<template v-else #suffix>{{ i18n.ts.none }}</template>
<template v-if="sensitiveMediaDetectionForm.modified.value" #footer>
<MkFormFooter :form="sensitiveMediaDetectionForm"/>
</template>
<div class="_gaps_m">
<span>{{ i18n.ts._sensitiveMediaDetection.description }}</span>
<div class="_gaps_m">
<div><SearchText>{{ i18n.ts._sensitiveMediaDetection.description }}</SearchText></div>
<MkRadios v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection">
<option value="none">{{ i18n.ts.none }}</option>
<option value="all">{{ i18n.ts.all }}</option>
<option value="local">{{ i18n.ts.localOnly }}</option>
<option value="remote">{{ i18n.ts.remoteOnly }}</option>
</MkRadios>
<MkRadios v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection">
<option value="none">{{ i18n.ts.none }}</option>
<option value="all">{{ i18n.ts.all }}</option>
<option value="local">{{ i18n.ts.localOnly }}</option>
<option value="remote">{{ i18n.ts.remoteOnly }}</option>
</MkRadios>
<MkRange v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :textConverter="(v) => `${v + 1}`">
<template #label>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</template>
<template #caption>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</template>
</MkRange>
<SearchMarker :keywords="['sensitivity']">
<MkRange v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :textConverter="(v) => `${v + 1}`">
<template #label><SearchLabel>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</SearchText></template>
</MkRange>
</SearchMarker>
<MkSwitch v-model="sensitiveMediaDetectionForm.state.enableSensitiveMediaDetectionForVideos">
<template #label>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</template>
</MkSwitch>
<SearchMarker :keywords="['video', 'analyze']">
<MkSwitch v-model="sensitiveMediaDetectionForm.state.enableSensitiveMediaDetectionForVideos">
<template #label><SearchLabel>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption><SearchText>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<MkSwitch v-model="sensitiveMediaDetectionForm.state.setSensitiveFlagAutomatically">
<template #label>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }} ({{ i18n.ts.notRecommended }})</template>
<template #caption>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</template>
</MkSwitch>
<SearchMarker :keywords="['flag', 'automatically']">
<MkSwitch v-model="sensitiveMediaDetectionForm.state.setSensitiveFlagAutomatically">
<template #label><SearchLabel>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }}</SearchLabel> ({{ i18n.ts.notRecommended }})</template>
<template #caption><SearchText>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<!-- 現状 false positive が多すぎて実用に耐えない
<!-- 現状 false positive が多すぎて実用に耐えない
<MkSwitch v-model="disallowUploadWhenPredictedAsPorn">
<template #label>{{ i18n.ts._sensitiveMediaDetection.disallowUploadWhenPredictedAsPorn }}</template>
</MkSwitch>
-->
</div>
</MkFolder>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #label>Active Email Validation</template>
<template v-if="emailValidationForm.savedState.enableActiveEmailValidation" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="emailValidationForm.modified.value" #footer>
<MkFormFooter :form="emailValidationForm"/>
</template>
<SearchMarker v-slot="slotProps" :keywords="['email', 'validation']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>Active Email Validation</SearchLabel></template>
<template v-if="emailValidationForm.savedState.enableActiveEmailValidation" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="emailValidationForm.modified.value" #footer>
<MkFormFooter :form="emailValidationForm"/>
</template>
<div class="_gaps_m">
<span>{{ i18n.ts.activeEmailValidationDescription }}</span>
<MkSwitch v-model="emailValidationForm.state.enableActiveEmailValidation">
<template #label>Enable</template>
</MkSwitch>
<MkSwitch v-model="emailValidationForm.state.enableVerifymailApi">
<template #label>Use Verifymail.io API</template>
</MkSwitch>
<MkInput v-model="emailValidationForm.state.verifymailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Verifymail.io API Auth Key</template>
</MkInput>
<MkSwitch v-model="emailValidationForm.state.enableTruemailApi">
<template #label>Use TrueMail API</template>
</MkSwitch>
<MkInput v-model="emailValidationForm.state.truemailInstance">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>TrueMail API Instance</template>
</MkInput>
<MkInput v-model="emailValidationForm.state.truemailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>TrueMail API Auth Key</template>
</MkInput>
</div>
</MkFolder>
<div class="_gaps_m">
<div><SearchText>{{ i18n.ts.activeEmailValidationDescription }}</SearchText></div>
<MkFolder>
<template #label>Banned Email Domains</template>
<template v-if="bannedEmailDomainsForm.modified.value" #footer>
<MkFormFooter :form="bannedEmailDomainsForm"/>
</template>
<SearchMarker>
<MkSwitch v-model="emailValidationForm.state.enableActiveEmailValidation">
<template #label><SearchLabel>Enable</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<div class="_gaps_m">
<MkTextarea v-model="bannedEmailDomainsForm.state.bannedEmailDomains">
<template #label>Banned Email Domains List</template>
</MkTextarea>
</div>
</MkFolder>
<SearchMarker>
<MkSwitch v-model="emailValidationForm.state.enableVerifymailApi">
<template #label><SearchLabel>Use Verifymail.io API</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<MkFolder>
<template #label>Log IP address</template>
<template v-if="ipLoggingForm.savedState.enableIpLogging" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="ipLoggingForm.modified.value" #footer>
<MkFormFooter :form="ipLoggingForm"/>
</template>
<SearchMarker>
<MkInput v-model="emailValidationForm.state.verifymailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>Verifymail.io API Auth Key</SearchLabel></template>
</MkInput>
</SearchMarker>
<div class="_gaps_m">
<MkSwitch v-model="ipLoggingForm.state.enableIpLogging">
<template #label>Enable</template>
</MkSwitch>
</div>
</MkFolder>
</div>
<SearchMarker>
<MkSwitch v-model="emailValidationForm.state.enableTruemailApi">
<template #label><SearchLabel>Use TrueMail API</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<SearchMarker>
<MkInput v-model="emailValidationForm.state.truemailInstance">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>TrueMail API Instance</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="emailValidationForm.state.truemailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label><SearchLabel>TrueMail API Auth Key</SearchLabel></template>
</MkInput>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['banned', 'email', 'domains', 'blacklist']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>Banned Email Domains</SearchLabel></template>
<template v-if="bannedEmailDomainsForm.modified.value" #footer>
<MkFormFooter :form="bannedEmailDomainsForm"/>
</template>
<div class="_gaps_m">
<SearchMarker>
<MkTextarea v-model="bannedEmailDomainsForm.state.bannedEmailDomains">
<template #label><SearchLabel>Banned Email Domains List</SearchLabel></template>
</MkTextarea>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['log', 'ipAddress']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>Log IP address</SearchLabel></template>
<template v-if="ipLoggingForm.savedState.enableIpLogging" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="ipLoggingForm.modified.value" #footer>
<MkFormFooter :form="ipLoggingForm"/>
</template>
<div class="_gaps_m">
<SearchMarker>
<MkSwitch v-model="ipLoggingForm.state.enableIpLogging">
<template #label><SearchLabel>Enable</SearchLabel></template>
</MkSwitch>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
</div>
</SearchMarker>
</div>
</PageWithHeader>
</template>
@@ -4,10 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<SearchMarker markerId="serverRules" :keywords="['rules']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-checkbox"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.serverRules }}</SearchLabel></template>
<div class="_gaps_m">
<div>{{ i18n.ts._serverRules.description }}</div>
<div><SearchText>{{ i18n.ts._serverRules.description }}</SearchText></div>
<Sortable
v-model="serverRules"
class="_gaps_m"
@@ -33,8 +37,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</div>
</div>
</PageWithHeader>
</MkFolder>
</SearchMarker>
</template>
<script lang="ts" setup>
@@ -42,9 +46,9 @@ import { defineAsyncComponent, ref, computed } from 'vue';
import * as os from '@/os.js';
import { fetchInstance, instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkFolder from '@/components/MkFolder.vue';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -60,13 +64,6 @@ const save = async () => {
const remove = (index: number): void => {
serverRules.value.splice(index, 1);
};
const headerTabs = computed(() => []);
definePage(() => ({
title: i18n.ts.serverRules,
icon: 'ti ti-checkbox',
}));
</script>
<style lang="scss" module>
+350 -273
View File
@@ -6,292 +6,369 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<div class="_gaps_m">
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-info-circle"></i></template>
<template #label>{{ i18n.ts.info }}</template>
<template v-if="infoForm.modified.value" #footer>
<MkFormFooter :form="infoForm"/>
</template>
<div class="_gaps">
<MkInput v-model="infoForm.state.name">
<template #label>{{ i18n.ts.instanceName }}<span v-if="infoForm.modifiedStates.name" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<MkInput v-model="infoForm.state.shortName">
<template #label>{{ i18n.ts._serverSettings.shortName }} ({{ i18n.ts.optional }})<span v-if="infoForm.modifiedStates.shortName" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.shortNameDescription }}</template>
</MkInput>
<MkTextarea v-model="infoForm.state.description">
<template #label>{{ i18n.ts.instanceDescription }}<span v-if="infoForm.modifiedStates.description" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkTextarea>
<FormSplit :minWidth="300">
<MkInput v-model="infoForm.state.maintainerName">
<template #label>{{ i18n.ts.maintainerName }}<span v-if="infoForm.modifiedStates.maintainerName" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
<MkInput v-model="infoForm.state.maintainerEmail" type="email">
<template #label>{{ i18n.ts.maintainerEmail }}<span v-if="infoForm.modifiedStates.maintainerEmail" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-mail"></i></template>
</MkInput>
</FormSplit>
<MkInput v-model="infoForm.state.tosUrl" type="url">
<template #label>{{ i18n.ts.tosUrl }}<span v-if="infoForm.modifiedStates.tosUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
<MkInput v-model="infoForm.state.privacyPolicyUrl" type="url">
<template #label>{{ i18n.ts.privacyPolicyUrl }}<span v-if="infoForm.modifiedStates.privacyPolicyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
<MkInput v-model="infoForm.state.inquiryUrl" type="url">
<template #label>{{ i18n.ts._serverSettings.inquiryUrl }}<span v-if="infoForm.modifiedStates.inquiryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
<MkInput v-model="infoForm.state.repositoryUrl" type="url">
<template #label>{{ i18n.ts.repositoryUrl }}<span v-if="infoForm.modifiedStates.repositoryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.repositoryUrlDescription }}</template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
<MkInfo v-if="!instance.providesTarball && !infoForm.state.repositoryUrl" warn>
{{ i18n.ts.repositoryUrlOrTarballRequired }}
</MkInfo>
<MkInput v-model="infoForm.state.impressumUrl" type="url">
<template #label>{{ i18n.ts.impressumUrl }}<span v-if="infoForm.modifiedStates.impressumUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.impressumDescription }}</template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-user-star"></i></template>
<template #label>{{ i18n.ts.pinnedUsers }}</template>
<template v-if="pinnedUsersForm.modified.value" #footer>
<MkFormFooter :form="pinnedUsersForm"/>
</template>
<MkTextarea v-model="pinnedUsersForm.state.pinnedUsers">
<template #label>{{ i18n.ts.pinnedUsers }}<span v-if="pinnedUsersForm.modifiedStates.pinnedUsers" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
</MkTextarea>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-world-cog"></i></template>
<template #label>ServiceWorker</template>
<template v-if="serviceWorkerForm.modified.value" #footer>
<MkFormFooter :form="serviceWorkerForm"/>
</template>
<div class="_gaps">
<MkSwitch v-model="serviceWorkerForm.state.enableServiceWorker">
<template #label>{{ i18n.ts.enableServiceworker }}<span v-if="serviceWorkerForm.modifiedStates.enableServiceWorker" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
</MkSwitch>
<template v-if="serviceWorkerForm.state.enableServiceWorker">
<MkInput v-model="serviceWorkerForm.state.swPublicKey">
<template #label>Public key<span v-if="serviceWorkerForm.modifiedStates.swPublicKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
<MkInput v-model="serviceWorkerForm.state.swPrivateKey">
<template #label>Private key<span v-if="serviceWorkerForm.modifiedStates.swPrivateKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
</template>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-ad"></i></template>
<template #label>{{ i18n.ts._ad.adsSettings }}</template>
<template v-if="adForm.modified.value" #footer>
<MkFormFooter :form="adForm"/>
</template>
<div class="_gaps">
<div class="_gaps_s">
<MkInput v-model="adForm.state.notesPerOneAd" :min="0" type="number">
<template #label>{{ i18n.ts._ad.notesPerOneAd }}<span v-if="adForm.modifiedStates.notesPerOneAd" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template>
</MkInput>
<MkInfo v-if="adForm.state.notesPerOneAd > 0 && adForm.state.notesPerOneAd < 20" :warn="true">
{{ i18n.ts._ad.adsTooClose }}
</MkInfo>
</div>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-world-search"></i></template>
<template #label>{{ i18n.ts._urlPreviewSetting.title }}</template>
<template v-if="urlPreviewForm.modified.value" #footer>
<MkFormFooter :form="urlPreviewForm"/>
</template>
<div class="_gaps">
<MkSwitch v-model="urlPreviewForm.state.urlPreviewEnabled">
<template #label>{{ i18n.ts._urlPreviewSetting.enable }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewEnabled" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkSwitch>
<template v-if="urlPreviewForm.state.urlPreviewEnabled">
<MkSwitch v-model="urlPreviewForm.state.urlPreviewAllowRedirect">
<template #label>{{ i18n.ts._urlPreviewSetting.allowRedirect }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewAllowRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.allowRedirectDescription }}</template>
</MkSwitch>
<MkSwitch v-model="urlPreviewForm.state.urlPreviewRequireContentLength">
<template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewRequireContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template>
</MkSwitch>
<MkInput v-model="urlPreviewForm.state.urlPreviewMaximumContentLength" type="number">
<template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewMaximumContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template>
</MkInput>
<MkInput v-model="urlPreviewForm.state.urlPreviewTimeout" type="number">
<template #label>{{ i18n.ts._urlPreviewSetting.timeout }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewTimeout" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template>
</MkInput>
<MkInput v-model="urlPreviewForm.state.urlPreviewUserAgent" type="text">
<template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewUserAgent" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template>
</MkInput>
<div>
<MkInput v-model="urlPreviewForm.state.urlPreviewSummaryProxyUrl" type="text">
<template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewSummaryProxyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template>
</MkInput>
<div :class="$style.subCaption">
{{ i18n.ts._urlPreviewSetting.summaryProxyDescription2 }}
<ul style="padding-left: 20px; margin: 4px 0">
<li>{{ i18n.ts._urlPreviewSetting.timeout }} / key:timeout</li>
<li>{{ i18n.ts._urlPreviewSetting.maximumContentLength }} / key:contentLengthLimit</li>
<li>{{ i18n.ts._urlPreviewSetting.requireContentLength }} / key:contentLengthRequired</li>
<li>{{ i18n.ts._urlPreviewSetting.userAgent }} / key:userAgent</li>
</ul>
</div>
</div>
</template>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-planet"></i></template>
<template #label>{{ i18n.ts.federation }}</template>
<template v-if="federationForm.savedState.federation === 'all'" #suffix>{{ i18n.ts.all }}</template>
<template v-else-if="federationForm.savedState.federation === 'specified'" #suffix>{{ i18n.ts.specifyHost }}</template>
<template v-else-if="federationForm.savedState.federation === 'none'" #suffix>{{ i18n.ts.none }}</template>
<template v-if="federationForm.modified.value" #footer>
<MkFormFooter :form="federationForm"/>
</template>
<div class="_gaps">
<MkRadios v-model="federationForm.state.federation">
<template #label>{{ i18n.ts.behavior }}<span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
<option value="all">{{ i18n.ts.all }}</option>
<option value="specified">{{ i18n.ts.specifyHost }}</option>
<option value="none">{{ i18n.ts.none }}</option>
</MkRadios>
<MkTextarea v-if="federationForm.state.federation === 'specified'" v-model="federationForm.state.federationHosts">
<template #label>{{ i18n.ts.federationAllowedHosts }}<span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template>
</MkTextarea>
<MkFolder>
<template #icon><i class="ti ti-list"></i></template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.deliverSuspendedSoftware }}</SearchLabel></template>
<template #footer>
<div class="_buttons">
<MkButton @click="federationForm.state.deliverSuspendedSoftware.push({software: '', versionRange: ''})"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
</div>
<SearchMarker path="/admin/settings" :label="i18n.ts.general" :keywords="['general', 'settings']" icon="ti ti-settings">
<div class="_gaps_m">
<SearchMarker v-slot="slotProps" :keywords="['information', 'meta']">
<MkFolder :defaultOpen="true">
<template #icon><SearchIcon><i class="ti ti-info-circle"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.info }}</SearchLabel></template>
<template v-if="infoForm.modified.value" #footer>
<MkFormFooter :form="infoForm"/>
</template>
<div :class="$style.metadataRoot" class="_gaps_s">
<MkInfo>{{ i18n.ts._serverSettings.deliverSuspendedSoftwareDescription }}</MkInfo>
<div v-for="(element, index) in federationForm.state.deliverSuspendedSoftware" :key="index" v-panel :class="$style.fieldDragItem">
<button class="_button" :class="$style.dragItemRemove" @click="federationForm.state.deliverSuspendedSoftware.splice(index, 1)"><i class="ti ti-x"></i></button>
<div :class="$style.dragItemForm">
<FormSplit :minWidth="200">
<MkInput v-model="element.software" small :placeholder="i18n.ts.softwareName">
</MkInput>
<MkInput v-model="element.versionRange" small :placeholder="i18n.ts.version">
</MkInput>
</FormSplit>
</div>
<div class="_gaps">
<SearchMarker :keywords="['name']">
<MkInput v-model="infoForm.state.name">
<template #label><SearchLabel>{{ i18n.ts.instanceName }}</SearchLabel><span v-if="infoForm.modifiedStates.name" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['shortName']">
<MkInput v-model="infoForm.state.shortName">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.shortName }}</SearchLabel> ({{ i18n.ts.optional }})<span v-if="infoForm.modifiedStates.shortName" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.shortNameDescription }}</SearchText></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['description']">
<MkTextarea v-model="infoForm.state.description">
<template #label><SearchLabel>{{ i18n.ts.instanceDescription }}</SearchLabel><span v-if="infoForm.modifiedStates.description" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkTextarea>
</SearchMarker>
<FormSplit :minWidth="300">
<SearchMarker :keywords="['maintainer', 'name']">
<MkInput v-model="infoForm.state.maintainerName">
<template #label><SearchLabel>{{ i18n.ts.maintainerName }}</SearchLabel><span v-if="infoForm.modifiedStates.maintainerName" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['maintainer', 'email', 'contact']">
<MkInput v-model="infoForm.state.maintainerEmail" type="email">
<template #label><SearchLabel>{{ i18n.ts.maintainerEmail }}</SearchLabel><span v-if="infoForm.modifiedStates.maintainerEmail" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-mail"></i></template>
</MkInput>
</SearchMarker>
</FormSplit>
<SearchMarker :keywords="['tos', 'termsOfService']">
<MkInput v-model="infoForm.state.tosUrl" type="url">
<template #label><SearchLabel>{{ i18n.ts.tosUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.tosUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['privacyPolicy']">
<MkInput v-model="infoForm.state.privacyPolicyUrl" type="url">
<template #label><SearchLabel>{{ i18n.ts.privacyPolicyUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.privacyPolicyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['inquiry', 'contact']">
<MkInput v-model="infoForm.state.inquiryUrl" type="url">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.inquiryUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.inquiryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</SearchText></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['repository', 'url']">
<MkInput v-model="infoForm.state.repositoryUrl" type="url">
<template #label><SearchLabel>{{ i18n.ts.repositoryUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.repositoryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.repositoryUrlDescription }}</SearchText></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
<MkInfo v-if="!instance.providesTarball && !infoForm.state.repositoryUrl" warn>
{{ i18n.ts.repositoryUrlOrTarballRequired }}
</MkInfo>
<SearchMarker :keywords="['impressum', 'legalNotice']">
<MkInput v-model="infoForm.state.impressumUrl" type="url">
<template #label><SearchLabel>{{ i18n.ts.impressumUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.impressumUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.impressumDescription }}</SearchText></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['pinned', 'users']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-user-star"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.pinnedUsers }}</SearchLabel></template>
<template v-if="pinnedUsersForm.modified.value" #footer>
<MkFormFooter :form="pinnedUsersForm"/>
</template>
<MkTextarea v-model="pinnedUsersForm.state.pinnedUsers">
<template #label>{{ i18n.ts.pinnedUsers }}<span v-if="pinnedUsersForm.modifiedStates.pinnedUsers" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.pinnedUsersDescription }}</SearchText></template>
</MkTextarea>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['serviceWorker']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-world-cog"></i></SearchIcon></template>
<template #label><SearchLabel>ServiceWorker</SearchLabel></template>
<template v-if="serviceWorkerForm.modified.value" #footer>
<MkFormFooter :form="serviceWorkerForm"/>
</template>
<div class="_gaps">
<SearchMarker>
<MkSwitch v-model="serviceWorkerForm.state.enableServiceWorker">
<template #label><SearchLabel>{{ i18n.ts.enableServiceworker }}</SearchLabel><span v-if="serviceWorkerForm.modifiedStates.enableServiceWorker" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.serviceworkerInfo }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<template v-if="serviceWorkerForm.state.enableServiceWorker">
<SearchMarker>
<MkInput v-model="serviceWorkerForm.state.swPublicKey">
<template #label><SearchLabel>Public key</SearchLabel><span v-if="serviceWorkerForm.modifiedStates.swPublicKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="serviceWorkerForm.state.swPrivateKey">
<template #label><SearchLabel>Private key</SearchLabel><span v-if="serviceWorkerForm.modifiedStates.swPrivateKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
</SearchMarker>
</template>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['ads']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-ad"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts._ad.adsSettings }}</SearchLabel></template>
<template v-if="adForm.modified.value" #footer>
<MkFormFooter :form="adForm"/>
</template>
<div class="_gaps">
<div class="_gaps_s">
<SearchMarker>
<MkInput v-model="adForm.state.notesPerOneAd" :min="0" type="number">
<template #label><SearchLabel>{{ i18n.ts._ad.notesPerOneAd }}</SearchLabel><span v-if="adForm.modifiedStates.notesPerOneAd" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template>
</MkInput>
</SearchMarker>
<MkInfo v-if="adForm.state.notesPerOneAd > 0 && adForm.state.notesPerOneAd < 20" :warn="true">
{{ i18n.ts._ad.adsTooClose }}
</MkInfo>
</div>
</div>
</MkFolder>
</SearchMarker>
<MkSwitch v-model="federationForm.state.signToActivityPubGet">
<template #label>{{ i18n.ts._serverSettings.signToActivityPubGet }}<span v-if="federationForm.modifiedStates.signToActivityPubGet" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.signToActivityPubGet_description }}</template>
</MkSwitch>
<MkSwitch v-model="federationForm.state.proxyRemoteFiles">
<template #label>{{ i18n.ts._serverSettings.proxyRemoteFiles }}<span v-if="federationForm.modifiedStates.proxyRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.proxyRemoteFiles_description }}</template>
</MkSwitch>
<MkSwitch v-model="federationForm.state.allowExternalApRedirect">
<template #label>{{ i18n.ts._serverSettings.allowExternalApRedirect }}<span v-if="federationForm.modifiedStates.allowExternalApRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>
<div>{{ i18n.ts._serverSettings.allowExternalApRedirect_description }}</div>
<div>{{ i18n.ts.needToRestartServerToApply }}</div>
<SearchMarker v-slot="slotProps" :keywords="['url', 'preview']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-world-search"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.title }}</SearchLabel></template>
<template v-if="urlPreviewForm.modified.value" #footer>
<MkFormFooter :form="urlPreviewForm"/>
</template>
</MkSwitch>
<MkSwitch v-model="federationForm.state.cacheRemoteFiles">
<template #label>{{ i18n.ts.cacheRemoteFiles }}<span v-if="federationForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
</MkSwitch>
<div class="_gaps">
<SearchMarker>
<MkSwitch v-model="urlPreviewForm.state.urlPreviewEnabled">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.enable }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewEnabled" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkSwitch>
</SearchMarker>
<template v-if="federationForm.state.cacheRemoteFiles">
<MkSwitch v-model="federationForm.state.cacheRemoteSensitiveFiles">
<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}<span v-if="federationForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
</MkSwitch>
</template>
</div>
</MkFolder>
<template v-if="urlPreviewForm.state.urlPreviewEnabled">
<SearchMarker :keywords="['allow', 'redirect']">
<MkSwitch v-model="urlPreviewForm.state.urlPreviewAllowRedirect">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.allowRedirect }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewAllowRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.allowRedirectDescription }}</template>
</MkSwitch>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-ghost"></i></template>
<template #label>{{ i18n.ts.proxyAccount }}</template>
<template v-if="proxyAccountForm.modified.value" #footer>
<MkFormFooter :form="proxyAccountForm"/>
</template>
<SearchMarker :keywords="['contentLength']">
<MkSwitch v-model="urlPreviewForm.state.urlPreviewRequireContentLength">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.requireContentLength }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewRequireContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template>
</MkSwitch>
</SearchMarker>
<div class="_gaps">
<MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<SearchMarker :keywords="['contentLength']">
<MkInput v-model="urlPreviewForm.state.urlPreviewMaximumContentLength" type="number">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewMaximumContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template>
</MkInput>
</SearchMarker>
<MkTextarea v-model="proxyAccountForm.state.description" :max="500" tall mfmAutocomplete :mfmPreview="true">
<template #label>{{ i18n.ts._profile.description }}</template>
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
</MkTextarea>
</div>
</MkFolder>
<SearchMarker :keywords="['timeout']">
<MkInput v-model="urlPreviewForm.state.urlPreviewTimeout" type="number">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.timeout }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewTimeout" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template>
</MkInput>
</SearchMarker>
<MkButton primary @click="openSetupWizard">
Open setup wizard
</MkButton>
</div>
<SearchMarker :keywords="['userAgent']">
<MkInput v-model="urlPreviewForm.state.urlPreviewUserAgent" type="text">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.userAgent }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewUserAgent" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template>
</MkInput>
</SearchMarker>
<div>
<SearchMarker :keywords="['proxy']">
<MkInput v-model="urlPreviewForm.state.urlPreviewSummaryProxyUrl" type="text">
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.summaryProxy }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewSummaryProxyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template>
</MkInput>
</SearchMarker>
<div :class="$style.subCaption">
{{ i18n.ts._urlPreviewSetting.summaryProxyDescription2 }}
<ul style="padding-left: 20px; margin: 4px 0">
<li>{{ i18n.ts._urlPreviewSetting.timeout }} / key:timeout</li>
<li>{{ i18n.ts._urlPreviewSetting.maximumContentLength }} / key:contentLengthLimit</li>
<li>{{ i18n.ts._urlPreviewSetting.requireContentLength }} / key:contentLengthRequired</li>
<li>{{ i18n.ts._urlPreviewSetting.userAgent }} / key:userAgent</li>
</ul>
</div>
</div>
</template>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['federation']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-planet"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.federation }}</SearchLabel></template>
<template v-if="federationForm.savedState.federation === 'all'" #suffix>{{ i18n.ts.all }}</template>
<template v-else-if="federationForm.savedState.federation === 'specified'" #suffix>{{ i18n.ts.specifyHost }}</template>
<template v-else-if="federationForm.savedState.federation === 'none'" #suffix>{{ i18n.ts.none }}</template>
<template v-if="federationForm.modified.value" #footer>
<MkFormFooter :form="federationForm"/>
</template>
<div class="_gaps">
<SearchMarker>
<MkRadios v-model="federationForm.state.federation">
<template #label><SearchLabel>{{ i18n.ts.behavior }}</SearchLabel><span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
<option value="all">{{ i18n.ts.all }}</option>
<option value="specified">{{ i18n.ts.specifyHost }}</option>
<option value="none">{{ i18n.ts.none }}</option>
</MkRadios>
</SearchMarker>
<SearchMarker :keywords="['hosts']">
<MkTextarea v-if="federationForm.state.federation === 'specified'" v-model="federationForm.state.federationHosts">
<template #label><SearchLabel>{{ i18n.ts.federationAllowedHosts }}</SearchLabel><span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template>
</MkTextarea>
</SearchMarker>
<SearchMarker :keywords="['suspended', 'software']">
<MkFolder>
<template #icon><i class="ti ti-list"></i></template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.deliverSuspendedSoftware }}</SearchLabel></template>
<template #footer>
<div class="_buttons">
<MkButton @click="federationForm.state.deliverSuspendedSoftware.push({software: '', versionRange: ''})"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
</div>
</template>
<div :class="$style.metadataRoot" class="_gaps_s">
<MkInfo>{{ i18n.ts._serverSettings.deliverSuspendedSoftwareDescription }}</MkInfo>
<div v-for="(element, index) in federationForm.state.deliverSuspendedSoftware" :key="index" v-panel :class="$style.fieldDragItem">
<button class="_button" :class="$style.dragItemRemove" @click="federationForm.state.deliverSuspendedSoftware.splice(index, 1)"><i class="ti ti-x"></i></button>
<div :class="$style.dragItemForm">
<FormSplit :minWidth="200">
<MkInput v-model="element.software" small :placeholder="i18n.ts.softwareName">
</MkInput>
<MkInput v-model="element.versionRange" small :placeholder="i18n.ts.version">
</MkInput>
</FormSplit>
</div>
</div>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['sign', 'get']">
<MkSwitch v-model="federationForm.state.signToActivityPubGet">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.signToActivityPubGet }}</SearchLabel><span v-if="federationForm.modifiedStates.signToActivityPubGet" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.signToActivityPubGet_description }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['proxy', 'remote', 'files']">
<MkSwitch v-model="federationForm.state.proxyRemoteFiles">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.proxyRemoteFiles }}</SearchLabel><span v-if="federationForm.modifiedStates.proxyRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.proxyRemoteFiles_description }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['allow', 'external', 'redirect']">
<MkSwitch v-model="federationForm.state.allowExternalApRedirect">
<template #label><SearchLabel>{{ i18n.ts._serverSettings.allowExternalApRedirect }}</SearchLabel><span v-if="federationForm.modifiedStates.allowExternalApRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>
<div><SearchText>{{ i18n.ts._serverSettings.allowExternalApRedirect_description }}</SearchText></div>
<div>{{ i18n.ts.needToRestartServerToApply }}</div>
</template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['cache', 'remote', 'files']">
<MkSwitch v-model="federationForm.state.cacheRemoteFiles">
<template #label><SearchLabel>{{ i18n.ts.cacheRemoteFiles }}</SearchLabel><span v-if="federationForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.cacheRemoteFilesDescription }}</SearchText>{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
</MkSwitch>
</SearchMarker>
<template v-if="federationForm.state.cacheRemoteFiles">
<SearchMarker :keywords="['cache', 'remote', 'sensitive', 'files']">
<MkSwitch v-model="federationForm.state.cacheRemoteSensitiveFiles">
<template #label><SearchLabel>{{ i18n.ts.cacheRemoteSensitiveFiles }}</SearchLabel><span v-if="federationForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</template>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker v-slot="slotProps" :keywords="['proxy', 'account']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-ghost"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.proxyAccount }}</SearchLabel></template>
<template v-if="proxyAccountForm.modified.value" #footer>
<MkFormFooter :form="proxyAccountForm"/>
</template>
<div class="_gaps">
<MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<SearchMarker :keywords="['description']">
<MkTextarea v-model="proxyAccountForm.state.description" :max="500" tall mfmAutocomplete :mfmPreview="true">
<template #label><SearchLabel>{{ i18n.ts._profile.description }}</SearchLabel></template>
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
</MkTextarea>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<MkButton primary @click="openSetupWizard">
Open setup wizard
</MkButton>
</div>
</SearchMarker>
</div>
</PageWithHeader>
</template>
@@ -6,17 +6,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 900px;">
<div class="_gaps_m">
<MkButton primary @click="onCreateWebhookClicked">
<i class="ti ti-plus"></i> {{ i18n.ts._webhookSettings.createWebhook }}
</MkButton>
<SearchMarker path="/admin/system-webhook" label="SystemWebhook" :keywords="['webhook']" icon="ti ti-webhook">
<div class="_gaps_m">
<SearchMarker>
<MkButton primary @click="onCreateWebhookClicked">
<i class="ti ti-plus"></i> <SearchLabel>{{ i18n.ts._webhookSettings.createWebhook }}</SearchLabel>
</MkButton>
</SearchMarker>
<FormSection>
<div class="_gaps">
<XItem v-for="item in webhooks" :key="item.id" :entity="item" @edit="onEditButtonClicked" @delete="onDeleteButtonClicked"/>
</div>
</FormSection>
</div>
<FormSection>
<div class="_gaps">
<XItem v-for="item in webhooks" :key="item.id" :entity="item" @edit="onEditButtonClicked" @delete="onDeleteButtonClicked"/>
</div>
</FormSection>
</div>
</SearchMarker>
</div>
</PageWithHeader>
</template>
@@ -366,6 +366,7 @@ definePage(() => ({
> .items {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
padding: 16px;
+2 -2
View File
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-shield-lock"></i></template>
<template #label><SearchLabel>{{ i18n.ts.totp }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.totpDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.totpDescription }}</SearchText></template>
<template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
<div v-if="$i.twoFactorEnabled" class="_gaps_s">
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['password', 'less', 'key', 'passkey', 'login', 'signin']">
<MkSwitch :disabled="!$i.twoFactorEnabled || $i.securityKeysList.length === 0" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)">
<template #label><SearchLabel>{{ i18n.ts.passwordLessLogin }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.passwordLessLoginDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.passwordLessLoginDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</div>
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/account-data" :label="i18n.ts._settings.accountData" :keywords="['import', 'export', 'data', 'archive']" icon="ti ti-package">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/package_3d.png" color="#ff9100">
<SearchKeyword>{{ i18n.ts._settings.accountDataBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.accountDataBanner }}</SearchText>
</MkFeatureBanner>
<div class="_gaps_s">
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/connect" :label="i18n.ts._settings.serviceConnection" :keywords="['app', 'service', 'connect', 'webhook', 'api', 'token']" icon="ti ti-link">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/link_3d.png" color="#ff0088">
<SearchKeyword>{{ i18n.ts._settings.serviceConnectionBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.serviceConnectionBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['api', 'app', 'token', 'accessToken']">
@@ -7,6 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<FormInfo warn>{{ i18n.ts.customCssWarn }}</FormInfo>
<FormInfo v-if="isSafeMode" warn>{{ i18n.ts.customCssIsDisabledBecauseSafeMode }}</FormInfo>
<MkCodeEditor v-model="localCustomCss" manualSave lang="css">
<template #label>CSS</template>
</MkCodeEditor>
@@ -17,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, watch, computed } from 'vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import FormInfo from '@/components/MkInfo.vue';
import { isSafeMode } from '@@/js/config.js';
import * as os from '@/os.js';
import { unisonReload } from '@/utility/unison-reload.js';
import { i18n } from '@/i18n.js';
@@ -97,8 +97,8 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { prefer } from '@/preferences.js';
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
import { reloadAsk } from '@/utility/reload-ask.js';
import { selectFile } from '@/utility/drive.js';
import { suggestReload } from '@/utility/reload-suggest.js';
const navWindow = prefer.model('deck.navWindow');
const useSimpleUiForNonRootPages = prefer.model('deck.useSimpleUiForNonRootPages');
@@ -109,8 +109,8 @@ const menuPosition = prefer.model('deck.menuPosition');
const navbarPosition = prefer.model('deck.navbarPosition');
const wallpaper = prefer.model('deck.wallpaper');
watch(wallpaper, async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
watch(wallpaper, () => {
suggestReload();
});
function setWallpaper(ev: MouseEvent) {
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/drive" :label="i18n.ts.drive" :keywords="['drive']" icon="ti ti-cloud">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/cloud_3d.png" color="#0059ff">
<SearchKeyword>{{ i18n.ts._settings.driveBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.driveBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['capacity', 'usage']">
@@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="keepOriginalFilename">
<MkSwitch v-model="keepOriginalFilename">
<template #label><SearchLabel>{{ i18n.ts.keepOriginalFilename }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['auto', 'nsfw', 'sensitive', 'media', 'file']">
<MkSwitch v-model="autoSensitive" @update:modelValue="saveProfile()">
<template #label><SearchLabel>{{ i18n.ts.enableAutoSensitive }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption><SearchKeyword>{{ i18n.ts.enableAutoSensitiveDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.enableAutoSensitiveDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</div>
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>{{ i18n.ts._preferencesBackup.autoPreferencesBackupIsNotEnabledForThisDevice }}</div>
<div><button class="_textButton" @click="enableAutoBackup">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipAutoBackup">{{ i18n.ts.skip }}</button></div>
</MkInfo>
<MkSuperMenu :def="menuDef" :grid="narrow" :searchIndex="SETTING_INDEX"></MkSuperMenu>
<MkSuperMenu :def="menuDef" :grid="narrow" :searchIndex="searchIndex"></MkSuperMenu>
</div>
</div>
<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
@@ -42,12 +42,12 @@ import { instance } from '@/instance.js';
import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import * as os from '@/os.js';
import { useRouter } from '@/router.js';
import { searchIndexes } from '@/utility/settings-search-index.js';
import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utility.js';
import { store } from '@/store.js';
import { signout } from '@/signout.js';
import { genSearchIndexes } from '@/utility/inapp-search.js';
const SETTING_INDEX = searchIndexes; // TODO: lazy load
const searchIndex = await import('search-index:settings').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
const indexInfo = {
title: i18n.ts.settings,
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/mute-block" :label="i18n.ts.muteAndBlock" icon="ti ti-ban" :keywords="['mute', 'block']">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/prohibited_3d.png" color="#ff2600">
<SearchKeyword>{{ i18n.ts._settings.muteAndBlockBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.muteAndBlockBanner }}</SearchText>
</MkFeatureBanner>
<div class="_gaps_s">
@@ -189,10 +189,10 @@ import { ensureSignin } from '@/i.js';
import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { reloadAsk } from '@/utility/reload-ask.js';
import { prefer } from '@/preferences.js';
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
import { Paginator } from '@/utility/paginator.js';
import { suggestReload } from '@/utility/reload-suggest.js';
const $i = ensureSignin();
@@ -216,8 +216,8 @@ const showSoftWordMutedWord = prefer.model('showSoftWordMutedWord');
watch([
showSoftWordMutedWord,
], async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
], () => {
suggestReload();
});
async function unrenoteMute(user, ev) {
@@ -64,7 +64,6 @@ import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
import { store } from '@/store.js';
import { reloadAsk } from '@/utility/reload-ask.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { prefer } from '@/preferences.js';
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/notifications" :label="i18n.ts.notifications" :keywords="['notifications']" icon="ti ti-bell">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/bell_3d.png" color="#ffff00">
<SearchKeyword>{{ i18n.ts._settings.notificationsBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.notificationsBanner }}</SearchText>
</MkFeatureBanner>
<FormSection first>
@@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<FormInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
<FormInfo>{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount"><SearchKeyword>{{ i18n.ts._accountDelete.requestAccountDelete }}</SearchKeyword></MkButton>
<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount"><SearchText>{{ i18n.ts._accountDelete.requestAccountDelete }}</SearchText></MkButton>
<MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
</div>
</MkFolder>
@@ -155,13 +155,13 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { ensureSignin } from '@/i.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { reloadAsk } from '@/utility/reload-ask.js';
import FormSection from '@/components/form/section.vue';
import { prefer } from '@/preferences.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import { signout } from '@/signout.js';
import { migrateOldSettings } from '@/pref-migrate.js';
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
import { suggestReload } from '@/utility/reload-suggest.js';
const $i = ensureSignin();
@@ -172,8 +172,8 @@ const devMode = prefer.model('devMode');
const stackingRouterView = prefer.model('experimental.stackingRouterView');
const enableFolderPageView = prefer.model('experimental.enableFolderPageView');
watch(skipNoteRender, async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
watch(skipNoteRender, () => {
suggestReload();
});
async function deleteAccount() {
@@ -7,10 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/plugin" :label="i18n.ts.plugins" :keywords="['plugin', 'addon', 'extension']" icon="ti ti-plug">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/electric_plug_3d.png" color="#ffbb00">
<SearchKeyword>{{ i18n.ts._settings.pluginBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.pluginBanner }}</SearchText>
</MkFeatureBanner>
<FormLink to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink>
<MkInfo v-if="isSafeMode" warn>{{ i18n.ts.pluginsAreDisabledBecauseSafeMode }}</MkInfo>
<FormLink v-else to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink>
<FormSection>
<template #label>{{ i18n.ts.manage }}</template>
@@ -103,10 +105,12 @@ import MkCode from '@/components/MkCode.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { changePluginActive, configPlugin, pluginLogs, uninstallPlugin, reloadPlugin } from '@/plugin.js';
import { prefer } from '@/preferences.js';
import { isSafeMode } from '@@/js/config.js';
import * as os from '@/os.js';
const plugins = prefer.r.plugins;
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/preferences" :label="i18n.ts.preferences" :keywords="['general', 'preferences']" icon="ti ti-adjustments">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/gear_3d.png" color="#00ff9d">
<SearchKeyword>{{ i18n.ts._settings.preferencesBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.preferencesBanner }}</SearchText>
</MkFeatureBanner>
<div class="_gaps_s">
@@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['realtimemode']">
<MkSwitch v-model="realtimeMode">
<template #label><i class="ti ti-bolt"></i> <SearchLabel>{{ i18n.ts.realtimeMode }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts._settings.realtimeMode_description }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts._settings.realtimeMode_description }}</SearchText></template>
</MkSwitch>
</SearchMarker>
@@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="pollingInterval">
<MkRange v-model="pollingInterval" :min="1" :max="3" :step="1" easing :showTicks="true" :textConverter="(v) => v === 1 ? i18n.ts.low : v === 2 ? i18n.ts.middle : v === 3 ? i18n.ts.high : ''">
<template #label><SearchLabel>{{ i18n.ts._settings.contentsUpdateFrequency }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts._settings.contentsUpdateFrequency_description }}</SearchKeyword><br><SearchKeyword>{{ i18n.ts._settings.contentsUpdateFrequency_description2 }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts._settings.contentsUpdateFrequency_description }}</SearchText><br><SearchText>{{ i18n.ts._settings.contentsUpdateFrequency_description2 }}</SearchText></template>
<template #prefix><i class="ti ti-player-play"></i></template>
<template #suffix><i class="ti ti-player-track-next"></i></template>
</MkRange>
@@ -165,7 +165,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="collapseRenotes">
<MkSwitch v-model="collapseRenotes">
<template #label><SearchLabel>{{ i18n.ts.collapseRenotes }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.collapseRenotesDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.collapseRenotesDescription }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -449,7 +449,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/mens_room_3d.png" color="#0011ff">
<SearchKeyword>{{ i18n.ts._settings.accessibilityBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.accessibilityBanner }}</SearchText>
</MkFeatureBanner>
<div class="_gaps_s">
@@ -489,7 +489,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="enablePullToRefresh">
<MkSwitch v-model="enablePullToRefresh">
<template #label><SearchLabel>{{ i18n.ts._settings.enablePullToRefresh }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts._settings.enablePullToRefresh_description }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts._settings.enablePullToRefresh_description }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -571,7 +571,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="animation">
<MkSwitch :modelValue="!reduceAnimation" @update:modelValue="v => reduceAnimation = !v">
<template #label><SearchLabel>{{ i18n.ts._settings.uiAnimations }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -580,7 +580,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="useBlurEffect">
<MkSwitch v-model="useBlurEffect">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -589,7 +589,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="useBlurEffectForModal">
<MkSwitch v-model="useBlurEffectForModal">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -598,7 +598,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="enableHighQualityImagePlaceholders">
<MkSwitch v-model="enableHighQualityImagePlaceholders">
<template #label><SearchLabel>{{ i18n.ts._settings.enableHighQualityImagePlaceholders }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -607,7 +607,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="useStickyIcons">
<MkSwitch v-model="useStickyIcons">
<template #label><SearchLabel>{{ i18n.ts._settings.useStickyIcons }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@@ -795,7 +795,6 @@ import MkInfo from '@/components/MkInfo.vue';
import { store } from '@/store.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { reloadAsk } from '@/utility/reload-ask.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { miLocalStorage } from '@/local-storage.js';
@@ -807,6 +806,7 @@ import { claimAchievement } from '@/utility/achievements.js';
import { instance } from '@/instance.js';
import { ensureSignin } from '@/i.js';
import { genId } from '@/utility/id.js';
import { suggestReload } from '@/utility/reload-suggest.js';
const $i = ensureSignin();
@@ -928,8 +928,8 @@ watch([
enablePullToRefresh,
reduceAnimation,
showAvailableReactionsFirstInNote,
], async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
], () => {
suggestReload();
});
const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const;
@@ -7,13 +7,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/privacy" :label="i18n.ts.privacy" :keywords="['privacy']" icon="ti ti-lock-open">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/unlocked_3d.png" color="#aeff00">
<SearchKeyword>{{ i18n.ts._settings.privacyBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.privacyBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['follow', 'lock']">
<MkSwitch v-model="isLocked" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.makeFollowManuallyApprove }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.lockedAccountInfo }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.lockedAccountInfo }}</SearchText></template>
</MkSwitch>
</SearchMarker>
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['reaction', 'public']">
<MkSwitch v-model="publicReactions" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.makeReactionsPublic }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.makeReactionsPublicDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.makeReactionsPublicDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
@@ -53,28 +53,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['online', 'status']">
<MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.hideOnlineStatus }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.hideOnlineStatusDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.hideOnlineStatusDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['crawle', 'index', 'search']">
<MkSwitch v-model="noCrawle" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.noCrawle }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.noCrawleDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.noCrawleDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['crawle', 'ai']">
<MkSwitch v-model="preventAiLearning" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.preventAiLearning }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.preventAiLearningDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.preventAiLearningDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['explore']">
<MkSwitch v-model="isExplorable" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.makeExplorable }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.makeExplorableDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.makeExplorableDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
@@ -146,7 +146,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<template #caption>
<div><SearchKeyword>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</SearchKeyword></div>
<div><SearchText>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</SearchText></div>
</template>
</FormSlot>
</SearchMarker>
@@ -183,7 +183,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<template #caption>
<div><SearchKeyword>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</SearchKeyword></div>
<div><SearchText>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</SearchText></div>
</template>
</FormSlot>
</SearchMarker>
@@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false">
<template #label><SearchLabel>{{ i18n.ts._profile.followedMessage }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption>
<div><SearchKeyword>{{ i18n.ts._profile.followedMessageDescription }}</SearchKeyword></div>
<div><SearchText>{{ i18n.ts._profile.followedMessageDescription }}</SearchText></div>
<div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div>
</template>
</MkInput>
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/security" :label="i18n.ts.security" :keywords="['security']" icon="ti ti-lock" :inlining="['2fa']">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/locked_with_key_3d.png" color="#ffbf00">
<SearchKeyword>{{ i18n.ts._settings.securityBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.securityBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['password']">
@@ -24,30 +24,34 @@ SPDX-License-Identifier: AGPL-3.0-only
<X2fa/>
<FormSection>
<template #label>{{ i18n.ts.signinHistory }}</template>
<MkPagination :paginator="paginator" withControl>
<template #default="{items}">
<div>
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
<header>
<i v-if="item.success" class="ti ti-check icon succ"></i>
<i v-else class="ti ti-circle-x icon fail"></i>
<code class="ip _monospace">{{ item.ip }}</code>
<MkTime :time="item.createdAt" class="time"/>
</header>
<SearchMarker :keywords="['signin', 'login', 'history', 'log']">
<FormSection>
<template #label><SearchLabel>{{ i18n.ts.signinHistory }}</SearchLabel></template>
<MkPagination :paginator="paginator" withControl>
<template #default="{items}">
<div>
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
<header>
<i v-if="item.success" class="ti ti-check icon succ"></i>
<i v-else class="ti ti-circle-x icon fail"></i>
<code class="ip _monospace">{{ item.ip }}</code>
<MkTime :time="item.createdAt" class="time"/>
</header>
</div>
</div>
</div>
</template>
</MkPagination>
</FormSection>
</template>
</MkPagination>
</FormSection>
</SearchMarker>
<FormSection>
<FormSlot>
<MkButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> {{ i18n.ts.regenerateLoginToken }}</MkButton>
<template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template>
</FormSlot>
</FormSection>
<SearchMarker :keywords="['regenerate', 'refresh', 'reset', 'token']">
<FormSection>
<FormSlot>
<MkButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> <SearchLabel>{{ i18n.ts.regenerateLoginToken }}</SearchLabel></MkButton>
<template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template>
</FormSlot>
</FormSection>
</SearchMarker>
</div>
</SearchMarker>
</template>
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/sounds" :label="i18n.ts.sounds" :keywords="['sounds']" icon="ti ti-music">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/speaker_high_volume_3d.png" color="#ff006f">
<SearchKeyword>{{ i18n.ts._settings.soundsBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.soundsBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['mute']">
@@ -35,7 +35,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div class="_gaps">
<MkInfo v-if="isSafeMode" warn>{{ i18n.ts.themeIsDefaultBecauseSafeMode }}</MkInfo>
<div v-else class="_gaps">
<template v-if="!store.r.darkMode.value">
<SearchMarker :keywords="['light', 'theme']">
<MkFolder :defaultOpen="true" :max-height="500">
@@ -204,12 +206,14 @@ import JSON5 from 'json5';
import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
import type { Theme } from '@/theme.js';
import { isSafeMode } from '@@/js/config.js';
import * as os from '@/os.js';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkThemePreview from '@/components/MkThemePreview.vue';
import MkInfo from '@/components/MkInfo.vue';
import { getBuiltinThemesRef, getThemesRef, removeTheme } from '@/theme.js';
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
import { store } from '@/store.js';
+2
View File
@@ -6,6 +6,7 @@
import { ref, defineAsyncComponent } from 'vue';
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
import { compareVersions } from 'compare-versions';
import { isSafeMode } from '@@/js/config.js';
import { genId } from '@/utility/id.js';
import * as Misskey from 'misskey-js';
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
@@ -232,6 +233,7 @@ export function launchPlugins() {
}
async function launchPlugin(id: Plugin['installId']): Promise<void> {
if (isSafeMode) return;
const plugin = prefer.s.plugins.find(x => x.installId === id);
if (!plugin) return;
@@ -491,10 +491,6 @@ export const ROUTE_DEF = [{
path: '/performance',
name: 'performance',
component: page(() => import('@/pages/admin/performance.vue')),
}, {
path: '/server-rules',
name: 'server-rules',
component: page(() => import('@/pages/admin/server-rules.vue')),
}, {
path: '/invites',
name: 'invites',
+4 -5
View File
@@ -37,11 +37,6 @@ html {
color: var(--MI_THEME-fg);
accent-color: var(--MI_THEME-accent);
&, * {
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
scrollbar-width: thin;
}
&.f-1 {
font-size: 15px;
}
@@ -91,7 +86,11 @@ html::selection {
100% {
opacity: 0;
}
}
html, body, main, div {
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
scrollbar-width: thin;
}
html,
@@ -48,10 +48,6 @@ function skip() {
.title {
padding: 0 10px;
font-weight: bold;
&:empty {
display: none;
}
}
.body {
@@ -0,0 +1,71 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<span :class="$style.icon">
<i class="ti ti-info-circle"></i>
</span>
<span :class="$style.title">{{ i18n.ts.reloadRequiredToApplySettings }}</span>
<span :class="$style.body"><button class="_textButton" style="color: var(--MI_THEME-fgOnAccent);" @click="reload">{{ i18n.ts.reload }}</button> | <button class="_textButton" style="color: var(--MI_THEME-fgOnAccent);" @click="skip">{{ i18n.ts.skip }}</button></span>
</div>
</template>
<script lang="ts" setup>
import { i18n } from '@/i18n.js';
import { shouldSuggestReload } from '@/utility/reload-suggest.js';
import { unisonReload } from '@/utility/unison-reload.js';
function reload() {
unisonReload();
}
function skip() {
shouldSuggestReload.value = false;
}
</script>
<style lang="scss" module>
.root {
--height: 24px;
font-size: 0.85em;
display: flex;
vertical-align: bottom;
width: 100%;
line-height: var(--height);
height: var(--height);
overflow: clip;
contain: strict;
background: var(--MI_THEME-accent);
color: var(--MI_THEME-fgOnAccent);
}
.icon {
margin-left: 10px;
animation: blink 2s infinite;
}
.title {
padding: 0 10px;
font-weight: bold;
animation: blink 2s infinite;
}
.body {
min-width: 0;
flex: 1;
overflow: clip;
white-space: nowrap;
text-overflow: ellipsis;
}
@keyframes blink {
0% { opacity: 1; }
10% { opacity: 1; }
50% { opacity: 0; }
90% { opacity: 1; }
100% { opacity: 1; }
}
</style>
+21 -1
View File
@@ -94,6 +94,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="dev" id="devTicker"><span style="animation: dev-ticker-blink 2s infinite;">DEV BUILD</span></div>
<div v-if="$i && $i.isBot" id="botWarn"><span style="animation: dev-ticker-blink 2s infinite;">{{ i18n.ts.loggedInAsBot }}</span></div>
<div v-if="isSafeMode" id="safemodeWarn">
<span style="animation: dev-ticker-blink 2s infinite;">{{ i18n.ts.safeModeEnabled }}</span>&nbsp;
<button class="_textButton" style="pointer-events: all;" @click="exitSafeMode">{{ i18n.ts.turnItOff }}</button>
</div>
</template>
<script lang="ts" setup>
@@ -101,7 +106,10 @@ import { defineAsyncComponent, ref, TransitionGroup } from 'vue';
import * as Misskey from 'misskey-js';
import { swInject } from './sw-inject.js';
import XNotification from './notification.vue';
import { isSafeMode } from '@@/js/config.js';
import { popups } from '@/os.js';
import { unisonReload } from '@/utility/unison-reload.js';
import { miLocalStorage } from '@/local-storage.js';
import { pendingApiRequestsCount } from '@/utility/misskey-api.js';
import * as sound from '@/utility/sound.js';
import { $i } from '@/i.js';
@@ -144,6 +152,13 @@ function onNotification(notification: Misskey.entities.Notification, isClient =
sound.playMisskeySfx('notification');
}
function exitSafeMode() {
miLocalStorage.removeItem('isSafeMode');
const url = new URL(window.location.href);
url.searchParams.delete('safemode');
unisonReload(url.toString());
}
if ($i) {
if (store.s.realtimeMode) {
const connection = useStream().useChannel('main');
@@ -396,7 +411,7 @@ if ($i) {
width: 100%;
height: max-content;
text-align: center;
z-index: 2147483647;
z-index: 2147483646;
color: #ff0;
background: rgba(0, 0, 0, 0.5);
padding: 4px 7px;
@@ -405,6 +420,11 @@ if ($i) {
user-select: none;
}
#safemodeWarn {
@extend #botWarn;
z-index: 2147483647;
}
#devTicker {
position: fixed;
bottom: 0;
+7 -1
View File
@@ -13,6 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.main, { [$style.withWallpaper]: withWallpaper, [$style.withSidebarAndTitlebar]: !isMobile && prefer.r['deck.navbarPosition'].value === 'left' && prefer.r.showTitlebar.value }]" :style="{ backgroundImage: prefer.s['deck.wallpaper'] != null ? `url(${ prefer.s['deck.wallpaper'] })` : null }">
<XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'top'"/>
<XReloadSuggest v-if="shouldSuggestReload"/>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
<XAnnouncements v-if="$i"/>
<XStatusBars/>
<div :class="$style.columnsWrapper">
@@ -81,12 +83,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, ref, useTemplateRef } from 'vue';
import { genId } from '@/utility/id.js';
import XCommon from './_common_/common.vue';
import { genId } from '@/utility/id.js';
import XSidebar from '@/ui/_common_/navbar.vue';
import XNavbarH from '@/ui/_common_/navbar-h.vue';
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XTitlebar from '@/ui/_common_/titlebar.vue';
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
import XReloadSuggest from '@/ui/_common_/ReloadSuggest.vue';
import * as os from '@/os.js';
import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
@@ -105,6 +109,8 @@ import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
import XChatColumn from '@/ui/deck/chat-column.vue';
import { mainRouter } from '@/router.js';
import { columns, layout, columnTypes, switchProfileMenu, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js';
import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
import { shouldSuggestReload } from '@/utility/reload-suggest.js';
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
+3
View File
@@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.contents, !isMobile && prefer.r.showTitlebar.value ? $style.withSidebarAndTitlebar : null]" @contextmenu.stop="onContextmenu">
<div>
<XReloadSuggest v-if="shouldSuggestReload"/>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
<XAnnouncements v-if="$i"/>
<XStatusBars :class="$style.statusbars"/>
@@ -38,6 +39,7 @@ import XCommon from './_common_/common.vue';
import type { PageMetadata } from '@/page.js';
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
import XReloadSuggest from '@/ui/_common_/ReloadSuggest.vue';
import XTitlebar from '@/ui/_common_/titlebar.vue';
import XSidebar from '@/ui/_common_/navbar.vue';
import * as os from '@/os.js';
@@ -50,6 +52,7 @@ import { mainRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
import { DI } from '@/di.js';
import { shouldSuggestReload } from '@/utility/reload-suggest.js';
const XWidgets = defineAsyncComponent(() => import('./_common_/widgets.vue'));
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
@@ -44,6 +44,8 @@
"😑": ["face", "indifferent", "-_-", "meh", "deadpan"],
"😒": ["indifference", "bored", "straight face", "serious", "sarcasm", "unimpressed", "skeptical", "dubious", "side_eye"],
"🙄": ["face", "eyeroll", "frustrated"],
"🙂‍↔️": ["face", "head", "horizontally", "no", "shake", "shaking"],
"🙂‍↕️": ["face", "head", "nod", "shaking", "vertically", "yes"],
"🤔": ["face", "hmmm", "think", "consider"],
"🤥": ["face", "lie", "pinocchio"],
"🤭": ["face", "whoops", "shock", "surprise"],
@@ -76,6 +78,7 @@
"😥": ["face", "phew", "sweat", "nervous"],
"🤤": ["face"],
"😪": ["face", "tired", "rest", "nap"],
"🫩": ["face", "bags", "bored", "exhausted", "eyes", "fatigued", "late", "sleepy", "tired", "weary"],
"😓": ["face", "hot", "sad", "tired", "exercise"],
"🥵": ["face", "feverish", "heat", "red", "sweating"],
"🥶": ["face", "blue", "freezing", "frozen", "frostbite", "icicles"],
@@ -92,10 +95,11 @@
"🥴": ["face", "dizzy", "intoxicated", "tipsy", "wavy"],
"🥱": ["face", "tired", "yawning"],
"😴": ["face", "tired", "sleepy", "night", "zzz"],
"👁️‍🗨️": ["balloon", "bubble", "eye", "speech", "witness"],
"💤": ["sleepy", "tired", "dream"],
"😶‍🌫️": [],
"😮‍💨": [],
"😵‍💫": [],
"😶‍🌫️": ["face", "absentminded", "clouds", "fog", "head"],
"😮‍💨": ["face", "blow", "blowing", "exhale", "exhaling", "exhausted", "gasp", "groan", "relief", "sigh", "smiley", "smoke", "whisper", "whistle"],
"😵‍💫": ["face", "confused", "dizzy", "eyes", "hypnotized", "omg", "smiley", "spiral", "trouble", "whoa", "woah", "woozy"],
"🫠": ["disappear", "dissolve", "liquid", "melt", "toketa"],
"🫢": ["amazement", "awe", "disbelief", "embarrass", "scared", "surprise", "ohoho"],
"🫣": ["captivated", "peep", "stare", "chunibyo"],
@@ -159,19 +163,19 @@
"🤞": ["good", "lucky"],
"🖖": ["hand", "fingers", "spock", "star trek"],
"✍": ["lower_left_ballpoint_pen", "stationery", "write", "compose"],
"🫰": [],
"🫱": [],
"🫲": [],
"🫳": [],
"🫴": [],
"🫵": [],
"🫰": ["<3", "crossed", "expensive", "finger", "hand", "heart", "index", "love", "money", "snap", "thumb"],
"🫱": ["hand", "handshake", "hold", "reach", "right", "rightward", "rightwards", "shake"],
"🫲": ["hand", "handshake", "hold", "left", "leftward", "leftwards", "reach", "shake"],
"🫳": ["dismiss", "down", "drop", "dropped", "hand", "palm", "pick", "shoo", "up"],
"🫴": ["beckon", "catch", "come", "hand", "hold", "know", "lift", "me", "offer", "palm", "tell"],
"🫵": ["at", "finger", "hand", "index", "pointing", "poke", "viewer", "you"],
"🫶": ["moemoekyun"],
"🤏": ["hand", "fingers"],
"🤌": ["hand", "fingers"],
"🤳": ["camera", "phone"],
"💅": ["beauty", "manicure", "finger", "fashion", "nail"],
"👄": ["mouth", "kiss"],
"🫦": [],
"🫦": ["anxious", "bite", "biting", "fear", "flirt", "flirting", "kiss", "lip", "lipstick", "nervous", "sexy", "uncomfortable", "worried", "worry"],
"🦷": ["teeth", "dentist"],
"👅": ["mouth", "playful"],
"👂": ["face", "hear", "sound", "listen"],
@@ -180,11 +184,12 @@
"👁": ["face", "look", "see", "watch", "stare"],
"👀": ["look", "watch", "stalk", "peek", "see"],
"🧠": ["smart", "intelligent"],
"🫀": [],
"🫁": [],
"🫀": ["anatomical", "beat", "cardiology", "heart", "heartbeat", "organ", "pulse", "real", "red"],
"🫁": ["breath", "breathe", "exhalation", "inhalation", "lung", "lungs", "organ", "respiration"],
"👤": ["user", "person", "human"],
"👥": ["user", "person", "human", "group", "team"],
"🗣": ["user", "person", "human", "sing", "say", "talk"],
"🫆": ["clue", "crime", "detective", "fingerprint", "forensics", "identity", "mystery", "print", "safety", "trace"],
"👶": ["child", "boy", "girl", "toddler"],
"🧒": ["gender-neutral", "young"],
"👦": ["man", "male", "guy", "teenager"],
@@ -199,6 +204,7 @@
"👩‍🦰": ["woman", "female", "girl", "ginger", "redhead"],
"👨‍🦰": ["man", "male", "boy", "guy", "ginger", "redhead"],
"👱‍♀️": ["woman", "female", "girl", "blonde", "person"],
"👱‍♂️": ["blond", "blond-haired", "hair", "man"],
"👱": ["man", "male", "boy", "blonde", "guy", "person"],
"🧑‍🦳": ["gray", "old", "white"],
"👩‍🦳": ["woman", "female", "girl", "gray", "old", "white"],
@@ -207,20 +213,27 @@
"👩‍🦲": ["woman", "female", "girl", "bald", "chemotherapy", "hairless", "shaven"],
"👨‍🦲": ["man", "male", "boy", "guy", "bald", "chemotherapy", "hairless", "shaven"],
"🧔": ["person", "bewhiskered"],
"🧔‍♀️": ["beard", "bearded", "whiskers", "woman"],
"🧔‍♂️": ["beard", "bearded", "man", "whiskers"],
"🧓": ["human", "elder", "senior", "gender-neutral"],
"👴": ["human", "male", "men", "old", "elder", "senior"],
"👵": ["human", "female", "women", "lady", "old", "elder", "senior"],
"👲": ["male", "boy", "chinese"],
"🧕": ["female", "hijab", "mantilla", "tichel"],
"👳‍♀️": ["female", "indian", "hinduism", "arabs", "woman"],
"👳‍♂️": ["man", "turban", "wearing"],
"👳": ["male", "indian", "hinduism", "arabs"],
"👮‍♀️": ["woman", "police", "law", "legal", "enforcement", "arrest", "911", "female"],
"👮‍♂️": ["apprehend", "arrest", "citation", "cop", "law", "man", "officer", "over", "police", "pulled", "undercover"],
"👮": ["man", "police", "law", "legal", "enforcement", "arrest", "911"],
"👷‍♀️": ["female", "human", "wip", "build", "construction", "worker", "labor", "woman"],
"👷‍♂️": ["build", "construction", "fix", "hardhat", "hat", "man", "rebuild", "remodel", "repair", "work", "worker"],
"👷": ["male", "human", "wip", "guy", "build", "construction", "worker", "labor"],
"💂‍♀️": ["uk", "gb", "british", "female", "royal", "woman"],
"💂‍♂️": ["buckingham", "guard", "helmet", "london", "man", "palace"],
"💂": ["uk", "gb", "british", "male", "guy", "royal"],
"🕵️‍♀️": ["human", "spy", "detective", "female", "woman"],
"🕵️‍♂️": ["detective", "man", "sleuth", "spy"],
"🕵": ["human", "spy", "detective"],
"🧑‍⚕️": ["doctor", "nurse", "therapist", "healthcare", "human"],
"👩‍⚕️": ["doctor", "nurse", "therapist", "healthcare", "woman", "human"],
@@ -270,102 +283,152 @@
"🧑‍⚖️": ["justice", "court", "human"],
"👩‍⚖️": ["justice", "court", "woman", "human"],
"👨‍⚖️": ["justice", "court", "man", "human"],
"🦸": ["good", "hero", "superhero", "superpower"],
"🦸‍♀️": ["woman", "female", "good", "heroine", "superpowers"],
"🦸‍♂️": ["man", "male", "good", "hero", "superpowers"],
"🦹": ["bad", "criminal", "evil", "superpower", "supervillain", "villain"],
"🦹‍♀️": ["woman", "female", "evil", "bad", "criminal", "heroine", "superpowers"],
"🦹‍♂️": ["man", "male", "evil", "bad", "criminal", "hero", "superpowers"],
"🤶": ["woman", "female", "xmas", "mother christmas"],
"🧑‍🎄": ["xmas", "christmas"],
"🎅": ["festival", "man", "male", "xmas", "father christmas"],
"🥷": [],
"🥷": ["assassin", "fight", "fighter", "hidden", "ninja", "person", "secret", "skills", "sly", "soldier", "stealth", "war"],
"🧙": ["fantasy", "mage", "magic", "play", "sorcerer", "sorceress", "sorcery", "spell", "summon", "witch", "wizard"],
"🧙‍♀️": ["woman", "female", "mage", "witch"],
"🧙‍♂️": ["man", "male", "mage", "sorcerer"],
"🧝": ["elf", "elves", "enchantment", "fantasy", "folklore", "magic", "magical", "myth"],
"🧝‍♀️": ["woman", "female"],
"🧝‍♂️": ["man", "male"],
"🧛": ["blood", "dracula", "fangs", "halloween", "scary", "supernatural", "teeth", "undead", "vampire"],
"🧛‍♀️": ["woman", "female"],
"🧛‍♂️": ["man", "male", "dracula"],
"🧟": ["apocalypse", "dead", "halloween", "horror", "scary", "undead", "walking", "zombie"],
"🧟‍♀️": ["woman", "female", "undead", "walking dead"],
"🧟‍♂️": ["man", "male", "dracula", "undead", "walking dead"],
"🧞": ["djinn", "fantasy", "genie", "jinn", "lamp", "myth", "rub", "wishes"],
"🧞‍♀️": ["woman", "female"],
"🧞‍♂️": ["man", "male"],
"🧜": ["creature", "fairytale", "folklore", "merperson", "ocean", "sea", "siren", "trident"],
"🧜‍♀️": ["woman", "female", "merwoman", "ariel"],
"🧜‍♂️": ["man", "male", "triton"],
"🧚": ["fairy", "fairytale", "fantasy", "myth", "person", "pixie", "tale", "wings"],
"🧚‍♀️": ["woman", "female"],
"🧚‍♂️": ["man", "male"],
"👼": ["heaven", "wings", "halo"],
"🧌": [],
"🧌": ["fairy", "fantasy", "monster", "tale", "troll", "trolling"],
"🤰": ["baby"],
"🫃": [],
"🫄": [],
"🫅": [],
"🫃": ["belly", "bloated", "full", "man", "overeat", "pregnant"],
"🫄": ["belly", "bloated", "full", "overeat", "person", "pregnant", "stuffed"],
"🫅": ["crown", "monarch", "noble", "person", "regal", "royal", "royalty"],
"🤱": ["nursing", "baby"],
"👩‍🍼": [],
"👨‍🍼": [],
"🧑‍🍼": [],
"👩‍🍼": ["baby", "feed", "feeding", "mom", "mother", "nanny", "newborn", "nursing", "woman"],
"👨‍🍼": ["baby", "dad", "father", "feed", "feeding", "man", "nanny", "newborn", "nursing"],
"🧑‍🍼": ["baby", "feed", "feeding", "nanny", "newborn", "nursing", "parent"],
"👸": ["girl", "woman", "female", "blond", "crown", "royal", "queen"],
"🤴": ["boy", "man", "male", "crown", "royal", "king"],
"👰": ["couple", "marriage", "wedding", "woman", "bride"],
"👰": ["couple", "marriage", "wedding", "woman", "bride"],
"🤵": ["couple", "marriage", "wedding", "groom"],
"👰‍♀️": ["bride", "veil", "wedding", "woman"],
"👰‍♂️": ["man", "veil", "wedding"],
"🤵": ["couple", "marriage", "wedding", "groom"],
"🤵‍♀️": ["formal", "tuxedo", "wedding", "woman"],
"🤵‍♂️": ["formal", "groom", "man", "tuxedo", "wedding"],
"🏃‍➡️": ["facing", "fast", "hurry", "marathon", "move", "person", "quick", "race", "racing", "right", "run", "rush", "speed"],
"🏃‍♀️": ["woman", "walking", "exercise", "race", "running", "female"],
"🏃": ["man", "walking", "exercise", "race", "running"],
"🏃‍♀️‍➡️": ["facing", "fast", "hurry", "marathon", "move", "quick", "race", "racing", "right", "run", "rush", "speed", "woman"],
"🏃‍♂️": ["fast", "hurry", "man", "marathon", "move", "quick", "race", "racing", "run", "rush", "speed"],
"🏃‍♂️‍➡️": ["facing", "fast", "hurry", "man", "marathon", "move", "quick", "race", "racing", "right", "run", "rush", "speed"],
"🚶‍➡️": ["amble", "facing", "gait", "hike", "man", "pace", "pedestrian", "person", "right", "stride", "stroll", "walk", "walking"],
"🚶‍♀️": ["human", "feet", "steps", "woman", "female"],
"🚶": ["human", "feet", "steps"],
"🚶‍♀️‍➡️": ["amble", "facing", "gait", "hike", "man", "pace", "pedestrian", "right", "stride", "stroll", "walk", "walking", "woman"],
"🚶‍♂️": ["amble", "gait", "hike", "man", "pace", "pedestrian", "stride", "stroll", "walk", "walking"],
"🚶‍♂️‍➡️": ["amble", "facing", "gait", "hike", "man", "pace", "pedestrian", "right", "stride", "stroll", "walk", "walking"],
"💃": ["female", "girl", "woman", "fun"],
"🕺": ["male", "boy", "fun", "dancer"],
"👯": ["female", "bunny", "women", "girls"],
"👯‍♀️": ["bestie", "bff", "bunny", "counterpart", "dancer", "double", "ear", "identical", "pair", "party", "partying", "people", "soulmate", "twin", "twinsies", "women"],
"👯‍♂️": ["male", "bunny", "men", "boys"],
"👫": ["pair", "people", "human", "love", "date", "dating", "like", "affection", "valentines", "marriage"],
"🧑‍🤝‍🧑": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "human"],
"👬": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "man", "human"],
"👭": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "female", "human"],
"🫂": [],
"🫂": ["comfort", "embrace", "farewell", "friendship", "goodbye", "hello", "hug", "hugging", "love", "people", "thanks"],
"🙇‍♀️": ["woman", "female", "girl"],
"🙇": ["man", "male", "boy"],
"🙇‍♂️": ["apology", "ask", "beg", "bow", "bowing", "favor", "forgive", "gesture", "man", "meditate", "meditation", "pity", "regret", "sorry"],
"🤦": ["again", "bewilder", "disbelief", "exasperation", "facepalm", "no", "not", "oh", "omg", "person", "shock", "smh"],
"🤦‍♂️": ["man", "male", "boy", "disbelief"],
"🤦‍♀️": ["woman", "female", "girl", "disbelief"],
"🤷": ["woman", "female", "girl", "confused", "indifferent", "doubt"],
"🤷‍♀️": ["doubt", "dunno", "guess", "idk", "ignorance", "indifference", "knows", "maybe", "shrug", "shrugging", "whatever", "who", "woman"],
"🤷‍♂️": ["man", "male", "boy", "confused", "indifferent", "doubt"],
"💁": ["female", "girl", "woman", "human", "information"],
"💁‍♀️": ["fetch", "flick", "flip", "gossip", "hand", "sarcasm", "sarcastic", "sassy", "seriously", "tipping", "whatever", "woman"],
"💁‍♂️": ["male", "boy", "man", "human", "information"],
"🙅": ["female", "girl", "woman", "nope"],
"🙅‍♀️": ["forbidden", "gesture", "hand", "no", "not", "prohibit", "woman"],
"🙅‍♂️": ["male", "boy", "man", "nope"],
"🙆": ["women", "girl", "female", "pink", "human", "woman"],
"🙆‍♀️": ["exercise", "gesture", "gesturing", "hand", "ok", "omg", "woman"],
"🙆‍♂️": ["men", "boy", "male", "blue", "human", "man"],
"🙋": ["female", "girl", "woman"],
"🙋‍♀️": ["gesture", "hand", "here", "know", "me", "pick", "question", "raise", "raising", "woman"],
"🙋‍♂️": ["male", "boy", "man"],
"🙎": ["female", "girl", "woman"],
"🙎‍♀️": ["disappointed", "downtrodden", "frown", "grimace", "pouting", "scowl", "sulk", "upset", "whine", "woman"],
"🙎‍♂️": ["male", "boy", "man"],
"🙍": ["female", "girl", "woman", "sad", "depressed", "discouraged", "unhappy"],
"🙍‍♀️": ["annoyed", "disappointed", "disgruntled", "disturbed", "frown", "frowning", "frustrated", "gesture", "irritated", "upset", "woman"],
"🙍‍♂️": ["male", "boy", "man", "sad", "depressed", "discouraged", "unhappy"],
"💇": ["female", "girl", "woman"],
"💇‍♀️": ["barber", "beauty", "chop", "cosmetology", "cut", "groom", "hair", "haircut", "parlor", "person", "shears", "style", "woman"],
"💇‍♂️": ["male", "boy", "man"],
"💆": ["female", "girl", "woman", "head"],
"💆‍♀️": ["face", "getting", "headache", "massage", "relax", "relaxing", "salon", "soothe", "spa", "tension", "therapy", "treatment", "woman"],
"💆‍♂️": ["male", "boy", "man", "head"],
"🧖": ["day", "luxurious", "pamper", "person", "relax", "room", "sauna", "spa", "steam", "steambath", "unwind"],
"🧖‍♀️": ["female", "woman", "spa", "steamroom", "sauna"],
"🧖‍♂️": ["male", "man", "spa", "steamroom", "sauna"],
"🧏": ["accessibility", "deaf", "ear", "gesture", "hear", "person"],
"🧏‍♀️": ["woman", "female"],
"🧏‍♂️": ["man", "male"],
"🧍": ["person", "stand", "standing"],
"🧍‍♀️": ["woman", "female"],
"🧍‍♂️": ["man", "male"],
"🧎": ["kneel", "kneeling", "knees", "person"],
"🧎‍➡️": ["facing", "kneel", "kneeling", "knees", "person", "right"],
"🧎‍♀️": ["woman", "female"],
"🧎‍♀️‍➡️": ["facing", "kneel", "kneeling", "knees", "right", "woman"],
"🧎‍♂️": ["man", "male"],
"🧎‍♂️‍➡️": ["facing", "kneel", "kneeling", "knees", "man", "right"],
"🧑‍🦯": ["accessibility", "blind"],
"🧑‍🦯‍➡️": ["accessibility", "blind", "cane", "facing", "person", "probing", "right", "white"],
"👩‍🦯": ["woman", "female", "accessibility", "blind"],
"👩‍🦯‍➡️": ["accessibility", "blind", "cane", "facing", "probing", "right", "white", "woman"],
"👨‍🦯": ["man", "male", "accessibility", "blind"],
"👨‍🦯‍➡️": ["accessibility", "blind", "cane", "facing", "man", "probing", "right", "white"],
"🧑‍🦼": ["accessibility"],
"🧑‍🦼‍➡️": ["accessibility", "facing", "motorized", "person", "right", "wheelchair"],
"👩‍🦼": ["woman", "female", "accessibility"],
"👩‍🦼‍➡️": ["accessibility", "facing", "motorized", "right", "wheelchair", "woman"],
"👨‍🦼": ["man", "male", "accessibility"],
"👨‍🦼‍➡️": ["accessibility", "facing", "man", "motorized", "right", "wheelchair"],
"🧑‍🦽": ["accessibility"],
"🧑‍🦽‍➡️": ["accessibility", "facing", "manual", "person", "right", "wheelchair"],
"👩‍🦽": ["woman", "female", "accessibility"],
"👩‍🦽‍➡️": ["accessibility", "facing", "manual", "right", "wheelchair", "woman"],
"👨‍🦽": ["man", "male", "accessibility"],
"👨‍🦽‍➡️": ["accessibility", "facing", "man", "manual", "right", "wheelchair"],
"💑": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"],
"👩‍❤️‍👨": ["anniversary", "babe", "bae", "couple", "dating", "heart", "kiss", "love", "man", "person", "relationship", "romance", "together", "woman", "you"],
"👩‍❤️‍👩": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"],
"👨‍❤️‍👨": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"],
"💏": ["pair", "valentines", "love", "like", "dating", "marriage"],
"👩‍❤️‍💋‍👨": ["anniversary", "babe", "bae", "couple", "date", "dating", "heart", "kiss", "love", "man", "mwah", "person", "romance", "together", "woman", "xoxo"],
"👩‍❤️‍💋‍👩": ["pair", "valentines", "love", "like", "dating", "marriage"],
"👨‍❤️‍💋‍👨": ["pair", "valentines", "love", "like", "dating", "marriage"],
"👨‍👩‍👦": ["boy", "child", "family", "man", "woman"],
"👪": ["home", "parents", "child", "mom", "dad", "father", "mother", "people", "human"],
"👨‍👩‍👧": ["home", "parents", "people", "human", "child"],
"👨‍👩‍👧‍👦": ["home", "parents", "people", "human", "children"],
@@ -391,6 +454,10 @@
"👨‍👧‍👦": ["home", "parent", "people", "human", "children"],
"👨‍👦‍👦": ["home", "parent", "people", "human", "children"],
"👨‍👧‍👧": ["home", "parent", "people", "human", "children"],
"🧑‍🧑‍🧒": ["adult", "child", "family"],
"🧑‍🧑‍🧒‍🧒": ["adult", "child", "family"],
"🧑‍🧒": ["adult", "child", "family"],
"🧑‍🧒‍🧒": ["adult", "child", "family"],
"🧶": ["ball", "crochet", "knit"],
"🧵": ["needle", "sewing", "spool", "string"],
"🧥": ["jacket"],
@@ -415,7 +482,7 @@
"👢": ["shoes", "fashion"],
"👞": ["fashion", "male"],
"👟": ["shoes", "sports", "sneakers"],
"🩴": [],
"🩴": ["beach", "flip", "flop", "sandal", "sandals", "shoe", "thong", "thongs", "zōri"],
"🩰": ["shoes", "sports"],
"🧦": ["stockings", "clothes"],
"🧤": ["hands", "winter", "clothes"],
@@ -424,7 +491,7 @@
"🎩": ["magic", "gentleman", "classy", "circus"],
"🧢": ["cap", "baseball"],
"⛑": ["construction", "build"],
"🪖": [],
"🪖": ["army", "helmet", "military", "soldier", "war", "warrior"],
"🎓": ["school", "college", "degree", "university", "graduation", "cap", "hat", "legal", "learn", "education"],
"👑": ["king", "kod", "leader", "royalty", "lord"],
"🎒": ["student", "education", "bag", "backpack"],
@@ -475,6 +542,7 @@
"🐦‍⬛": ["animal", "nature", "bird", "black", "crow", "raven", "rook"],
"🦅": ["animal", "nature", "bird"],
"🦉": ["animal", "nature", "bird", "hoot"],
"🐦‍🔥": ["animal", "nature", "ascend", "ascension", "emerge", "fantasy", "firebird", "glory", "immortal", "phoenix", "rebirth", "reincarnation", "reinvent", "renewal", "revival", "revive", "rise", "transform"],
"🦇": ["animal", "nature", "blind", "vampire"],
"🐺": ["animal", "nature", "wild"],
"🐗": ["animal", "nature"],
@@ -575,6 +643,7 @@
"🌿": ["vegetable", "plant", "medicine", "weed", "grass", "lawn"],
"☘": ["vegetable", "plant", "nature", "irish", "clover"],
"🍀": ["vegetable", "plant", "nature", "lucky", "irish"],
"🪾": ["plant", "nature", "bare", "barren", "branches", "dead", "drought", "leafless", "tree", "trunk", "winter", "wood"],
"🎍": ["plant", "nature", "vegetable", "panda", "pine_decoration"],
"🎋": ["plant", "nature", "branch", "summer"],
"🍃": ["nature", "plant", "tree", "vegetable", "grass", "lawn", "spring"],
@@ -644,10 +713,11 @@
"💧": ["water", "drip", "faucet", "spring"],
"💦": ["water", "drip", "oops"],
"🌊": ["sea", "water", "wave", "nature", "tsunami", "disaster"],
"🪷": [],
"🪸": [],
"🪹": [],
"🪺": [],
"🪷": ["plant", "nature", "beauty", "buddhism", "calm", "flower", "hinduism", "lotus", "peace", "purity", "serenity"],
"🪸": ["animal", "nature", "change", "climate", "coral", "ocean", "reef", "sea"],
"🪹": ["plant", "nature", "branch", "empty", "home", "nest", "nesting"],
"🪺": ["plant", "nature", "bird", "branch", "egg", "eggs", "nest", "nesting"],
"🍋‍🟩": ["acidity", "citrus", "cocktail", "fruit", "garnish", "key", "lime", "margarita", "mojito", "refreshing", "salsa", "sour", "tangy", "tequila", "tropical", "zest"],
"🍏": ["fruit", "nature"],
"🍎": ["fruit", "mac", "school"],
"🍐": ["fruit", "nature", "food"],
@@ -667,6 +737,8 @@
"🥑": ["fruit", "food"],
"🫛": ["beans", "edamame", "legume", "pea", "pod", "vegetable", "food"],
"🥦": ["fruit", "food", "vegetable"],
"🍄‍🟫": ["food", "fungi", "fungus", "mushroom", "nature", "pizza", "portobello", "shiitake", "shroom", "spore", "sprout", "toppings", "truffle", "vegetable", "vegetarian", "veggie"],
"🫜": ["beet", "food", "garden", "radish", "root", "salad", "turnip", "vegetable", "vegetarian"],
"🍅": ["fruit", "vegetable", "nature", "food"],
"🍆": ["vegetable", "nature", "food", "aubergine"],
"🥒": ["fruit", "food", "pickle"],
@@ -759,7 +831,7 @@
"🍵": ["drink", "bowl", "breakfast", "green", "british"],
"🥤": ["drink", "soda"],
"☕": ["beverage", "caffeine", "latte", "espresso"],
"🫖": [],
"🫖": ["brew", "drink", "food", "pot", "tea", "teapot"],
"🧋": ["tapioca"],
"🍼": ["food", "container", "milk"],
"🧃": ["food", "drink"],
@@ -772,9 +844,9 @@
"🥣": ["food", "breakfast", "cereal", "oatmeal", "porridge"],
"🥡": ["food", "leftovers"],
"🥢": ["food"],
"🫗": [],
"🫘": [],
"🫙": [],
"🫗": ["accident", "drink", "empty", "glass", "liquid", "oops", "pour", "pouring", "spill", "water"],
"🫘": ["beans", "food", "kidney", "legume", "small"],
"🫙": ["condiment", "container", "empty", "jar", "nothing", "sauce", "store"],
"⚽": ["sports", "football"],
"🏀": ["sports", "balls", "NBA"],
"🏈": ["sports", "balls", "NFL"],
@@ -788,6 +860,7 @@
"⛳": ["sports", "business", "flag", "hole", "summer"],
"🏌️‍♀️": ["sports", "business", "woman", "female"],
"🏌": ["sports", "business"],
"🏌️‍♂️": ["sport", "ball", "birdie", "caddy", "driving", "golf", "golfing", "green", "man", "pga", "putt", "range", "tee"],
"🏓": ["sports", "pingpong"],
"🏸": ["sports"],
"🥅": ["sports"],
@@ -799,10 +872,13 @@
"⛷": ["sports", "winter", "snow"],
"🏂": ["sports", "winter"],
"🤺": ["sports", "fencing", "sword"],
"🤼": ["sport", "combat", "duel", "grapple", "people", "ring", "tournament", "wrestle", "wrestling"],
"🤼‍♀️": ["sports", "wrestlers"],
"🤼‍♂️": ["sports", "wrestlers"],
"🤸": ["sport", "active", "cartwheel", "cartwheeling", "excited", "flip", "gymnastics", "happy", "person", "somersault"],
"🤸‍♀️": ["gymnastics"],
"🤸‍♂️": ["gymnastics"],
"🤾": ["sport", "athletics", "ball", "catch", "chuck", "handball", "hurl", "lob", "person", "pitch", "playing", "throw", "toss"],
"🤾‍♀️": ["sports"],
"🤾‍♂️": ["sports"],
"⛸": ["sports"],
@@ -815,32 +891,42 @@
"🥋": ["judo", "karate", "taekwondo"],
"🚣‍♀️": ["sports", "hobby", "water", "ship", "woman", "female"],
"🚣": ["sports", "hobby", "water", "ship"],
"🚣‍♂️": ["sport", "boat", "canoe", "cruise", "fishing", "lake", "man", "oar", "paddle", "raft", "river", "row", "rowboat", "rowing"],
"🧗": ["climb", "climber", "climbing", "mountain", "person", "rock", "scale", "up"],
"🧗‍♀️": ["sports", "hobby", "woman", "female", "rock"],
"🧗‍♂️": ["sports", "hobby", "man", "male", "rock"],
"🏊‍♀️": ["sports", "exercise", "human", "athlete", "water", "summer", "woman", "female"],
"🏊": ["sports", "exercise", "human", "athlete", "water", "summer"],
"🏊‍♂️": ["sport", "freestyle", "man", "swim", "swimmer", "swimming", "triathlon"],
"🤽": ["sport", "person", "playing", "polo", "swimming", "water", "waterpolo"],
"🤽‍♀️": ["sports", "pool"],
"🤽‍♂️": ["sports", "pool"],
"🧘": ["cross", "legged", "legs", "lotus", "meditation", "peace", "person", "position", "relax", "serenity", "yoga", "yogi", "zen"],
"🧘‍♀️": ["woman", "female", "meditation", "yoga", "serenity", "zen", "mindfulness"],
"🧘‍♂️": ["man", "male", "meditation", "yoga", "serenity", "zen", "mindfulness"],
"🏄‍♀️": ["sports", "ocean", "sea", "summer", "beach", "woman", "female"],
"🏄": ["sports", "ocean", "sea", "summer", "beach"],
"🏄‍♂️": ["sport", "beach", "man", "ocean", "surf", "surfer", "surfing", "swell", "waves"],
"🛀": ["clean", "shower", "bathroom"],
"⛹️‍♀️": ["sports", "human", "woman", "female"],
"⛹": ["sports", "human"],
"⛹️‍♂️": ["sport", "athletic", "ball", "basketball", "bouncing", "championship", "dribble", "man", "net", "player", "throw"],
"🏋️‍♀️": ["sports", "training", "exercise", "woman", "female"],
"🏋": ["sports", "training", "exercise"],
"🏋️‍♂️": ["sport", "barbell", "bodybuilder", "deadlift", "lifter", "lifting", "man", "powerlifting", "weight", "weightlifter", "weights", "workout"],
"🚴‍♀️": ["sports", "bike", "exercise", "hipster", "woman", "female"],
"🚴": ["sports", "bike", "exercise", "hipster"],
"🚴‍♂️": ["sport", "bicycle", "bicyclist", "bike", "biking", "cycle", "cyclist", "man", "riding"],
"🚵‍♀️": ["transportation", "sports", "human", "race", "bike", "woman", "female"],
"🚵": ["transportation", "sports", "human", "race", "bike"],
"🚵‍♂️": ["sport", "bicycle", "bicyclist", "bike", "biking", "cycle", "cyclist", "man", "mountain", "riding"],
"🏇": ["animal", "betting", "competition", "gambling", "luck"],
"🤿": ["sports"],
"🪀": ["sports"],
"🪁": ["sports"],
"🦺": ["sports"],
"🪡": [],
"🪢": [],
"🪡": ["embroidery", "needle", "sew", "sewing", "stitches", "sutures", "tailoring", "thread"],
"🪢": ["cord", "knot", "rope", "tangled", "tie", "twine", "twist"],
"🕴": ["suit", "business", "levitate", "hover", "jump"],
"🏆": ["win", "award", "contest", "place", "ftw", "ceremony"],
"🎽": ["play", "pageant"],
@@ -856,6 +942,7 @@
"🎭": ["acting", "theater", "drama"],
"🎨": ["design", "paint", "draw", "colors"],
"🎪": ["festival", "carnival", "party"],
"🤹": ["sport", "act", "balance", "balancing", "handle", "juggle", "juggling", "manage", "multitask", "person", "skill"],
"🤹‍♀️": ["juggle", "balance", "skill", "multitask"],
"🤹‍♂️": ["juggle", "balance", "skill", "multitask"],
"🎤": ["sound", "music", "PA", "sing", "talkshow"],
@@ -872,6 +959,7 @@
"🪕": ["music", "instrument"],
"🪗": ["music", "instrument"],
"🪘": ["music", "instrument"],
"🪉": ["cupid", "harp", "instrument", "love", "music", "orchestra"],
"🎬": ["movie", "film", "record"],
"🎮": ["play", "console", "PS4", "controller"],
"👾": ["game", "arcade", "play"],
@@ -881,11 +969,11 @@
"🎰": ["bet", "gamble", "vegas", "fruit machine", "luck", "casino"],
"🧩": ["interlocking", "puzzle", "piece"],
"🎳": ["sports", "fun", "play"],
"🪄": [],
"🪅": [],
"🪆": [],
"🪬": [],
"🪩": [],
"🪄": ["magic", "magician", "wand", "witch", "wizard"],
"🪅": ["candy", "celebrate", "celebration", "cinco", "de", "festive", "mayo", "party", "pinada", "pinata", "piñata"],
"🪆": ["babooshka", "baboushka", "babushka", "doll", "dolls", "matryoshka", "nesting", "russia"],
"🪬": ["amulet", "fatima", "fortune", "guide", "hamsa", "hand", "mary", "miriam", "palm", "protect", "protection"],
"🪩": ["ball", "dance", "disco", "glitter", "mirror", "party"],
"🚗": ["red", "transportation", "vehicle"],
"🚕": ["uber", "vehicle", "cars", "transportation"],
"🚙": ["transportation", "vehicle"],
@@ -941,7 +1029,7 @@
"🚀": ["launch", "ship", "staffmode", "NASA", "outer space", "outer_space", "fly"],
"🛰": ["communication", "gps", "orbit", "spaceflight", "NASA", "ISS"],
"🛻": ["car"],
"🛼": [],
"🛼": ["blades", "roller", "skate", "skates", "sport"],
"💺": ["sit", "airplane", "transport", "bus", "flight", "fly"],
"🛶": ["boat", "paddle", "water", "ship"],
"⚓": ["ship", "ferry", "sea", "boat"],
@@ -1013,12 +1101,12 @@
"🕋": ["mecca", "mosque", "islam"],
"⛩": ["temple", "japan", "kyoto"],
"🛕": ["temple"],
"🪨": [],
"🪵": [],
"🛖": [],
"🛝": [],
"🛞": [],
"🛟": [],
"🪨": ["boulder", "heavy", "rock", "solid", "stone", "tough"],
"🪵": ["log", "lumber", "timber", "wood"],
"🛖": ["home", "house", "hut", "roundhouse", "shelter", "yurt"],
"🛝": ["amusement", "park", "play", "playground", "playing", "slide", "sliding", "theme"],
"🛞": ["car", "circle", "tire", "turn", "vehicle", "wheel"],
"🛟": ["buoy", "float", "life", "lifesaver", "preserver", "rescue", "ring", "safety", "save", "saver", "swim"],
"⌚": ["time", "accessories"],
"📱": ["technology", "apple", "gadgets", "dial"],
"📲": ["iphone", "incoming"],
@@ -1059,7 +1147,7 @@
"⌛": ["time", "clock", "oldschool", "limit", "exam", "quiz", "test"],
"📡": ["communication", "future", "radio", "space"],
"🔋": ["power", "energy", "sustain"],
"🪫": [],
"🪫": ["battery", "drained", "electronic", "energy", "low", "power"],
"🔌": ["charger", "power"],
"💡": ["light", "electricity", "idea"],
"🔦": ["dark", "camping", "sight", "night"],
@@ -1075,9 +1163,11 @@
"💰": ["dollar", "payment", "coins", "sale"],
"🪙": ["dollar", "payment", "coins", "sale"],
"💳": ["money", "sales", "dollar", "bill", "payment", "shopping"],
"🪪": [],
"🪪": ["card", "credentials", "document", "id", "identification", "license", "security"],
"🥾": ["backpacking", "boot", "brown", "camping", "hiking", "outdoors", "shoe"],
"💎": ["blue", "ruby", "diamond", "jewelry"],
"⚖": ["law", "fairness", "weight"],
"⛓️‍💥": ["break", "breaking", "broken", "chain", "cuffs", "freedom"],
"🧰": ["tools", "diy", "fix", "maintainer", "mechanic"],
"🔧": ["tools", "diy", "ikea", "fix", "maintainer"],
"🔨": ["tools", "build", "create"],
@@ -1093,6 +1183,7 @@
"🪛": ["tool"],
"🪝": ["tool"],
"🪜": ["tool"],
"🪏": ["bury", "dig", "garden", "hole", "plant", "scoop", "shovel", "snow", "spade"],
"🧱": ["bricks"],
"⛓": ["lock", "arrest"],
"🧲": ["attraction", "magnetic"],
@@ -1123,8 +1214,8 @@
"🩺": ["health", "hospital", "medicine", "needle", "doctor", "nurse"],
"🪒": ["health"],
"🪮": ["afro", "comb", "hair", "pick"],
"🩻": [],
"🩼": [],
"🩻": ["bones", "doctor", "medical", "skeleton", "skull", "x-ray", "xray"],
"🩼": ["aid", "cane", "crutch", "disability", "help", "hurt", "injured", "mobility", "stick"],
"🧬": ["biologist", "genetics", "life"],
"🧫": ["bacteria", "biology", "culture", "lab"],
"🧪": ["chemistry", "experiment", "lab", "science"],
@@ -1159,7 +1250,7 @@
"🪤": ["household"],
"🪣": ["household"],
"🪥": ["household"],
"🫧": [],
"🫧": ["bubble", "bubbles", "burp", "clean", "floating", "pearl", "soap", "underwater"],
"⛱": ["weather", "summer"],
"🗿": ["rock", "easter island", "moai"],
"🛍": ["mall", "buy", "purchase"],
@@ -1249,8 +1340,8 @@
"🖌": ["drawing", "creativity", "art"],
"🔍": ["search", "zoom", "find", "detective"],
"🔎": ["search", "zoom", "find", "detective"],
"🪦": [],
"🪧": [],
"🪦": ["cemetery", "dead", "grave", "graveyard", "headstone", "memorial", "rip", "tomb", "tombstone"],
"🪧": ["card", "demonstration", "notice", "picket", "placard", "plaque", "protest", "sign"],
"💯": ["score", "perfect", "numbers", "century", "exam", "quiz", "test", "pass", "hundred"],
"🔢": ["numbers", "blue-square"],
"🩷": ["love", "like", "affection", "valentines"],
@@ -1275,8 +1366,8 @@
"💘": ["love", "like", "heart", "affection", "valentines"],
"💝": ["love", "valentines"],
"💟": ["purple-square", "love", "like"],
"❤️‍🔥": [],
"❤️‍🩹": [],
"❤️‍🔥": ["burn", "fire", "heart", "love", "lust", "sacred"],
"❤️‍🩹": ["healthier", "heart", "improving", "mending", "recovering", "recuperating", "well"],
"☮": ["hippie"],
"✝": ["christianity"],
"☪": ["islam"],
@@ -1304,6 +1395,8 @@
"♓": ["purple-square", "sign", "zodiac", "astrology"],
"🆔": ["purple-square", "words"],
"⚛": ["science", "physics", "chemistry"],
"♀️": ["female", "sign", "woman", "pink-square"],
"♂️": ["male", "man", "sign", "blue-square"],
"⚧️": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"],
"🈳": ["kanji", "japanese", "chinese", "empty", "sky", "blue-square", "aki"],
"🈹": ["cut", "divide", "chinese", "kanji", "pink-square", "waribiki"],
@@ -1459,13 +1552,15 @@
"": ["math", "calculation", "subtract", "less"],
"➗": ["divide", "math", "calculation"],
"✖️": ["math", "calculation"],
"🟰": [],
"🟰": ["answer", "equal", "equality", "equals", "heavy", "math", "sign"],
"♾": ["forever"],
"💲": ["money", "sales", "payment", "currency", "buck"],
"💱": ["money", "sales", "dollar", "travel"],
"⚕️": ["aesculapius", "medical", "medicine", "staff", "symbol"],
"©️": ["ip", "license", "circle", "law", "legal"],
"®️": ["alphabet", "circle"],
"™️": ["trademark", "brand", "law", "legal"],
"🫟": ["drip", "holi", "ink", "liquid", "mess", "paint", "spill", "splatter", "stain"],
"🔚": ["words", "arrow"],
"🔙": ["arrow", "words", "return"],
"🔛": ["arrow", "words"],
@@ -1576,6 +1671,7 @@
"🇧🇲": ["bm", "bermuda", "flag", "nation", "country", "banner"],
"🇧🇹": ["bt", "bhutan", "flag", "nation", "country", "banner"],
"🇧🇴": ["bo", "bolivia", "flag", "nation", "country", "banner"],
"🇧🇻": ["bv", "bouvet", "island", "flag", "nation", "country", "banner"],
"🇧🇶": ["bq", "bonaire", "flag", "nation", "country", "banner"],
"🇧🇦": ["ba", "bosnia", "herzegovina", "flag", "nation", "country", "banner"],
"🇧🇼": ["bw", "botswana", "flag", "nation", "country", "banner"],
@@ -1593,10 +1689,12 @@
"🇮🇨": ["ic", "canary", "islands", "flag", "nation", "country", "banner"],
"🇰🇾": ["ky", "cayman", "islands", "flag", "nation", "country", "banner"],
"🇨🇫": ["cf", "central", "african", "republic", "flag", "nation", "country", "banner"],
"🇪🇦": ["ea", "ceuta", "melilla", "flag", "nation", "country", "banner"],
"🇹🇩": ["td", "chad", "flag", "nation", "country", "banner"],
"🇨🇱": ["cl", "chile", "flag", "nation", "country", "banner"],
"🇨🇳": ["cn", "china", "chinese", "prc", "flag", "country", "nation", "banner"],
"🇨🇽": ["cx", "christmas", "island", "flag", "nation", "country", "banner"],
"🇨🇵": ["cp", "clipperton", "island", "flag", "nation", "country", "banner"],
"🇨🇨": ["cc", "cocos", "keeling", "islands", "flag", "nation", "country", "banner"],
"🇨🇴": ["co", "colombia", "flag", "nation", "country", "banner"],
"🇰🇲": ["km", "comoros", "flag", "nation", "country", "banner"],
@@ -1610,6 +1708,7 @@
"🇨🇾": ["cy", "cyprus", "flag", "nation", "country", "banner"],
"🇨🇿": ["cz", "czech", "republic", "flag", "nation", "country", "banner"],
"🇩🇰": ["dk", "denmark", "flag", "nation", "country", "banner"],
"🇩🇬": ["dg", "diego", "garcia", "flag", "nation", "country", "banner"],
"🇩🇯": ["dj", "djibouti", "flag", "nation", "country", "banner"],
"🇩🇲": ["dm", "dominica", "flag", "nation", "country", "banner"],
"🇩🇴": ["do", "dominican", "republic", "flag", "nation", "country", "banner"],
@@ -1646,6 +1745,7 @@
"🇬🇼": ["gw", "guiana", "bissau", "flag", "nation", "country", "banner"],
"🇬🇾": ["gy", "guyana", "flag", "nation", "country", "banner"],
"🇭🇹": ["ht", "haiti", "flag", "nation", "country", "banner"],
"🇭🇲": ["hm", "heard", "mcdonald", "islands", "flag", "nation", "country", "banner"],
"🇭🇳": ["hn", "honduras", "flag", "nation", "country", "banner"],
"🇭🇰": ["hk", "hong", "kong", "flag", "nation", "country", "banner"],
"🇭🇺": ["hu", "hungary", "flag", "nation", "country", "banner"],
@@ -1733,10 +1833,12 @@
"🇷🇴": ["ro", "romania", "flag", "nation", "country", "banner"],
"🇷🇺": ["ru", "russian", "federation", "flag", "nation", "country", "banner"],
"🇷🇼": ["rw", "rwanda", "flag", "nation", "country", "banner"],
"🇨🇶": ["cq", "sark", "flag", "nation", "country", "banner"],
"🇧🇱": ["bl", "saint", "barthélemy", "flag", "nation", "country", "banner"],
"🇸🇭": ["sh", "saint", "helena", "ascension", "tristan", "cunha", "flag", "nation", "country", "banner"],
"🇰🇳": ["kn", "saint", "kitts", "nevis", "flag", "nation", "country", "banner"],
"🇱🇨": ["lc", "saint", "lucia", "flag", "nation", "country", "banner"],
"🇲🇫": ["mf", "st", "martin", "flag", "nation", "country", "banner"],
"🇵🇲": ["pm", "saint", "pierre", "miquelon", "flag", "nation", "country", "banner"],
"🇻🇨": ["vc", "saint", "vincent", "grenadines", "flag", "nation", "country", "banner"],
"🇼🇸": ["ws", "western", "samoa", "flag", "nation", "country", "banner"],
@@ -1762,6 +1864,7 @@
"🇸🇩": ["sd", "sudan", "flag", "nation", "country", "banner"],
"🇸🇷": ["sr", "suriname", "flag", "nation", "country", "banner"],
"🇸🇿": ["sz", "eswatini", "flag", "nation", "country", "banner"],
"🇸🇯": ["sj", "svalbard", "jan", "mayen", "flag", "nation", "country", "banner"],
"🇸🇪": ["se", "sweden", "flag", "nation", "country", "banner"],
"🇨🇭": ["ch", "switzerland", "confoederatio", "helvetica", "flag", "nation", "country", "banner"],
"🇸🇾": ["sy", "syrian", "arab", "republic", "flag", "nation", "country", "banner"],
@@ -1788,6 +1891,7 @@
"🏴󠁧󠁢󠁳󠁣󠁴󠁿": ["flag", "scottish"],
"🏴󠁧󠁢󠁷󠁬󠁳󠁿": ["flag", "welsh"],
"🇺🇸": ["us", "usa", "united", "states", "america", "flag", "nation", "country", "banner"],
"🇺🇲": ["um", "us", "outlying", "islands", "flag", "nation", "country", "banner"],
"🇻🇮": ["vi", "virgin", "islands", "us", "flag", "nation", "country", "banner"],
"🇺🇾": ["uy", "uruguay", "flag", "nation", "country", "banner"],
"🇺🇿": ["uz", "uzbekistan", "flag", "nation", "country", "banner"],
@@ -36,6 +36,9 @@
"🤡":["ピエロの顔","ピエロ","顔"],
"😏":["にやにやした顔","顔","にやにや"],
"😶":["口のない顔","顔","口","静かに","沈黙"],
"🙂‍↔️":["いいえ","不賛成","顔","首を振る","首を横に振る"],
"🙂‍↕️":["はい","頷く","顔","首を振る","首を縦に振る"],
"🫩":["クマ","徹夜","疲れた","眠い","顔"],
"🫥":["点線の顔","落ち込んだ","消える","隠れる","内向的","目に見えない"],
"😐":["普通の顔","無表情","顔","平静"],
"🫤":["口が斜めになった顔","がっかり","無関心","疑い深い","不安"],
@@ -111,6 +114,38 @@
"💩":["うんち","マンガ","漫画","フン","顔","モンスター"],
"👻":["お化け","妖怪","顔","おとぎ話","ファンタジー","幽霊","モンスター","ハロウィーン"],
"💀":["ドクロ","体","死","顔","おとぎ話","モンスター","骸骨","ハロウィーン"],
"🫆":["指紋","鑑識","生体認証","セキュリティ"],
"🏃‍➡️":["ジョギング","マラソン","ランナー","ランニング","右向き","急ぐ","走る","走る人","駆け足"],
"🏃‍♀️‍➡️":["ジョギング","マラソン","ランナー","ランニング","右向き","女性","急ぐ","走る","走る女","駆け足"],
"🏃‍♂️‍➡️":["ジョギング","マラソン","ランナー","ランニング","右向き","男性","走る男"],
"🚶‍➡️":["ウォーキング","ハイキング","ぶらつく","人","右向き","大股","散歩","歩く","歩行","歩行者"],
"🚶‍♀️‍➡️":["ウォーキング","ハイキング","右向き","女性","散歩","歩く","歩く女","歩行","歩行者"],
"🚶‍♂️‍➡️":["ウォーキング","右向き","歩く男","歩行","歩行者","男性"],
"🧎‍➡️":["ひざまずく","人","右向き","座る","正座する人"],
"🧎‍♀️‍➡️":["ひざまずく","右向き","女性","座る","正座する女性"],
"🧎‍♂️‍➡️":["ひざまずく","右向き","座る","正座する男性","男性"],
"🧑‍🦯‍➡️":["アクセシビリティ","右向き","杖をついた人","目","視覚","障がい"],
"👩‍🦯‍➡️":["アクセシビリティ","右向き","女性","杖をついた女性","白杖をついた女性","目","視覚","障がい"],
"👨‍🦯‍➡️":["アクセシビリティ","右向き","杖をついた男性","男性","白杖をついた男性","目","視覚","障がい"],
"🧑‍🦼‍➡️":["アクセシビリティ","右向き","車いす","障がい","電動車椅子の人"],
"👩‍🦼‍➡️":["アクセシビリティ","右向き","女性","車いす","障がい","電動車椅子の女性"],
"👨‍🦼‍➡️":["アクセシビリティ","右向き","男性","車いす","障がい","電動車椅子の男性"],
"🧑‍🦽‍➡️":["アクセシビリティ","右向き","手動式車椅子の人","車いす","障がい"],
"👩‍🦽‍➡️":["アクセシビリティ","右向き","女性","手動式車椅子の女性","車いす","障がい"],
"👨‍🦽‍➡️":["アクセシビリティ","右向き","手動式車椅子の男性","男性","車いす","障がい"],
"👨‍👩‍👦":["女性","子供","家族","男の子","男性","親子"],
"🧑‍🧑‍🧒":["大人二人","子供一人","家族","大人二人と子供一人","親子"],
"🧑‍🧑‍🧒‍🧒":["大人二人","子供二人","家族","大人二人と子供二人","親子"],
"🧑‍🧒":["大人一人","子供一人","家族","大人一人と子供一人","親子"],
"🧑‍🧒‍🧒":["大人一人","子供二人","家族","大人一人と子供二人","親子"],
"🐦‍🔥":["ファンタジー","フェニックス","不死鳥","再生","復活","火の鳥","生まれ変わり","神話","転生","輪廻"],
"🪾":["不毛","干ばつ","葉のない木","冬"],
"🍋‍🟩":["さわやか","トロピカル","フルーツ","マルガリータ","モヒート","ライム","果物","柑橘類","緑","酸っぱい"],
"🍄‍🟫":["きのこ","キノコ","しいたけ","トリュフ","ブラウンマッシュルーム","ポートベロー","マッシュルーム","椎茸","茸","菌類"],
"🫜":["ビーツ","庭","根","カブ","野菜"],
"🪉":["オーケストラ","キューピッド","ハープ","弦楽器","愛","楽器","音楽"],
"⛓️‍💥":["くさり","チェーン","壊れた","壊れた鎖"],
"🪏":["シャベル","ショベル","スコップ","掘る","穴","鋤"],
"☠":["ドクロマーク","体","交差した骨","死","顔","モンスター","骸骨","ハロウィーン"],
"👽":["宇宙人","怪獣","異星人","顔","おとぎ話","ファンタジー","モンスター","宇宙","UFO"],
"🤖":["ロボットの顔","顔","モンスター","ロボット"],
@@ -1518,6 +1553,7 @@
"©️":["コピーライトマーク","著作権"],
"®️":["登録商標マーク","登録済み","商標"],
"™️":["商標マーク","マーク","tm","商標"],
"🫟":["しぶき","ペンキ","飛沫","飛び散り","スプラッシュ"],
"🔚":["ENDと左矢印","矢印","端"],
"🔙":["BACKと左矢印","矢印","戻る"],
"🔛":["ON!と左右矢印","矢印","マーク","オン"],
@@ -1643,6 +1679,7 @@
"🇧🇷":["ブラジル国旗","ブラジル","国旗"],
"🇧🇸":["バハマ国旗","バハマ","国旗"],
"🇧🇹":["ブータン国旗","ブータン","国旗"],
"🇧🇻":["ブーベ島の旗","ブーベ島","国旗"],
"🇧🇼":["ボツワナ国旗","ボツワナ","国旗"],
"🇧🇾":["ベラルーシ国旗","ベラルーシ","国旗"],
"🇧🇿":["ベリーズ国旗","ベリーズ","国旗"],
@@ -1658,6 +1695,8 @@
"🇨🇲":["カメルーン国旗","カメルーン","国旗"],
"🇨🇳":["中国国旗","中国","国旗"],
"🇨🇴":["コロンビア国旗","コロンビア","国旗"],
"🇨🇵":["クリッパートン島の旗","クリッパートン島","国旗"],
"🇨🇶":["サーク島の旗", "サーク島", "国旗"],
"🇨🇷":["コスタリカ国旗","コスタリカ","国旗"],
"🇨🇺":["キューバ国旗","キューバ","国旗"],
"🇨🇻":["カーボベルデ国旗","カーボ","ケープ","国旗","ベルデ"],
@@ -1666,11 +1705,13 @@
"🇨🇾":["キプロス国旗","キプロス","国旗"],
"🇨🇿":["チェコ国旗","チェコ共和国","国旗"],
"🇩🇪":["ドイツ国旗","国旗","ドイツ"],
"🇩🇬":["ディエゴガルシア島の旗","ディエゴガルシア島","国旗"],
"🇩🇯":["ジブチ国旗","ジブチ","国旗"],
"🇩🇰":["デンマーク国旗","デンマーク","国旗"],
"🇩🇲":["ドミニカ国旗","ドミニカ","国旗"],
"🇩🇴":["ドミニカ共和国国旗","ドミニカ共和国","国旗"],
"🇩🇿":["アルジェリア国旗","アルジェリア","国旗"],
"🇪🇦":["セウタ・メリリャの旗","セウタ・メリリャ","国旗"],
"🇪🇨":["エクアドル国旗","エクアドル","国旗"],
"🏴󠁧󠁢󠁥󠁮󠁧󠁿":["イングランドの旗","イングランド","旗"],
"🇪🇪":["エストニア国旗","エストニア","国旗"],
@@ -1706,6 +1747,7 @@
"🇬🇼":["ギニアビサウ国旗","ビサウ","国旗","ギニア"],
"🇬🇾":["ガイアナ国旗","国旗","ガイアナ"],
"🇭🇰":["香港の旗","中国","国旗","香港"],
"🇭🇲":["ハード島・マクドナルド諸島の旗","ハード島・マクドナルド諸島","国旗"],
"🇭🇳":["ホンジュラス国旗","国旗","ホンジュラス"],
"🇭🇷":["クロアチア国旗","クロアチア","国旗"],
"🇭🇹":["ハイチ国旗","国旗","ハイチ"],
@@ -1751,6 +1793,7 @@
"🇲🇨":["モナコ国旗","国旗","モナコ"],
"🇲🇩":["モルドバ国旗","国旗","モルドバ"],
"🇲🇪":["モンテネグロ国旗","国旗","モンテネグロ"],
"🇲🇫":["サン・マルタンの旗","サン・マルタン","国旗"],
"🇲🇬":["マダガスカル国旗","国旗","マダガスカル"],
"🇲🇭":["マーシャル諸島国旗","国旗","諸島","マーシャル"],
"🇲🇰":["マケドニア国旗","国旗","マケドニア"],
@@ -1811,6 +1854,7 @@
"🇸🇬":["シンガポール国旗","国旗","シンガポール"],
"🇸🇭":["セントヘレナ島の旗","旗","ヘレナ","セント"],
"🇸🇮":["スロベニア国旗","国旗","スロベニア"],
"🇸🇯":["スバールバル諸島・ヤンマイエン島の旗","スバールバル諸島・ヤンマイエン島","国旗"],
"🇸🇰":["スロバキア国旗","国旗","スロバキア"],
"🇸🇱":["シエラレオネ国旗","国旗","シエラレオネ"],
"🇸🇲":["サンマリノ国旗","国旗","サンマリノ"],
@@ -1842,6 +1886,7 @@
"🇹🇿":["タンザニア国旗","国旗","タンザニア"],
"🇺🇦":["ウクライナ国旗","国旗","ウクライナ"],
"🇺🇬":["ウガンダ国旗","国旗","ウガンダ"],
"🇺🇲":["合衆国領有小離島の旗","合衆国領有小離島","国旗"],
"🇺🇳":["国連の旗","旗","国連","連合","国際"],
"🇺🇸":["アメリカ国旗","アメリカ","旗","合衆","合衆国","アメリカ合衆国","合衆国領有小離島"],
"🇺🇾":["ウルグアイ国旗","国旗","ウルグアイ"],
@@ -36,6 +36,9 @@
"🤡": ["ぴえろのかお","ぴえろ","かお"],
"😏": ["にやにやしたかお","かお","にやにや"],
"😶": ["くちのないかお","かお","くち","しずかに","ちんもく"],
"🙂‍↔️": ["いいえ","ふさんせい","かお","くびをふる","くびをよこにふる"],
"🙂‍↕️": ["はい","うなずく","かお","くびをふる","くびをたてにふる"],
"🫩": ["くま","てつや","つかれた","ねむい","かお"],
"🫥": ["てんせんのかお","おちこんだ","きえる","かくれる","ないこうてき","めにみえない"],
"😐": ["ふつうのかお","むひょうじょう","かお","へいせい"],
"🫤": ["くちがななめになったかお","がっかり","むかんしん","うたがいぶかい","ふあん"],
@@ -111,6 +114,38 @@
"💩": ["うんち","まんが","ふん","かお","もんすたー"],
"👻": ["おばけ","ようかい","かお","おとぎばなし","ふぁんたじー","ゆうれい","もんすたー","はろうぃーん"],
"💀": ["どくろ","からだ","し","かお","おとぎばなし","もんすたー","がいこつ","はろうぃーん"],
"🫆": ["しもん","かんしき","せいたいにんしょう","せきゅりてぃ"],
"🏃‍➡️": ["じょぎんぐ","まらそん","らんなー","らんにんぐ","みぎむき","いそぐ","はしる","はしるひと","かけあし"],
"🏃‍♀️‍➡️": ["じょぎんぐ","まらそん","らんなー","らんにんぐ","みぎむき","じょせい","いそぐ","はしる","はしるおんな","かけあし"],
"🏃‍♂️‍➡️": ["じょぎんぐ","まらそん","らんなー","らんにんぐ","みぎむき","だんせい","はしるおとこ"],
"🚶‍➡️": ["うぉーきんぐ","はいきんぐ","ぶらつく","ひと","みぎむき","おおまた","さんぽ","あるく","ほこう","ほこうしゃ"],
"🚶‍♀️‍➡️": ["うぉーきんぐ","はいきんぐ","みぎむき","じょせい","さんぽ","あるく","あるくおんな","ほこう","ほこうしゃ"],
"🚶‍♂️‍➡️": ["うぉーきんぐ","みぎむき","あるくおとこ","ほこう","ほこうしゃ","だんせい"],
"🧎‍➡️": ["ひざまずく","ひと","みぎむき","すわる","せいざするひと"],
"🧎‍♀️‍➡️": ["ひざまずく","みぎむき","じょせい","すわる","せいざするじょせい"],
"🧎‍♂️‍➡️": ["ひざまずく","みぎむき","すわる","せいざするだんせい","だんせい"],
"🧑‍🦯‍➡️": ["あくせしびりてぃ","みぎむき","つえをついたひと","め","しかく","しょうがい"],
"👩‍🦯‍➡️": ["あくせしびりてぃ","みぎむき","じょせい","つえをついたじょせい","はくじょうをついたじょせい","め","しかく","しょうがい"],
"👨‍🦯‍➡️": ["あくせしびりてぃ","みぎむき","つえをついただんせい","だんせい","はくじょうをついただんせい","め","しかく","しょうがい"],
"🧑‍🦼‍➡️": ["あくせしびりてぃ","みぎむき","くるまいす","しょうがい","でんどうくるまいすのひと"],
"👩‍🦼‍➡️": ["あくせしびりてぃ","みぎむき","じょせい","くるまいす","しょうがい","でんどうくるまいすのじょせい"],
"👨‍🦼‍➡️": ["あくせしびりてぃ","みぎむき","だんせい","くるまいす","しょうがい","でんどうくるまいすのだんせい"],
"🧑‍🦽‍➡️": ["あくせしびりてぃ","みぎむき","しゅどうしきくるまいすのひと","くるまいす","しょうがい"],
"👩‍🦽‍➡️": ["あくせしびりてぃ","みぎむき","じょせい","しゅどうしきくるまいすのじょせい","くるまいす","しょうがい"],
"👨‍🦽‍➡️": ["あくせしびりてぃ","みぎむき","しゅどうしきくるまいすのだんせい","だんせい","くるまいす","しょうがい"],
"👨‍👩‍👦": ["じょせい","こども","かぞく","おとこのこ","だんせい","おやこ"],
"🧑‍🧑‍🧒": ["おとなふたり","こどもひとり","かぞく","おとなふたりとこどもひとり","おやこ"],
"🧑‍🧑‍🧒‍🧒": ["おとなふたり","こどもふたり","かぞく","おとなふたりとこどもふたり","おやこ"],
"🧑‍🧒": ["おとなひとり","こどもひとり","かぞく","おとなひとりとこどもひとり","おやこ"],
"🧑‍🧒‍🧒": ["おとなひとり","こどもふたり","かぞく","おとなひとりとこどもふたり","おやこ"],
"🐦‍🔥": ["ふぁんたじー","ふぇにっくす","ふしちょう","さいせい","ふっかつ","ひのとり","うまれかわり","しんわ","てんせい","りんね"],
"🪾": ["ふもう","かんばつ","はのないき","ふゆ"],
"🍋‍🟩": ["さわやか","とろぴかる","ふるーつ","まるがりーた","もひーと","らいむ","くだもの","かんきつるい","みどり","すっぱい"],
"🍄‍🟫": ["きのこ","しいたけ","とりゅふ","ぶらうんまっしゅるーむ","ぽーとべろー","まっしゅるーむ","たけ","きんるい"],
"🫜": ["びーつ","にわ","ね","かぶ","やさい"],
"🪉": ["おーけすとら","きゅーぴっど","はーぷ","げんがっき","あい","がっき","おんがく"],
"⛓️‍💥": ["くさり","ちぇーん","こわれた","こわれたくさり"],
"🪏": ["しゃべる","しょべる","すこっぷ","ほる","あな","すき"],
"☠": ["どくろまーく","からだ","こうさしたほね","し","かお","もんすたー","がいこつ","はろうぃーん"],
"👽": ["うちゅうじん","かいじゅう","いせいじん","かお","おとぎばなし","ふぁんたじー","もんすたー","うちゅう","UFO"],
"🤖": ["ろぼっとのかお","かお","もんすたー","ろぼっと"],
@@ -382,9 +417,9 @@
"🚶‍♀️": ["あるくじょせい","はいきんぐ","ほこうしゃ","あるく","うぉーきんぐ","じょせい","おんな"],
"🚶": ["あるくひと","はいきんぐ","ほこうしゃ","あるく","うぉーきんぐ"],
"🚶‍♂️": ["あるくだんせい","はいきんぐ","ほこうしゃ","あるく","うぉーきんぐ","おとこ","だんせい"],
"👩‍🦯": ["しろつえをもったじょせい","あくせしびりてぃ","めがふじゆう","じょせい","おんな"],
"🧑‍🦯": ["しろつえをもったひと","あくせしびりてぃ","めがふじゆう"],
"👨‍🦯": ["しろつえをもっただんせい","あくせしびりてぃ","めがふじゆう","だんせい","おとこ"],
"👩‍🦯": ["はくじょうをもったじょせい","あくせしびりてぃ","めがふじゆう","じょせい","おんな"],
"🧑‍🦯": ["はくじょうをもったひと","あくせしびりてぃ","めがふじゆう"],
"👨‍🦯": ["はくじょうをもっただんせい","あくせしびりてぃ","めがふじゆう","だんせい","おとこ"],
"🏃‍♀️": ["はしるじょせい","まらそん","らんなー","らんにんぐ","じょせい","おんな"],
"🏃": ["はしるひと","まらそん","らんなー","らんにんぐ"],
"🏃‍♂️": ["はしるだんせい","まらそん","らんなー","らんにんぐ","おとこ","だんせい"],
@@ -1129,7 +1164,7 @@
"🧾": ["りょうしゅうしょ","かいけい","ぼき","しょうこ","しょうめい"],
"💎": ["ほうせき","だいあもんど","じゅえる","ろまんす"],
"⚖": ["はかり","てんびん","こうせい","てんびんざ","ものさし","どうぐ","じゅうりょう","せいざ"],
"🦯": ["しろつえ","あくせしびりてぃ","めがふじゆう"],
"🦯": ["はくじょう","あくせしびりてぃ","めがふじゆう"],
"🧰": ["どうぐばこ","むね","せいびし","こうぐ"],
"🔧": ["れんち","どうぐ"],
"🪛": ["どらいばー","ねじ","こうぐ"],
@@ -1518,6 +1553,7 @@
"©️": ["こぴーらいとまーく","ちょさくけん"],
"®️": ["とうろくしょうひょうまーく","とうろくずみ","しょうひょう"],
"™️": ["しょうひょうまーく","まーく","tm","しょうひょう"],
"🫟": ["しぶき","ぺんき","ひまつ","とびちり","すぷらっしゅ"],
"🔚": ["ENDとひだりやじるし","やじるし","はじ"],
"🔙": ["BACKとひだりやじるし","やじるし","もどる"],
"🔛": ["ON!とさゆうやじるし","やじるし","まーく","おん"],
@@ -1643,6 +1679,7 @@
"🇧🇷": ["ぶらじるこっき","ぶらじる","こっき"],
"🇧🇸": ["ばはまこっき","ばはま","こっき"],
"🇧🇹": ["ぶーたんこっき","ぶーたん","こっき"],
"🇧🇻": ["ぶーべとうのはた","ぶーべとう","こっき"],
"🇧🇼": ["ぼつわなこっき","ぼつわな","こっき"],
"🇧🇾": ["べらるーしこっき","べらるーし","こっき"],
"🇧🇿": ["べりーずこっき","べりーず","こっき"],
@@ -1658,6 +1695,8 @@
"🇨🇲": ["かめるーんこっき","かめるーん","こっき"],
"🇨🇳": ["ちゅうごくこっき","ちゅうごく","こっき"],
"🇨🇴": ["ころんびあこっき","ころんびあ","こっき"],
"🇨🇵": ["くりっぱーとんとうのはた","くりっぱーとんとう","こっき"],
"🇨🇶": ["さーくとうのはた", "さーくとう", "こっき"],
"🇨🇷": ["こすたりかこっき","こすたりか","こっき"],
"🇨🇺": ["きゅーばこっき","きゅーば","こっき"],
"🇨🇻": ["かーぼべるでこっき","かーぼ","けーぷ","こっき","べるで"],
@@ -1666,11 +1705,13 @@
"🇨🇾": ["きぷろすこっき","きぷろす","こっき"],
"🇨🇿": ["ちぇここっき","ちぇこきょうわこく","こっき"],
"🇩🇪": ["どいつこっき","こっき","どいつ"],
"🇩🇬": ["でぃえごがるしあとうのはた","でぃえごがるしあとう","こっき"],
"🇩🇯": ["じぶちこっき","じぶち","こっき"],
"🇩🇰": ["でんまーくこっき","でんまーく","こっき"],
"🇩🇲": ["どみにかこっき","どみにか","こっき"],
"🇩🇴": ["どみにかきょうわこくこっき","どみにかきょうわこく","こっき"],
"🇩🇿": ["あるじぇりあこっき","あるじぇりあ","こっき"],
"🇪🇦": ["せうた・めりりゃのはた","せうた・めりりゃ","こっき"],
"🇪🇨": ["えくあどるこっき","えくあどる","こっき"],
"🏴󠁧󠁢󠁥󠁮󠁧󠁿": ["いんぐらんどのはた","いんぐらんど","こっき"],
"🇪🇪": ["えすとにあこっき","えすとにあ","こっき"],
@@ -1706,6 +1747,7 @@
"🇬🇼": ["ぎにあびさうこっき","びさう","こっき","ぎにあ"],
"🇬🇾": ["がいあなこっき","こっき","がいあな"],
"🇭🇰": ["ほんこんのはた","ちゅうごく","こっき","ほんこん"],
"🇭🇲": ["はーどとう・まくどなるどしょとうのはた","はーどとう・まくどなるどしょとう","こっき"],
"🇭🇳": ["ほんじゅらすこっき","こっき","ほんじゅらす"],
"🇭🇷": ["くろあちあこっき","くろあちあ","こっき"],
"🇭🇹": ["はいちこっき","こっき","はいち"],
@@ -1751,6 +1793,7 @@
"🇲🇨": ["もなここっき","こっき","もなこ"],
"🇲🇩": ["もるどばこっき","こっき","もるどば"],
"🇲🇪": ["もんてねぐろこっき","こっき","もんてねぐろ"],
"🇲🇫": ["さん・まるたんのはた","さん・まるたん","こっき"],
"🇲🇬": ["まだがすかるこっき","こっき","まだがすかる"],
"🇲🇭": ["まーしゃるしょとうこっき","こっき","しょとう","まーしゃる"],
"🇲🇰": ["まけどにあこっき","こっき","まけどにあ"],
@@ -1811,6 +1854,7 @@
"🇸🇬": ["しんがぽーるこっき","こっき","しんがぽーる"],
"🇸🇭": ["せんとへれなとうのはた","はた","へれな","せんと"],
"🇸🇮": ["すろべにあこっき","こっき","すろべにあ"],
"🇸🇯": ["すばーるばるしょとう・やんまいえんとうのはた","すばーるばるしょとう・やんまいえんとう","こっき"],
"🇸🇰": ["すろばきあこっき","こっき","すろばきあ"],
"🇸🇱": ["しえられおねこっき","こっき","しえられおね"],
"🇸🇲": ["さんまりのこっき","こっき","さんまりの"],
@@ -1842,6 +1886,7 @@
"🇹🇿": ["たんざにあこっき","こっき","たんざにあ"],
"🇺🇦": ["うくらいなこっき","こっき","うくらいな"],
"🇺🇬": ["うがんだこっき","こっき","うがんだ"],
"🇺🇲": ["がっしゅうこくりょうゆうしょうりとうのはた","がっしゅうこくりょうゆうしょうりとう","こっき"],
"🇺🇳": ["こくれんのはた","はた","こくれん","れんごう","こくさい"],
"🇺🇸": ["あめりかこっき","あめりか","はた","ごうしゅう","がっしゅうこく","あめりかがっしゅうこく","がっしゅうこくりょうゆうしょうりとう"],
"🇺🇾": ["うるぐあいこっき","こっき","うるぐあい"],
@@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { GeneratedSearchIndexItem } from 'search-index';
export type SearchIndexItem = {
id: string;
parentId?: string;
path?: string;
label: string;
keywords: string[];
texts: string[];
icon?: string;
};
export function genSearchIndexes(generated: GeneratedSearchIndexItem[]): SearchIndexItem[] {
const rootMods = new Map(generated.map(item => [item.id, item]));
// link inlining here
for (const item of generated) {
if (item.inlining) {
for (const id of item.inlining) {
const inline = rootMods.get(id);
if (inline) {
inline.parentId = item.id;
inline.path = item.path;
} else {
console.log('[Settings Search Index] Failed to inline', id);
}
}
}
}
return generated;
}
@@ -1,40 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { unisonReload } from '@/utility/unison-reload.js';
let isReloadConfirming = false;
export async function reloadAsk(opts: {
unison?: boolean;
reason?: string;
}) {
if (isReloadConfirming) {
return;
}
isReloadConfirming = true;
const { canceled } = await os.confirm(opts.reason == null ? {
type: 'info',
text: i18n.ts.reloadConfirm,
} : {
type: 'info',
title: i18n.ts.reloadConfirm,
text: opts.reason,
}).finally(() => {
isReloadConfirming = false;
});
if (canceled) return;
if (opts.unison) {
unisonReload();
} else {
window.location.reload();
}
}
@@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ref } from 'vue';
export const shouldSuggestReload = ref(false);
export function suggestReload() {
shouldSuggestReload.value = true;
}
@@ -1,36 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { searchIndexes as generated } from 'search-index:settings';
import type { GeneratedSearchIndexItem } from 'search-index:settings';
export type SearchIndexItem = {
id: string;
parentId?: string;
path?: string;
label: string;
keywords: string[];
icon?: string;
};
const rootMods = new Map(generated.map(item => [item.id, item]));
// link inlining here
for (const item of generated) {
if (item.inlining) {
for (const id of item.inlining) {
const inline = rootMods.get(id);
if (inline) {
inline.parentId = item.id;
inline.path = item.path;
} else {
console.log('[Settings Search Index] Failed to inline', id);
}
}
}
}
export const searchIndexes: SearchIndexItem[] = generated;
+20 -11
View File
@@ -3,16 +3,25 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare module 'search-index:settings' {
export type GeneratedSearchIndexItem = {
id: string;
parentId?: string;
path?: string;
label: string;
keywords: string[];
icon?: string;
inlining?: string[];
};
type XGeneratedSearchIndexItem = {
id: string;
parentId?: string;
path?: string;
label: string;
keywords: string[];
texts: string[];
icon?: string;
inlining?: string[];
};
export const searchIndexes: GeneratedSearchIndexItem[];
declare module 'search-index' {
export type GeneratedSearchIndexItem = XGeneratedSearchIndexItem;
}
declare module 'search-index:settings' {
export const searchIndexes: XGeneratedSearchIndexItem[];
}
declare module 'search-index:admin' {
export const searchIndexes: XGeneratedSearchIndexItem[];
}
+5
View File
@@ -28,6 +28,11 @@ export const searchIndexes = [{
mainVirtualModule: 'search-index:settings',
modulesToHmrOnUpdate: ['src/pages/settings/index.vue'],
verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
}, {
targetFilePaths: ['src/pages/admin/*.vue'],
mainVirtualModule: 'search-index:admin',
modulesToHmrOnUpdate: ['src/pages/admin/index.vue'],
verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
}] satisfies SearchIndexOptions[];
/**
-23
View File
@@ -1,23 +0,0 @@
{
"$schema": "https://swc.rs/schema.json",
"jsc": {
"parser": {
"syntax": "typescript",
"dynamicImport": true,
"decorators": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"experimental": {
"keepImportAssertions": true
},
"baseUrl": "src",
"paths": {
"@/*": ["*"]
},
"target": "es2022"
},
"minify": false
}
+1 -1
View File
@@ -9,7 +9,7 @@ export default [
'**/node_modules',
'built',
'coverage',
'jest.config.ts',
'vitest.config.ts',
'test',
'test-d',
'generator',
+1 -1
View File
@@ -7,7 +7,7 @@
"generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix"
},
"devDependencies": {
"@readme/openapi-parser": "2.7.0",
"@readme/openapi-parser": "5.0.0",
"@types/node": "22.16.4",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
@@ -2,7 +2,7 @@ import assert from 'assert';
import { mkdir, readFile, writeFile } from 'fs/promises';
import type { OpenAPIV3_1 } from 'openapi-types';
import { toPascal } from 'ts-case-convert';
import OpenAPIParser from '@readme/openapi-parser';
import { parse } from '@readme/openapi-parser';
import openapiTS, { astToString } from 'openapi-typescript';
import type { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
import ts from 'typescript';
@@ -401,7 +401,7 @@ async function main() {
await mkdir(generatePath, { recursive: true });
const openApiJsonPath = './api.json';
const openApiDocs = await OpenAPIParser.parse(openApiJsonPath) as OpenAPIV3_1.Document;
const openApiDocs = await parse(openApiJsonPath) as OpenAPIV3_1.Document;
const typeFileName = './built/autogen/types.ts';
await generateBaseTypes(openApiDocs, openApiJsonPath, typeFileName);
-207
View File
@@ -1,207 +0,0 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/en/configuration.html
*/
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "C:\\Users\\ai\\AppData\\Local\\Temp\\jest",
// Automatically clear mock calls and instances between every test
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "json",
// "jsx",
// "ts",
// "tsx",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
moduleNameMapper: {
// Do not resolve .wasm.js to .wasm by the rule below
'^(.+)\\.wasm\\.js$': '$1.wasm.js',
// SWC converts @/foo/bar.js to `../../src/foo/bar.js`, and then this rule
// converts it again to `../../src/foo/bar` which then can be resolved to
// `.ts` files.
// See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225
// TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can
// directly import `.ts` files without this hack.
'^((?:\\.{1,2}|[A-Z:])*/.*)\\.js$': '$1',
},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
roots: [
"<rootDir>"
],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
testMatch: [
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[tj]s?(x)",
"<rootDir>/test/**/*"
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jasmine2",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
transform: {
"^.+\\.(t|j)sx?$": ["@swc/jest"],
},
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "\\\\node_modules\\\\",
// "\\.pnp\\.[^\\\\]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
+9 -12
View File
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.8.0-alpha.1",
"version": "2025.8.0-alpha.4",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@@ -25,8 +25,8 @@
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit",
"lint": "pnpm typecheck && pnpm eslint",
"jest": "jest --coverage --detectOpenHandles",
"test": "pnpm jest && pnpm tsd",
"vitest": "vitest run --coverage",
"test": "pnpm vitest && pnpm tsd",
"update-autogen-code": "pnpm --filter misskey-js-type-generator generate && ncp generator/built/autogen src/autogen"
},
"repository": {
@@ -36,22 +36,19 @@
},
"devDependencies": {
"@microsoft/api-extractor": "7.52.8",
"@swc/jest": "0.2.39",
"@types/jest": "29.5.14",
"@types/node": "22.16.4",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"jest": "29.7.0",
"jest-fetch-mock": "3.0.3",
"jest-websocket-mock": "2.5.0",
"mock-socket": "9.3.1",
"@vitest/coverage-v8": "3.2.4",
"esbuild": "0.25.6",
"execa": "9.6.0",
"glob": "11.0.3",
"ncp": "2.0.0",
"nodemon": "3.1.10",
"execa": "8.0.1",
"tsd": "0.32.0",
"typescript": "5.8.3",
"esbuild": "0.25.6",
"glob": "11.0.3"
"vitest": "3.2.4",
"vitest-websocket-mock": "0.5.0"
},
"files": [
"built"
+1
View File
@@ -1,3 +1,4 @@
import { describe, test } from 'vitest';
import { expectType } from 'tsd';
import * as Misskey from '../src/index.js';
+1
View File
@@ -1,3 +1,4 @@
import { describe, test } from 'vitest';
import { expectType } from 'tsd';
import * as Misskey from '../src/index.js';
+141 -125
View File
@@ -1,41 +1,24 @@
import { enableFetchMocks } from 'jest-fetch-mock';
import { vi, describe, test, expect } from 'vitest';
import { APIClient, isAPIError } from '../src/api.js';
enableFetchMocks();
function getFetchCall(call: any[]) {
const { body, method } = call[1];
const contentType = call[1].headers['Content-Type'];
if (
body == null ||
(contentType === 'application/json' && typeof body !== 'string') ||
(contentType === 'multipart/form-data' && !(body instanceof FormData))
) {
throw new Error('invalid body');
}
return {
url: call[0],
method: method,
contentType: contentType,
body: body instanceof FormData ? Object.fromEntries(body.entries()) : JSON.parse(body),
};
}
describe('API', () => {
test('success', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
const body = await req.json();
if (req.method == 'POST' && req.url == 'https://misskey.test/api/i') {
if (body.i === 'TOKEN') {
return JSON.stringify({ id: 'foo' });
} else {
return { status: 400 };
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async (url, options) => {
if (url === 'https://misskey.test/api/i' && options?.method === 'POST') {
if (options.body) {
const body = JSON.parse(options.body as string);
if (body.i === 'TOKEN') {
return new Response(JSON.stringify({ id: 'foo' }), { status: 200 });
}
}
return new Response(null, { status: 400 });
}
} else {
return { status: 404 };
}
});
return new Response(null, { status: 404 });
});
const cli = new APIClient({
origin: 'https://misskey.test',
@@ -48,28 +31,38 @@ describe('API', () => {
id: 'foo'
});
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://misskey.test/api/i',
fetch('https://misskey.test/api/i', {
method: 'POST',
contentType: 'application/json',
body: { i: 'TOKEN' }
})
expect(fetchMock).toHaveBeenCalledWith('https://misskey.test/api/i', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'omit',
cache: 'no-cache',
body: JSON.stringify({ i: 'TOKEN' }),
});
fetchMock.mockRestore();
});
test('with params', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
const body = await req.json();
if (req.method == 'POST' && req.url == 'https://misskey.test/api/notes/show') {
if (body.i === 'TOKEN' && body.noteId === 'aaaaa') {
return JSON.stringify({ id: 'foo' });
} else {
return { status: 400 };
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async (url, options) => {
if (url === 'https://misskey.test/api/notes/show' && options?.method === 'POST') {
if (options.body) {
const body = JSON.parse(options.body as string);
if (body.i === 'TOKEN' && body.noteId === 'aaaaa') {
return new Response(JSON.stringify({ id: 'foo' }), { status: 200 });
}
}
return new Response(null, { status: 400 });
}
} else {
return { status: 404 };
}
});
return new Response(null, { status: 404 });
});
const cli = new APIClient({
origin: 'https://misskey.test',
@@ -82,23 +75,34 @@ describe('API', () => {
id: 'foo'
});
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://misskey.test/api/notes/show',
expect(fetchMock).toHaveBeenCalledWith('https://misskey.test/api/notes/show', {
method: 'POST',
contentType: 'application/json',
body: { i: 'TOKEN', noteId: 'aaaaa' }
headers: {
'Content-Type': 'application/json',
},
credentials: 'omit',
cache: 'no-cache',
body: JSON.stringify({ noteId: 'aaaaa', i: 'TOKEN' }),
});
fetchMock.mockRestore();
});
test('multipart/form-data', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
if (req.method == 'POST' && req.url == 'https://misskey.test/api/drive/files/create') {
return JSON.stringify({ id: 'foo' });
} else {
return { status: 404 };
}
});
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async (url, options) => {
if (url === 'https://misskey.test/api/drive/files/create' && options?.method === 'POST') {
if (options.body instanceof FormData) {
const file = options.body.get('file');
if (file instanceof File && file.name === 'foo.txt') {
return new Response(JSON.stringify({ id: 'foo' }), { status: 200 });
}
}
return new Response(null, { status: 400 });
}
return new Response(null, { status: 404 });
});
const cli = new APIClient({
origin: 'https://misskey.test',
@@ -116,26 +120,26 @@ describe('API', () => {
id: 'foo'
});
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://misskey.test/api/drive/files/create',
expect(fetchMock).toHaveBeenCalledWith('https://misskey.test/api/drive/files/create', {
method: 'POST',
contentType: undefined,
body: {
i: 'TOKEN',
file: testFile,
}
body: expect.any(FormData),
headers: {},
credentials: 'omit',
cache: 'no-cache',
});
fetchMock.mockRestore();
});
test('204 No Content で null が返る', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
if (req.method == 'POST' && req.url == 'https://misskey.test/api/reset-password') {
return { status: 204 };
} else {
return { status: 404 };
}
});
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async (url, options) => {
if (url === 'https://misskey.test/api/reset-password' && options?.method === 'POST') {
return new Response(null, { status: 204 });
}
return new Response(null, { status: 404 });
});
const cli = new APIClient({
origin: 'https://misskey.test',
@@ -146,37 +150,42 @@ describe('API', () => {
expect(res).toEqual(null);
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://misskey.test/api/reset-password',
expect(fetchMock).toHaveBeenCalledWith('https://misskey.test/api/reset-password', {
method: 'POST',
contentType: 'application/json',
body: { i: 'TOKEN', token: 'aaa', password: 'aaa' }
headers: {
'Content-Type': 'application/json',
},
credentials: 'omit',
cache: 'no-cache',
body: JSON.stringify({ token: 'aaa', password: 'aaa', i: 'TOKEN' }),
});
fetchMock.mockRestore();
});
test('インスタンスの credential が指定されていても引数で credential が null ならば null としてリクエストされる', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
const body = await req.json();
if (req.method == 'POST' && req.url == 'https://misskey.test/api/i') {
if (typeof body.i === 'string') {
return JSON.stringify({ id: 'foo' });
} else {
return {
status: 401,
body: JSON.stringify({
error: {
message: 'Credential required.',
code: 'CREDENTIAL_REQUIRED',
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
}
})
};
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async (url, options) => {
if (url === 'https://misskey.test/api/i' && options?.method === 'POST') {
if (options.body) {
const body = JSON.parse(options.body as string);
if (typeof body.i === 'string') {
return new Response(JSON.stringify({ id: 'foo' }), { status: 200 });
} else {
return new Response(JSON.stringify({
error: {
message: 'Credential required.',
code: 'CREDENTIAL_REQUIRED',
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
}
}), { status: 401 });
}
}
return new Response(null, { status: 400 });
}
} else {
return { status: 404 };
}
});
return new Response(null, { status: 404 });
});
try {
const cli = new APIClient({
@@ -187,24 +196,24 @@ describe('API', () => {
await cli.request('i', {}, null);
} catch (e) {
expect(isAPIError(e)).toEqual(true);
} finally {
fetchMock.mockRestore();
}
});
test('api error', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
return {
status: 500,
body: JSON.stringify({
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async () => {
return new Response(JSON.stringify({
error: {
message: 'Internal error occurred. Please contact us if the error persists.',
code: 'INTERNAL_ERROR',
id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
kind: 'server',
},
})
};
});
}), { status: 500 });
});
try {
const cli = new APIClient({
@@ -216,12 +225,17 @@ describe('API', () => {
} catch (e: any) {
expect(isAPIError(e)).toEqual(true);
expect(e.id).toEqual('5d37dbcb-891e-41ca-a3d6-e690c97775ac');
} finally {
fetchMock.mockRestore();
}
});
test('network error', async () => {
fetchMock.resetMocks();
fetchMock.mockAbort();
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async () => {
throw new Error('Network error');
});
try {
const cli = new APIClient({
@@ -232,17 +246,17 @@ describe('API', () => {
await cli.request('i');
} catch (e) {
expect(isAPIError(e)).toEqual(false);
} finally {
fetchMock.mockRestore();
}
});
test('json parse error', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
return {
status: 500,
body: '<html>I AM NOT JSON</html>'
};
});
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async () => {
return new Response('<html>I AM NOT JSON</html>', { status: 500 });
});
try {
const cli = new APIClient({
@@ -253,18 +267,18 @@ describe('API', () => {
await cli.request('i');
} catch (e) {
expect(isAPIError(e)).toEqual(false);
} finally {
fetchMock.mockRestore();
}
});
test('admin/roles/create の型が合う', async() => {
fetchMock.resetMocks();
fetchMock.mockResponse(async () => {
return {
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async () => {
// 本来返すべき値は`Role`型だが、テストなのでお茶を濁す
status: 200,
body: '{}'
};
});
return new Response('{}', { status: 200 });
});
const cli = new APIClient({
origin: 'https://misskey.test',
@@ -292,5 +306,7 @@ describe('API', () => {
},
target: 'manual',
});
fetchMock.mockRestore();
})
});
+2 -1
View File
@@ -1,4 +1,5 @@
import WS from 'jest-websocket-mock';
import { describe, test, expect } from 'vitest';
import WS from 'vitest-websocket-mock';
import Stream from '../src/streaming.js';
describe('Streaming', () => {
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig, configDefaults } from 'vitest/config';
export default defineConfig({
test: {
include: ['test/**/*.ts'],
coverage: {
exclude: [
...configDefaults.coverage.exclude!,
'src/autogen/**/*',
'generator/**/*',
'built/**/*',
'test-d/**/*',
'build.js',
],
}
},
});
+94 -269
View File
@@ -99,8 +99,8 @@ importers:
specifier: 3.826.0
version: 3.826.0(@aws-sdk/client-s3@3.826.0)
'@discordapp/twemoji':
specifier: 15.1.0
version: 15.1.0
specifier: 16.0.1
version: 16.0.1
'@fastify/accepts':
specifier: 5.0.2
version: 5.0.2
@@ -168,8 +168,8 @@ importers:
specifier: 1.12.0
version: 1.12.0
'@twemoji/parser':
specifier: 15.1.1
version: 15.1.1
specifier: 16.0.0
version: 16.0.0
'@types/redis-info':
specifier: 3.0.3
version: 3.0.3
@@ -294,8 +294,8 @@ importers:
specifier: 0.51.0
version: 0.51.0
mfm-js:
specifier: 0.24.0
version: 0.24.0
specifier: 0.25.0
version: 0.25.0
microformats-parser:
specifier: 2.0.3
version: 2.0.3
@@ -707,8 +707,8 @@ importers:
specifier: 1.1.0
version: 1.1.0
'@discordapp/twemoji':
specifier: 15.1.0
version: 15.1.0
specifier: 16.0.1
version: 16.0.1
'@github/webauthn-json':
specifier: 2.1.1
version: 2.1.1
@@ -734,8 +734,8 @@ importers:
specifier: 0.19.0
version: 0.19.0
'@twemoji/parser':
specifier: 15.1.1
version: 15.1.1
specifier: 16.0.0
version: 16.0.0
'@vitejs/plugin-vue':
specifier: 5.2.4
version: 5.2.4(vite@6.3.5(@types/node@22.16.4)(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3))(vue@3.5.17(typescript@5.8.3))
@@ -818,8 +818,8 @@ importers:
specifier: 0.20.0
version: 0.20.0
mfm-js:
specifier: 0.24.0
version: 0.24.0
specifier: 0.25.0
version: 0.25.0
misskey-bubble-game:
specifier: workspace:*
version: link:../misskey-bubble-game
@@ -1080,8 +1080,8 @@ importers:
packages/frontend-embed:
dependencies:
'@discordapp/twemoji':
specifier: 15.1.0
version: 15.1.0
specifier: 16.0.1
version: 16.0.1
'@rollup/plugin-json':
specifier: 6.1.0
version: 6.1.0(rollup@4.45.1)
@@ -1092,8 +1092,8 @@ importers:
specifier: 5.2.0
version: 5.2.0(rollup@4.45.1)
'@twemoji/parser':
specifier: 15.1.1
version: 15.1.1
specifier: 16.0.0
version: 16.0.0
'@vitejs/plugin-vue':
specifier: 5.2.4
version: 5.2.4(vite@6.3.5(@types/node@22.16.4)(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3))(vue@3.5.17(typescript@5.8.3))
@@ -1119,8 +1119,8 @@ importers:
specifier: 2.2.3
version: 2.2.3
mfm-js:
specifier: 0.24.0
version: 0.24.0
specifier: 0.25.0
version: 0.25.0
misskey-js:
specifier: workspace:*
version: link:../misskey-js
@@ -1372,12 +1372,6 @@ importers:
'@microsoft/api-extractor':
specifier: 7.52.8
version: 7.52.8(@types/node@22.16.4)
'@swc/jest':
specifier: 0.2.39
version: 0.2.39(@swc/core@1.12.0)
'@types/jest':
specifier: 29.5.14
version: 29.5.14
'@types/node':
specifier: 22.16.4
version: 22.16.4
@@ -1387,27 +1381,18 @@ importers:
'@typescript-eslint/parser':
specifier: 8.37.0
version: 8.37.0(eslint@9.31.0)(typescript@5.8.3)
'@vitest/coverage-v8':
specifier: 3.2.4
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.16.4)(happy-dom@17.6.3)(jsdom@26.1.0(bufferutil@4.0.9)(canvas@3.1.0)(utf-8-validate@6.0.5))(msw@2.10.4(@types/node@22.16.4)(typescript@5.8.3))(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3))
esbuild:
specifier: 0.25.6
version: 0.25.6
execa:
specifier: 8.0.1
version: 8.0.1
specifier: 9.6.0
version: 9.6.0
glob:
specifier: 11.0.3
version: 11.0.3
jest:
specifier: 29.7.0
version: 29.7.0(@types/node@22.16.4)
jest-fetch-mock:
specifier: 3.0.3
version: 3.0.3(encoding@0.1.13)
jest-websocket-mock:
specifier: 2.5.0
version: 2.5.0
mock-socket:
specifier: 9.3.1
version: 9.3.1
ncp:
specifier: 2.0.0
version: 2.0.0
@@ -1420,12 +1405,18 @@ importers:
typescript:
specifier: 5.8.3
version: 5.8.3
vitest:
specifier: 3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.16.4)(happy-dom@17.6.3)(jsdom@26.1.0(bufferutil@4.0.9)(canvas@3.1.0)(utf-8-validate@6.0.5))(msw@2.10.4(@types/node@22.16.4)(typescript@5.8.3))(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)
vitest-websocket-mock:
specifier: 0.5.0
version: 0.5.0(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.16.4)(happy-dom@17.6.3)(jsdom@26.1.0(bufferutil@4.0.9)(canvas@3.1.0)(utf-8-validate@6.0.5))(msw@2.10.4(@types/node@22.16.4)(typescript@5.8.3))(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3))
packages/misskey-js/generator:
devDependencies:
'@readme/openapi-parser':
specifier: 2.7.0
version: 2.7.0(openapi-types@12.1.3)
specifier: 5.0.0
version: 5.0.0(openapi-types@12.1.3)
'@types/node':
specifier: 22.16.4
version: 22.16.4
@@ -1543,8 +1534,9 @@ packages:
'@analytics/type-utils@0.6.2':
resolution: {integrity: sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg==}
'@apidevtools/swagger-methods@3.0.2':
resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==}
'@apidevtools/json-schema-ref-parser@14.1.1':
resolution: {integrity: sha512-uGF1YGOzzD50L7HLNWclXmsEhQflw8/zZHIz0/AzkJrKL5r9PceUipZxR/cp/8veTk4TVfdDJLyIwXLjaP5ePg==}
engines: {node: '>= 20'}
'@asamuzakjp/css-color@2.8.3':
resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==}
@@ -1964,8 +1956,8 @@ packages:
resolution: {integrity: sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==}
engines: {node: '>=14.0'}
'@discordapp/twemoji@15.1.0':
resolution: {integrity: sha512-QdpV4ifTONAXvDjRrMohausZeGrQ1ac/Ox6togUh6Xl3XKJ/KAaMMuAEi0qsb0wDwoVTSZBll5Y6+N3hB2ktBw==}
'@discordapp/twemoji@16.0.1':
resolution: {integrity: sha512-figLiBWzjS5cyrAjLaGjM8AAaowO3qvK8rg5bA2dElB4qsaPMvBVlFDMO2d3x+nC1igt7kgWH4dvNmvvUHUF8w==}
'@emnapi/runtime@1.4.3':
resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
@@ -2567,10 +2559,6 @@ packages:
resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
'@jest/create-cache-key-function@30.0.2':
resolution: {integrity: sha512-AwlDAHwEHDi+etw9vKWx9HeIApVos8GD/sSTpHtDkqhm9OWuEUPKKPP6EaS17yv0GSzBB3TeeJFLyJ5LPjRqWg==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jest/environment@29.7.0':
resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -2591,10 +2579,6 @@ packages:
resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
'@jest/pattern@30.0.1':
resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jest/reporters@29.7.0':
resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -2608,10 +2592,6 @@ packages:
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
'@jest/schemas@30.0.1':
resolution: {integrity: sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@jest/source-map@29.6.3':
resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -2632,10 +2612,6 @@ packages:
resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
'@jest/types@30.0.1':
resolution: {integrity: sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
'@joshwooding/vite-plugin-react-docgen-typescript@0.5.0':
resolution: {integrity: sha512-qYDdL7fPwLRI+bJNurVcis+tNgJmvWjH4YTBGXTA8xMuxFrnAz6E5o35iyzyKbq5J5Lr8mJGfrR5GXl+WGwhgQ==}
peerDependencies:
@@ -2645,29 +2621,21 @@ packages:
typescript:
optional: true
'@jridgewell/gen-mapping@0.3.5':
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
engines: {node: '>=6.0.0'}
'@jridgewell/gen-mapping@0.3.12':
resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
'@jridgewell/resolve-uri@3.1.0':
resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
engines: {node: '>=6.0.0'}
'@jridgewell/set-array@1.2.1':
resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
engines: {node: '>=6.0.0'}
'@jridgewell/source-map@0.3.6':
resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@jsdevtools/ono@7.1.3':
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
'@jridgewell/trace-mapping@0.3.29':
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
'@kurkle/color@0.3.2':
resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
@@ -3255,19 +3223,15 @@ packages:
'@prisma/instrumentation@5.22.0':
resolution: {integrity: sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q==}
'@readme/better-ajv-errors@2.2.2':
resolution: {integrity: sha512-YBWor3QZhavGHqTEHJfYfk2e0UczQaImc0MIIJ5JNJrns4HKm94pwUdjkPVZO2eZJBt/Rzs/D8ZrdvFFwRh0zQ==}
'@readme/better-ajv-errors@2.3.2':
resolution: {integrity: sha512-T4GGnRAlY3C339NhoUpgJJFsMYko9vIgFAlhgV+/vEGFw66qEY4a4TRJIAZBcX/qT1pq5DvXSme+SQODHOoBrw==}
engines: {node: '>=18'}
peerDependencies:
ajv: 4.11.8 - 8
'@readme/json-schema-ref-parser@1.2.0':
resolution: {integrity: sha512-Bt3QVovFSua4QmHa65EHUmh2xS0XJ3rgTEUPH998f4OW4VVJke3BuS16f+kM0ZLOGdvIrzrPRqwihuv5BAjtrA==}
deprecated: This package is no longer maintained. Please use `@apidevtools/json-schema-ref-parser` instead.
'@readme/openapi-parser@2.7.0':
resolution: {integrity: sha512-P8WSr8WTOxilnT89tcCRKWYsG/II4sAwt1a/DIWub8xTtkrG9cCBBy/IUcvc5X8oGWN82MwcTA3uEkDrXZd/7A==}
engines: {node: '>=18'}
'@readme/openapi-parser@5.0.0':
resolution: {integrity: sha512-dO3yReohn5lqsJQ4tyXpc6grjab4a4fybUY2yxkqOmWP++2QP3Q7wIj2BwABsdU72KMCfNCRyV6VTuxxnKKuMA==}
engines: {node: '>=20'}
peerDependencies:
openapi-types: '>=7'
@@ -3572,9 +3536,6 @@ packages:
'@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
'@sinclair/typebox@0.34.37':
resolution: {integrity: sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==}
'@sindresorhus/is@5.6.0':
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
engines: {node: '>=14.16'}
@@ -4166,12 +4127,6 @@ packages:
peerDependencies:
'@swc/core': '*'
'@swc/jest@0.2.39':
resolution: {integrity: sha512-eyokjOwYd0Q8RnMHri+8/FS1HIrIUKK/sRrFp8c1dThUOfNeCWbLmBP1P5VsKdvmkd25JaH+OKYwEYiAYg9YAA==}
engines: {npm: '>= 7.0.0'}
peerDependencies:
'@swc/core': '*'
'@swc/types@0.1.22':
resolution: {integrity: sha512-D13mY/ZA4PPEFSy6acki9eBT/3WgjMoRqNcdpIvjaYLQ44Xk5BdaL7UkDxAh6Z9UOe7tCCp67BVmZCojYp9owg==}
@@ -4270,14 +4225,8 @@ packages:
resolution: {integrity: sha512-oKarNCN1QUhG148M88mtZdOlBZWWGcInquef+U8QL7gwJkRuNo5WS45Fjsd+3hM9cDJWGpqSZ4Oo097KDx4IWA==}
engines: {node: '>=14.17'}
'@twemoji/parser@15.0.0':
resolution: {integrity: sha512-lh9515BNsvKSNvyUqbj5yFu83iIDQ77SwVcsN/SnEGawczhsKU6qWuogewN1GweTi5Imo5ToQ9s+nNTf97IXvg==}
'@twemoji/parser@15.1.0':
resolution: {integrity: sha512-3HTiSxPvkWUJ4kZeCvwyKlIwkpTUfBOk6igpBBRQni58ceQMv5YK4smkc8vX/eqOlMMNER/9qobv+Q6Q8LVrqA==}
'@twemoji/parser@15.1.1':
resolution: {integrity: sha512-CChRzIu6ngkCJOmURBlYEdX5DZSu+bBTtqR60XjBkFrmvplKW7OQsea+i8XwF4bLVlUXBO7ZmHhRPDzfQyLwwg==}
'@twemoji/parser@16.0.0':
resolution: {integrity: sha512-jmuIjkp3OIaEemwMy3sArBwZSuZkRqmueGwRe2Zk4cFzbUJISFBJSZLDUUBNIgq3c+nY49ideYN2OiII6JUqwA==}
'@types/accepts@1.3.7':
resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
@@ -5413,9 +5362,6 @@ packages:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
call-me-maybe@1.0.2:
resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==}
callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@@ -5803,9 +5749,6 @@ packages:
engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'}
hasBin: true
cross-fetch@3.1.6:
resolution: {integrity: sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==}
cross-fetch@4.1.0:
resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==}
@@ -7483,9 +7426,6 @@ packages:
resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
jest-fetch-mock@3.0.3:
resolution: {integrity: sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==}
jest-get-type@29.6.3:
resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -7523,10 +7463,6 @@ packages:
resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
jest-regex-util@30.0.1:
resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
jest-resolve-dependencies@29.7.0:
resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -7559,9 +7495,6 @@ packages:
resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
jest-websocket-mock@2.5.0:
resolution: {integrity: sha512-a+UJGfowNIWvtIKIQBHoEWIUqRxxQHFx4CXT+R5KxxKBtEQ5rS3pPOV/5299sHzqbmeCzxxY5qE4+yfXePePig==}
jest-worker@29.7.0:
resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -7986,8 +7919,8 @@ packages:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
mfm-js@0.24.0:
resolution: {integrity: sha512-6m8N0ElH9/4CA1izhVqmxTfLj5Z9RspdqM/lMew4xU/UTgm4Pf//VpDunpasxbRFjeJSVW+zoVwL4ZPfPtfiQg==}
mfm-js@0.25.0:
resolution: {integrity: sha512-JoK5TOtswXIvZSZ9hUEL+ZkcNV4onu/DtkaKeXK846+sJBBF8DvxYmPutt7nPaRDsUMmJGr64PNVMFpMGdk3hw==}
microformats-parser@2.0.3:
resolution: {integrity: sha512-MhZoQXkvWrvI3iu1IJ7Zk2VglphPt5P482E5rXvHalvtxhuS/QioMB8gEr8omt7Fd0K/qQEAbTmabvNpCbi3EQ==}
@@ -9094,9 +9027,6 @@ packages:
promise-limit@2.7.0:
resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==}
promise-polyfill@8.3.0:
resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==}
promise-retry@2.0.1:
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
engines: {node: '>=10'}
@@ -10047,6 +9977,7 @@ packages:
supertest@7.1.1:
resolution: {integrity: sha512-aI59HBTlG9e2wTjxGJV+DygfNLgnWbGdZxiA/sgrnNNikIW8lbDvCtF6RnhZoJ82nU7qv7ZLjrvWqCEm52fAmw==}
engines: {node: '>=14.18.0'}
deprecated: Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net
supports-color@10.0.0:
resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==}
@@ -10651,6 +10582,11 @@ packages:
peerDependencies:
vitest: '>=2.0.0'
vitest-websocket-mock@0.5.0:
resolution: {integrity: sha512-vzBWeuF/kD/OCOFzB7WAclb7PxfI105qPkZtdOkPMwZdilBskQjJL4l319JtPtmeovDU7ZVhO3hTfGPjM4txQQ==}
peerDependencies:
vitest: '>=3'
vitest@3.2.4:
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -10717,8 +10653,8 @@ packages:
vue-component-type-helpers@2.2.12:
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
vue-component-type-helpers@3.0.3:
resolution: {integrity: sha512-koiBu7lO8e6w/UlbZAAIW11qcFQocYIl7Nh/SVwGZ804ej5KrncU32bRxi2zfU2Kyf6HWuk1CeeVP2rhIL+vyQ==}
vue-component-type-helpers@3.0.4:
resolution: {integrity: sha512-WtR3kPk8vqKYfCK/HGyT47lK/T3FaVyWxaCNuosaHYE8h9/k0lYRZ/PI/+T/z2wP+uuNKmL6z30rOcBboOu/YA==}
vue-demi@0.14.7:
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
@@ -11025,8 +10961,8 @@ snapshots:
'@ampproject/remapping@2.3.0':
dependencies:
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.29
'@analytics/cookie-utils@0.2.12':
dependencies:
@@ -11064,7 +11000,10 @@ snapshots:
'@analytics/type-utils@0.6.2': {}
'@apidevtools/swagger-methods@3.0.2': {}
'@apidevtools/json-schema-ref-parser@14.1.1':
dependencies:
'@types/json-schema': 7.0.15
js-yaml: 4.1.0
'@asamuzakjp/css-color@2.8.3':
dependencies:
@@ -11579,8 +11518,8 @@ snapshots:
'@babel/generator@7.24.7':
dependencies:
'@babel/types': 7.28.1
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.29
jsesc: 2.5.2
'@babel/helper-compilation-targets@7.24.7':
@@ -11893,9 +11832,9 @@ snapshots:
transitivePeerDependencies:
- web-streams-polyfill
'@discordapp/twemoji@15.1.0':
'@discordapp/twemoji@16.0.1':
dependencies:
'@twemoji/parser': 15.1.0
'@twemoji/parser': 16.0.0
fs-extra: 8.1.0
jsonfile: 5.0.0
universalify: 0.1.2
@@ -12436,10 +12375,6 @@ snapshots:
dependencies:
'@jest/types': 29.6.3
'@jest/create-cache-key-function@30.0.2':
dependencies:
'@jest/types': 30.0.1
'@jest/environment@29.7.0':
dependencies:
'@jest/fake-timers': 29.7.0
@@ -12476,11 +12411,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@jest/pattern@30.0.1':
dependencies:
'@types/node': 22.16.4
jest-regex-util: 30.0.1
'@jest/reporters@29.7.0':
dependencies:
'@bcoe/v8-coverage': 0.2.3
@@ -12488,7 +12418,7 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/trace-mapping': 0.3.29
'@types/node': 22.16.4
chalk: 4.1.2
collect-v8-coverage: 1.0.1
@@ -12514,13 +12444,9 @@ snapshots:
dependencies:
'@sinclair/typebox': 0.27.8
'@jest/schemas@30.0.1':
dependencies:
'@sinclair/typebox': 0.34.37
'@jest/source-map@29.6.3':
dependencies:
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/trace-mapping': 0.3.29
callsites: 3.1.0
graceful-fs: 4.2.11
@@ -12542,7 +12468,7 @@ snapshots:
dependencies:
'@babel/core': 7.24.7
'@jest/types': 29.6.3
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/trace-mapping': 0.3.29
babel-plugin-istanbul: 6.1.1
chalk: 4.1.2
convert-source-map: 2.0.0
@@ -12567,16 +12493,6 @@ snapshots:
'@types/yargs': 17.0.33
chalk: 4.1.2
'@jest/types@30.0.1':
dependencies:
'@jest/pattern': 30.0.1
'@jest/schemas': 30.0.1
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 22.16.4
'@types/yargs': 17.0.33
chalk: 4.1.2
'@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@6.3.5(@types/node@22.16.4)(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3))':
dependencies:
glob: 10.4.5
@@ -12586,30 +12502,25 @@ snapshots:
optionalDependencies:
typescript: 5.8.3
'@jridgewell/gen-mapping@0.3.5':
'@jridgewell/gen-mapping@0.3.12':
dependencies:
'@jridgewell/set-array': 1.2.1
'@jridgewell/sourcemap-codec': 1.5.0
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/trace-mapping': 0.3.29
'@jridgewell/resolve-uri@3.1.0': {}
'@jridgewell/set-array@1.2.1': {}
'@jridgewell/source-map@0.3.6':
dependencies:
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/gen-mapping': 0.3.12
'@jridgewell/trace-mapping': 0.3.29
'@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/trace-mapping@0.3.25':
'@jridgewell/trace-mapping@0.3.29':
dependencies:
'@jridgewell/resolve-uri': 3.1.0
'@jridgewell/sourcemap-codec': 1.5.0
'@jsdevtools/ono@7.1.3': {}
'@kurkle/color@0.3.2': {}
'@levischuck/tiny-cbor@0.2.2': {}
@@ -13285,7 +13196,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@readme/better-ajv-errors@2.2.2(ajv@8.17.1)':
'@readme/better-ajv-errors@2.3.2(ajv@8.17.1)':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/runtime': 7.27.0
@@ -13295,23 +13206,14 @@ snapshots:
leven: 3.1.0
picocolors: 1.1.1
'@readme/json-schema-ref-parser@1.2.0':
'@readme/openapi-parser@5.0.0(openapi-types@12.1.3)':
dependencies:
'@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.15
call-me-maybe: 1.0.2
js-yaml: 4.1.0
'@readme/openapi-parser@2.7.0(openapi-types@12.1.3)':
dependencies:
'@apidevtools/swagger-methods': 3.0.2
'@jsdevtools/ono': 7.1.3
'@readme/better-ajv-errors': 2.2.2(ajv@8.17.1)
'@readme/json-schema-ref-parser': 1.2.0
'@apidevtools/json-schema-ref-parser': 14.1.1
'@readme/better-ajv-errors': 2.3.2(ajv@8.17.1)
'@readme/openapi-schemas': 3.1.0
'@types/json-schema': 7.0.15
ajv: 8.17.1
ajv-draft-04: 1.0.0(ajv@8.17.1)
call-me-maybe: 1.0.2
openapi-types: 12.1.3
'@readme/openapi-schemas@3.1.0': {}
@@ -13646,8 +13548,6 @@ snapshots:
'@sinclair/typebox@0.27.8': {}
'@sinclair/typebox@0.34.37': {}
'@sindresorhus/is@5.6.0': {}
'@sindresorhus/is@7.0.1': {}
@@ -14330,7 +14230,7 @@ snapshots:
ts-dedent: 2.2.0
type-fest: 2.19.0
vue: 3.5.17(typescript@5.8.3)
vue-component-type-helpers: 3.0.3
vue-component-type-helpers: 3.0.4
'@stylistic/eslint-plugin@2.13.0(eslint@9.31.0)(typescript@5.8.3)':
dependencies:
@@ -14424,13 +14324,6 @@ snapshots:
'@swc/counter': 0.1.3
jsonc-parser: 3.2.0
'@swc/jest@0.2.39(@swc/core@1.12.0)':
dependencies:
'@jest/create-cache-key-function': 30.0.2
'@swc/core': 1.12.0
'@swc/counter': 0.1.3
jsonc-parser: 3.2.0
'@swc/types@0.1.22':
dependencies:
'@swc/counter': 0.1.3
@@ -14591,11 +14484,7 @@ snapshots:
'@tsd/typescript@5.8.3': {}
'@twemoji/parser@15.0.0': {}
'@twemoji/parser@15.1.0': {}
'@twemoji/parser@15.1.1': {}
'@twemoji/parser@16.0.0': {}
'@types/accepts@1.3.7':
dependencies:
@@ -15709,7 +15598,7 @@ snapshots:
ast-v8-to-istanbul@0.3.3:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/trace-mapping': 0.3.29
estree-walker: 3.0.3
js-tokens: 9.0.1
@@ -16026,8 +15915,6 @@ snapshots:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
call-me-maybe@1.0.2: {}
callsites@3.1.0: {}
camelcase-keys@6.2.2:
@@ -16386,21 +16273,6 @@ snapshots:
- supports-color
- ts-node
create-jest@29.7.0(@types/node@22.16.4):
dependencies:
'@jest/types': 29.6.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
jest-config: 29.7.0(@types/node@22.16.4)
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
cron-parser@4.9.0:
dependencies:
luxon: 3.3.0
@@ -16414,12 +16286,6 @@ snapshots:
dependencies:
cross-spawn: 7.0.6
cross-fetch@3.1.6(encoding@0.1.13):
dependencies:
node-fetch: 2.7.0(encoding@0.1.13)
transitivePeerDependencies:
- encoding
cross-fetch@4.1.0(encoding@0.1.13):
dependencies:
node-fetch: 2.7.0(encoding@0.1.13)
@@ -18382,7 +18248,7 @@ snapshots:
istanbul-lib-source-maps@5.0.6:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/trace-mapping': 0.3.29
debug: 4.4.1(supports-color@10.0.0)
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
@@ -18456,25 +18322,6 @@ snapshots:
- supports-color
- ts-node
jest-cli@29.7.0(@types/node@22.16.4):
dependencies:
'@jest/core': 29.7.0
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
create-jest: 29.7.0(@types/node@22.16.4)
exit: 0.1.2
import-local: 3.1.0
jest-config: 29.7.0(@types/node@22.16.4)
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.2
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
jest-config@29.7.0(@types/node@22.15.31):
dependencies:
'@babel/core': 7.24.7
@@ -18563,13 +18410,6 @@ snapshots:
jest-mock: 29.7.0
jest-util: 29.7.0
jest-fetch-mock@3.0.3(encoding@0.1.13):
dependencies:
cross-fetch: 3.1.6(encoding@0.1.13)
promise-polyfill: 8.3.0
transitivePeerDependencies:
- encoding
jest-get-type@29.6.3: {}
jest-haste-map@29.7.0:
@@ -18624,8 +18464,6 @@ snapshots:
jest-regex-util@29.6.3: {}
jest-regex-util@30.0.1: {}
jest-resolve-dependencies@29.7.0:
dependencies:
jest-regex-util: 29.6.3
@@ -18752,11 +18590,6 @@ snapshots:
jest-util: 29.7.0
string-length: 4.0.2
jest-websocket-mock@2.5.0:
dependencies:
jest-diff: 29.7.0
mock-socket: 9.3.1
jest-worker@29.7.0:
dependencies:
'@types/node': 22.16.4
@@ -18776,18 +18609,6 @@ snapshots:
- supports-color
- ts-node
jest@29.7.0(@types/node@22.16.4):
dependencies:
'@jest/core': 29.7.0
'@jest/types': 29.6.3
import-local: 3.1.0
jest-cli: 29.7.0(@types/node@22.16.4)
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
jju@1.4.0: {}
joi@17.13.3:
@@ -19284,9 +19105,9 @@ snapshots:
methods@1.1.2: {}
mfm-js@0.24.0:
mfm-js@0.25.0:
dependencies:
'@twemoji/parser': 15.0.0
'@twemoji/parser': 16.0.0
microformats-parser@2.0.3:
dependencies:
@@ -20475,8 +20296,6 @@ snapshots:
promise-limit@2.7.0: {}
promise-polyfill@8.3.0: {}
promise-retry@2.0.1:
dependencies:
err-code: 2.0.3
@@ -22115,7 +21934,7 @@ snapshots:
v8-to-istanbul@9.2.0:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
'@jridgewell/trace-mapping': 0.3.29
'@types/istanbul-lib-coverage': 2.0.6
convert-source-map: 2.0.0
@@ -22187,6 +22006,12 @@ snapshots:
dependencies:
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.16.4)(happy-dom@17.6.3)(jsdom@26.1.0(bufferutil@4.0.9)(canvas@3.1.0)(utf-8-validate@6.0.5))(msw@2.10.4(@types/node@22.16.4)(typescript@5.8.3))(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)
vitest-websocket-mock@0.5.0(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.16.4)(happy-dom@17.6.3)(jsdom@26.1.0(bufferutil@4.0.9)(canvas@3.1.0)(utf-8-validate@6.0.5))(msw@2.10.4(@types/node@22.16.4)(typescript@5.8.3))(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)):
dependencies:
'@vitest/utils': 3.2.4
mock-socket: 9.3.1
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.16.4)(happy-dom@17.6.3)(jsdom@26.1.0(bufferutil@4.0.9)(canvas@3.1.0)(utf-8-validate@6.0.5))(msw@2.10.4(@types/node@22.16.4)(typescript@5.8.3))(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3)
vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.16.4)(happy-dom@17.6.3)(jsdom@26.1.0(bufferutil@4.0.9)(canvas@3.1.0)(utf-8-validate@6.0.5))(msw@2.10.4(@types/node@22.16.4)(typescript@5.8.3))(sass@1.89.2)(terser@5.43.1)(tsx@4.20.3):
dependencies:
'@types/chai': 5.2.2
@@ -22265,7 +22090,7 @@ snapshots:
vue-component-type-helpers@2.2.12: {}
vue-component-type-helpers@3.0.3: {}
vue-component-type-helpers@3.0.4: {}
vue-demi@0.14.7(vue@3.5.17(typescript@5.8.3)):
dependencies: