1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2024-11-23 14:46:44 +09:00

Merge remote-branch 'misskey/develop'

This commit is contained in:
NoriDev 2024-07-31 03:06:03 +09:00
commit 58c022d241
160 changed files with 5923 additions and 4156 deletions

View File

@ -5,7 +5,7 @@
"workspaceFolder": "/workspace",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20.12.2"
"version": "20.16.0"
},
"ghcr.io/devcontainers-contrib/features/corepack:1": {}
},

View File

@ -9,7 +9,6 @@ on:
paths:
- packages/backend/**
- .github/workflows/get-api-diff.yml
- .github/workflows/get-api-diff.yml
jobs:
get-from-cherrypick:
runs-on: ubuntu-latest
@ -18,7 +17,7 @@ jobs:
strategy:
matrix:
node-version: [20.12.2]
node-version: [20.16.0]
api-json-name: [api-base.json, api-head.json]
include:
- api-json-name: api-base.json

View File

@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
node-version: [20.12.2]
node-version: [20.16.0]
steps:
- uses: actions/checkout@v4.1.1

View File

@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
node-version: [20.12.2]
node-version: [20.16.0]
services:
postgres:
@ -71,7 +71,7 @@ jobs:
strategy:
matrix:
node-version: [20.12.2]
node-version: [20.16.0]
services:
postgres:

View File

@ -21,7 +21,7 @@ jobs:
strategy:
matrix:
node-version: [20.12.2]
node-version: [20.16.0]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:

View File

@ -26,7 +26,7 @@ jobs:
strategy:
matrix:
node-version: [20.12.2]
node-version: [20.16.0]
steps:
- uses: actions/checkout@v4.1.1
@ -61,7 +61,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [20.12.2]
node-version: [20.16.0]
browser: [chrome]
services:

View File

@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
node-version: [20.12.2]
node-version: [20.16.0]
steps:
- uses: actions/checkout@v4.1.1

View File

@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
node-version: [20.12.2]
node-version: [20.16.0]
steps:
- uses: actions/checkout@v4.1.1

View File

@ -1 +1 @@
20.12.2
20.16.0

View File

@ -8,11 +8,20 @@
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
- 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
- Feat: ユーザ作成時にSystemWebhookを送信可能に #14281
- Feat: メディアサイレンスを実装 #13842
- メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。
- Enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
- 翻訳の更新
- 依存関係の更新
### Client
- Feat: ユーザーページから「このユーザーのノートを検索」できるように (#14128)
- Feat: 検索ページはクエリを受け付けるようになりました (#14128)
- Enhance: 検索ページのUI改善 (#14128)
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加
- Enhance: 非ログイン時のハイライトTLのデザインを改善
@ -23,6 +32,15 @@
- Enhance: AiScriptを0.19.0にアップデート
- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
- Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように
- Enhance: 検索(ノート/ユーザー)で `#` から始まる文字列を入力すると、そのハッシュタグのノート/ユーザー一覧ページが表示できるように
- Enhance: 検索(ノート/ユーザー)において、入力に空白が含まれている場合は照会を行わないように
- Enhance: 検索(ノート/ユーザー)において、照会を行うかどうか、ハッシュタグのノート/ユーザー一覧ページを表示するかどうかの確認ダイアログを出すように
- Enhance: 検索(ノート/ユーザー)で `@` から始まる文字列(`@user@host`など)を入力すると、そのユーザーを照会できるように
- Enhance: ドライブのファイル・フォルダをドラッグしなくても移動できるように
(Cherry-picked from https://github.com/nafu-at/misskey/commit/b89c2af6945c6a9f9f10e83f54d2bcf0f240b0b4, https://github.com/nafu-at/misskey/commit/8a7d710c6acb83f50c83f050bd1423c764d60a99)
- Enhance: デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように
- Enhance: ブラウザのコンテキストメニューを使用できるように
- Enhance: 連合の「連合中」,「購読中」,「配信中」に対してブロックしているサーバー、配信停止しているサーバーを含めないように
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
@ -41,6 +59,18 @@
- Fix: リアクションしたユーザー一覧のユーザー名がはみ出る問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/672)
- Fix: `/share`ページにおいて絵文字ピッカーを開くことができない問題を修正
- Fix: deck uiの通知音が重なる問題 (#14029)
- Fix: ダイレクト投稿の"削除して編集"において、宛先が保持されていなかった問題を修正
- Fix: 投稿フォームへのURL貼り付けによる引用が下書きに保存されていなかった問題を修正
- Fix: "削除して編集"や下書きにおいて、リアクションの受け入れ設定が保持/保存されていなかった問題を修正
- Fix: 照会に `#` から始まる文字列を入力してそのハッシュタグのページを表示する際、入力が `#` のみの場合に「指定されたURLに該当するページはありませんでした。」が表示されてしまう問題を修正
- Fix: 照会に `@` から始まる文字列を入力してユーザーを照会する際、入力が `@` のみの場合に「問題が発生しました」が表示されてしまう問題を修正
- Fix: 投稿フォームにートのURLを貼り付けて"引用として添付"した場合、投稿文を空にすることによるRenote化が出来なかった問題を修正
- Fix: フォロー中のユーザーに関する"TLに他の人への返信を含める"の設定が分かりづらい問題を修正
- Fix: タイムラインページを開いた時、`TLに他の人への返信を含める`がオフのときに`ファイル付きのみ`をオンにできない問題を修正
- Fix: deck uiでタイムラインを切り替えた際にTLの設定項目が更新されず、`TLに他の人への返信を含める`のトグルが表示されない問題を修正
- Fix: ウィジェットのタイムライン選択欄に無効化されたタイムラインが表示される問題を修正
- Fix: サウンドにドライブの音声を使用している際にドライブの音声が再生できなくなると設定が変更できなくなる問題を修正
### Server
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
@ -51,6 +81,7 @@
- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
- Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように
- Enhance: エンドポイント`api/meta`にプロパティ`noteSearchableScope`が増え、`string`値`local`または`global`を返却します
- Fix: チャート生成時にinstance.suspensionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
@ -78,6 +109,15 @@
- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正
(Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1)
- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251
- Fix: `users/search`において `@` から始まる文字列が与えられた際の処理が正しくなかった問題を修正
- 名前や自己紹介に `@` から始まる文言が含まれるユーザーも検索できるようになります
- Fix: 一部のMisskey以外のソフトウェアからファイルを受け取れない問題
(Cherry-picked from https://github.com/Secineralyr/misskey.dream/pull/73/commits/652eaff1e8aa00b890d71d2e1e52c263c1e67c76)
- NOTE: `drive_file`の`url`, `uri`, `src`の上限が512から1024に変更されます
Migrationではカラム定義の変更のみが行われます。
サーバー管理者は各サーバーの必要に応じ`drive_file` `("uri")`に対するインデックスを張りなおすことでより安定しDBの探索が行われる可能性があります。詳細 は [GitHub](https://github.com/misskey-dev/misskey/pull/14323#issuecomment-2257562228)で確認可能です
- Fix: 自分のフォロワー限定投稿に対するリプライがホームタイムラインで見えないことが有る問題を修正
- Fix: フォローしていないユーザによるフォロワー限定投稿に対するリプライがソーシャルタイムラインで表示されることがある問題を修正
### Misskey.js
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)

View File

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.4
ARG NODE_VERSION=20.12.2-bullseye
ARG NODE_VERSION=20.16.0-bullseye
# build assets & compile TypeScript

View File

@ -1278,8 +1278,6 @@ _sfx:
notification: "الإشعارات"
chat: "المحادثة"
chatBg: "المحادثة (الخلفية)"
antenna: "الهوائيات"
channel: "إشعارات القنات"
_ago:
future: "المستقبَل"
justNow: "اللحظة"
@ -1584,6 +1582,10 @@ _webhookSettings:
active: "مُفعّل"
_events:
reaction: "عند التفاعل"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "البريد الإلكتروني "
_moderationLogTypes:
suspend: "علِق"
deleteDriveFile: "حُذف الملف"

View File

@ -1112,8 +1112,6 @@ _sfx:
notification: "বিজ্ঞপ্তি"
chat: "চ্যাট"
chatBg: "চ্যাট (ব্যাকগ্রাউন্ড)"
antenna: "অ্যান্টেনাগুলি"
channel: "চ্যানেলের বিজ্ঞপ্তি"
_ago:
future: "ভবিষ্যৎ"
justNow: "এইমাত্র"
@ -1130,29 +1128,6 @@ _time:
minute: "মিনিট"
hour: "ঘণ্টা"
day: "দিন"
_tutorial:
title: "CherryPick কিভাবে ব্যাবহার করবেন"
step1_1: "স্বাগতম!"
step1_2: "এই স্ক্রীনটিকে \"টাইমলাইন\" বলা হয় এবং কালানুক্রমিক ক্রমে আপনার এবং আপনি যাদের \"অনুসরণ করেন\" তাদের \"নোটগুলি\" দেখায়৷"
step1_3: "আপনি আপনার টাইমলাইনে কিছু দেখতে পাবেন না কারণ আপনি এখনও কোনো নোট পোস্ট করেননি এবং আপনি কাউকে অনুসরণ করছেন না৷"
step2_1: "নোট তৈরি করার আগে বা কাউকে অনুসরণ করার আগে প্রথমে আপনার প্রোফাইলটি সম্পূর্ণ করুন।"
step2_2: "আপনি কে তা জানা অনেক লোকের জন্য আপনার নোটগুলি দেখা এবং অনুসরণ করাকে সহজ করে তোলে৷"
step3_1: "আপনি কি সফলভাবে আপনার প্রোফাইল সেট আপ করেছেন?"
step3_2: "এখন, কিছু নোট পোস্ট করার চেষ্টা করুন। পোস্ট ফর্ম খুলতে পেন্সিল চিহ্নযুক্ত বাটনে ক্লিক করুন।"
step3_3: "বিষয়বস্তু লেখার পরে, আপনি ফর্মের উপরের ডানদিকের বাটনে ক্লিক করে পোস্ট করতে পারেন।"
step3_4: "পোস্ট করার মত কিছু মনে পরছে না? \"আমি মিসকি সেট আপ করছি\" বললে কেমন হয়?"
step4_1: "পোস্ট করেছেন?"
step4_2: "সাবাশ! এখন আপনার নোট টাইমলাইনে দেখা যাবে।"
step5_1: "এখন অন্যদেরকে অনুসরণ করে আপনার টাইমলাইনকে প্রাণবন্ত করে তুলুন।"
step5_2: "আপনি {featured}-এ জনপ্রিয় নোটগুলি দেখতে পারেন, যাতে আপনি যে ব্যক্তিকে পছন্দ করেন তাকে বেছে নিতে এবং অনুসরণ করতে পারেন, অথবা {explore}-এ জনপ্রিয় ব্যবহারকারীদের দেখতে পারেন৷"
step5_3: "একজন ব্যবহারকারীকে অনুসরণ করতে, ব্যবহারকারীর আইকনে ক্লিক করুন এবং ব্যবহারকারীর পৃষ্ঠাতে \"অনুসরণ করুন\" বাটনে ক্লিক করুন।"
step5_4: "যদি ব্যবহারকারীর নামের পাশে একটি লক আইকন থাকে তাহলে আপনার অনুসরণের অনুরোধ গ্রহণ করার জন্য তারা কিছু সময় নিতে পারে।"
step6_1: "সবকিছু ঠিক থাকলে আপনি টাইমলাইনে অন্য ব্যবহারকারীদের নোট দেখতে পাবেন।"
step6_2: "আপনি সহজেই আপনার প্রতিক্রিয়া জানাতে অন্য ব্যক্তির নোটে \"রিঅ্যাকশন\" যোগ করতে পারেন।"
step6_3: "একটি রিঅ্যাকশন যোগ করতে, নোটে \"+\" চিহ্নে ক্লিক করুন এবং আপনার পছন্দের রিঅ্যাকশন নির্বাচন করুন।"
step7_1: "অভিনন্দন! আপনি এখন CherryPick-র প্রাথমিক টিউটোরিয়ালটি শেষ করেছেন।"
step7_2: "আপনি যদি CherryPick সম্পর্কে আরও জানতে চান, তাহলে {help} এ দেখুন।"
step7_3: "এখন CherryPick উপভোগ করুন 🚀"
_2fa:
alreadyRegistered: "আপনি ইতিমধ্যে একটি 2-ফ্যাক্টর অথেনটিকেশন ডিভাইস নিবন্ধন করেছেন৷"
step1: "প্রথমে, আপনার ডিভাইসে {a} বা {b} এর মতো একটি অথেনটিকেশন অ্যাপ ইনস্টল করুন৷"
@ -1451,6 +1426,10 @@ _deck:
_webhookSettings:
name: "নাম"
active: "চালু"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "ইমেইল"
_moderationLogTypes:
suspend: "স্থগিত করা"
resetPassword: "পাসওয়ার্ড রিসেট করুন"

View File

@ -1415,7 +1415,7 @@ _achievements:
_login3:
title: "Principiant I"
description: "Vas iniciar sessió fa tres dies"
flavor: "Des d'avui diguem Misskist"
flavor: "Des d'avui diguem Cherrypikist"
_login7:
title: "Principiant II"
description: "Vas iniciar sessió fa set dies"
@ -1894,8 +1894,6 @@ _sfx:
noteMy: "Nota (per mi)"
notification: "Notificacions"
chat: "Xat"
antenna: "Antenes"
channel: "Notificacions dels canals"
reaction: "Quan se selecciona una reacció "
_soundSettings:
driveFile: "Fer servir un fitxer d'àudio del disc"
@ -2226,6 +2224,10 @@ _deck:
_webhookSettings:
name: "Nom"
active: "Activat"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "Correu electrònic"
_moderationLogTypes:
suspend: "Suspèn"
resetPassword: "Restableix la contrasenya"

View File

@ -1192,7 +1192,7 @@ _achievements:
_login3:
title: "Začátečník I"
description: "Přihlaste se celkově za 3 dny"
flavor: "Ode dneška mi říkejte Misskista."
flavor: "Ode dneška mi říkejte Cherrypikista."
_login7:
title: "Začátečník II"
description: "Přihlaste se celkově za 7 dní"
@ -1650,8 +1650,6 @@ _sfx:
notification: "Oznámení"
chat: "Zprávy"
chatBg: "Chat (Pozadí)"
antenna: "Antény"
channel: "Oznámení kanálu"
_ago:
future: "Budoucí"
justNow: "Teď"
@ -2027,6 +2025,10 @@ _webhookSettings:
renote: "Při renotaci poznámky"
reaction: "Při obdržení reakce"
mention: "Při zmínce"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "Email"
_moderationLogTypes:
suspend: "Zmrazit"
resetPassword: "Resetovat heslo"

View File

@ -1345,7 +1345,7 @@ _achievements:
_login3:
title: "Anfänger "
description: "An 3 Tagen eingeloggt"
flavor: "Nenn' mich ab heute Misskist"
flavor: "Nenn' mich ab heute Cherrypikist"
_login7:
title: "Anfänger Ⅱ"
description: "An 7 Tagen eingeloggt"
@ -1886,8 +1886,6 @@ _sfx:
notification: "Benachrichtigungen"
chat: "Chat"
chatBg: "Chat (Hintergrund)"
antenna: "Antennen"
channel: "Kanalbenachrichtigung"
_ago:
future: "Zukunft"
justNow: "Gerade eben"
@ -2293,6 +2291,10 @@ _webhookSettings:
renote: "Wenn du ein Renote erhältst"
reaction: "Wenn du eine Reaktion erhältst"
mention: "Wenn du erwähnt wirst"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "Email"
_moderationLogTypes:
createRole: "Rolle erstellt"
deleteRole: "Rolle gelöscht"

View File

@ -303,8 +303,6 @@ _sfx:
notification: "Ειδοποιήσεις"
chat: "Συνομιλία"
chatBg: "Συνομιλία (Παρασκήνιο)"
antenna: "Αντένες"
channel: "Ειδοποιήσεις καναλιών"
_ago:
future: "Μελλοντικό"
justNow: "Μόλις τώρα"

View File

@ -137,6 +137,7 @@ copyFileId: "Copy file ID"
copyFolderId: "Copy folder ID"
copyProfileUrl: "Copy profile URL"
searchUser: "Search for a user"
searchThisUsersNotes: "Search this users notes"
reply: "Reply"
loadMore: "Load more"
showMore: "Show more"
@ -185,7 +186,7 @@ enterEmoji: "Enter an emoji"
renote: "Renote"
unrenote: "Remove renote"
renoted: "Renoted."
renotedToX: "Renote from {name} users。"
renotedToX: "Renote to {name}."
quoted: "Quoted."
replied: "Replied."
cantRenote: "This post can't be renoted."
@ -205,8 +206,8 @@ add: "Add"
reaction: "Reactions"
reactions: "Reactions"
emojiPicker: "Emoji picker"
pinnedEmojisForReactionSettingDescription: "Set the emojis which should be pinned and displayed immediately when reacting."
pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker"
pinnedEmojisForReactionSettingDescription: "Set the emojis to be pinned and displayed when reacting."
pinnedEmojisSettingDescription: "Set the emojis to be pinned and displayed when viewing emoji picker."
emojiPickerDisplay: "Emoji picker display"
overwriteFromPinnedEmojisForReaction: "Override from reaction settings"
overwriteFromPinnedEmojis: "Override from general settings"
@ -234,6 +235,7 @@ editList: "Edit list"
selectChannel: "Select a channel"
selectAntenna: "Select an antenna"
editAntenna: "Edit antenna"
createAntenna: "Create an antenna"
selectWidget: "Select a widget"
editWidgets: "Edit widgets"
editWidgetsExit: "Done"
@ -260,6 +262,10 @@ addAccount: "Add account"
reloadAccountsList: "Reload account list"
loginFailed: "Failed to sign in"
showOnRemote: "View on remote instance"
continueOnRemote: "リモートで続行"
chooseServerOnMisskeyHub: "Choose a server from the Misskey Hub"
specifyServerHost: "Specify a server host directly"
inputHostName: "Enter the domain"
general: "General"
wallpaper: "Wallpaper"
setWallpaper: "Set wallpaper"
@ -270,6 +276,7 @@ followConfirm: "Are you sure that you want to follow {name}?"
proxyAccount: "Proxy account"
proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead."
host: "Host"
selectSelf: "Select myself"
selectUser: "Select a user"
recipient: "Recipient"
annotation: "Comments"
@ -397,6 +404,7 @@ selectFile: "Select a file"
selectFiles: "Select files"
selectFolder: "Select a folder"
selectFolders: "Select folders"
fileNotSelected: ""
renameFile: "Rename file"
folderName: "Folder name"
createFolder: "Create a folder"
@ -566,6 +574,7 @@ noMessagesYet: "No messages yet"
newMessageExists: "There are new messages"
onlyOneFileCanBeAttached: "You can only attach one file to a message"
signinRequired: "Please register or sign in before continuing"
signinOrContinueOnRemote: "To continue, you need to move your server or sign up / log in to this server."
invitations: "Invites"
invitationCode: "Invitation code"
checking: "Checking..."
@ -938,6 +947,7 @@ administration: "Management"
accounts: "Accounts"
switch: "Switch"
noMaintainerInformationWarning: "Maintainer information is not configured."
noInquiryUrlWarning: "Inquiry URL isnt set"
noBotProtectionWarning: "Bot protection is not configured."
configure: "Configure"
postToGallery: "Create new gallery post"
@ -1130,6 +1140,7 @@ thisPostMayBeAnnoyingHome: "Post to home timeline"
thisPostMayBeAnnoyingCancel: "Cancel"
thisPostMayBeAnnoyingIgnore: "Post anyway"
collapseRenotes: "Collapse renotes you've already seen"
collapseRenotesDescription: "Collapse notes that you've reacted to or renoted before."
collapseDefault: "Collapse notes using specific MFM syntax"
internalServerError: "Internal Server Error"
internalServerErrorDescription: "The server has run into an unexpected error."
@ -1207,6 +1218,8 @@ preservedUsernames: "Reserved usernames"
preservedUsernamesDescription: "List usernames to reserve separated by linebreaks. These will become unable during normal account creation, but can be used by administrators to manually create accounts. Already existing accounts using these usernames will not be affected."
createNoteFromTheFile: "Compose note from this file"
archive: "Archive"
archived: "Archived"
unarchive: "Unarchive"
channelArchiveConfirmTitle: "Really archive {name}?"
channelArchiveConfirmDescription: "An archived channel won't appear in the channel list or search results anymore. New posts can also not be added to it anymore."
thisChannelArchived: "This channel has been archived."
@ -1217,6 +1230,7 @@ preventAiLearning: "Reject usage in Machine Learning (Generative AI)"
preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored."
options: "Options"
specifyUser: "Specific user"
specifyHost: "Specify a host"
failedToPreviewUrl: "Could not preview"
update: "Update"
rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction"
@ -1351,6 +1365,11 @@ keepOriginalFilenameDescription: "If you turn off this setting, files names will
noDescription: "There is not the explanation"
alwaysConfirmFollow: "Always confirm when following"
inquiry: "Contact"
tryAgain: "Please try again later"
confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media"
sensitiveMediaRevealConfirm: "This might be a sensitive media. Are you sure to reveal?"
createdLists: "Created lists"
createdAntennas: "Created antennas"
showUnreadNotificationsCount: "Show the number of unread notifications"
showCatOnly: "Show only cats"
additionalPermissionsForFlash: "Allow to add permission to Play"
@ -1588,6 +1607,8 @@ _serverSettings:
fanoutTimelineDescription: "Greatly increases performance of timeline retrieval and reduces load on the database when enabled. In exchange, memory usage of Redis will increase. Consider disabling this in case of low server memory or server instability."
fanoutTimelineDbFallback: "Fallback to database"
fanoutTimelineDbFallbackDescription: "When enabled, the timeline will fall back to the database for additional queries if the timeline is not cached. Disabling it further reduces the server load by eliminating the fallback process, but limits the range of timelines that can be retrieved."
inquiryUrl: "Inquiry URL"
inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information."
_accountMigration:
moveFrom: "Migrate another account to this one"
moveFromSub: "Create alias to another account"
@ -1659,7 +1680,7 @@ _achievements:
_login3:
title: "Beginner I"
description: "Log in for a total of 3 days"
flavor: "Starting today, just call me Misskist"
flavor: "Starting today, just call me Cherrypikist"
_login7:
title: "Beginner II"
description: "Log in for a total of 7 days"
@ -1908,6 +1929,7 @@ _role:
canManageAvatarDecorations: "Manage avatar decorations"
driveCapacity: "Drive capacity"
alwaysMarkNsfw: "Always mark files as NSFW"
canUpdateBioMedia: "Allow to edit an icon or a banner image"
pinMax: "Maximum number of pinned notes"
antennaMax: "Maximum number of antennas"
wordMuteMax: "Maximum number of characters allowed in word mutes"
@ -2245,8 +2267,6 @@ _sfx:
notification: "Notifications"
chat: "Chat"
chatBg: "Chat (Background)"
antenna: "Antennas"
channel: "Channel notifications"
reaction: "On choosing a reaction"
_soundSettings:
driveFile: "Use an audio file in Drive."
@ -2372,7 +2392,7 @@ _permissions:
"read:admin:invite-codes": "View invite codes"
"write:admin:announcements": "Manage announcements"
"read:admin:announcements": "View announcements"
"write:admin:avatar-decorations": "Manage avatar decorations"
"write:admin:avatar-decorations": "Can manage avatar decorations"
"read:admin:avatar-decorations": "View avatar decorations"
"write:admin:federation": "Manage federation data"
"write:admin:account": "Manage user account"
@ -2676,6 +2696,7 @@ _deck:
alwaysShowMainColumn: "Always show main column"
columnAlign: "Align columns"
addColumn: "Add column"
newNoteNotificationSettings: "Notification setting for new notes"
configureColumn: "Column settings"
swapLeft: "Swap with the left column"
swapRight: "Swap with the right column"
@ -2714,6 +2735,7 @@ _drivecleaner:
orderByCreatedAtAsc: "Ascending Dates"
_webhookSettings:
createWebhook: "Create Webhook"
modifyWebhook: "Modify Webhook"
name: "Name"
secret: "Secret"
events: "Webhook Events"
@ -2726,6 +2748,26 @@ _webhookSettings:
renote: "When renoted"
reaction: "When receiving a reaction"
mention: "When being mentioned"
_systemEvents:
abuseReport: "When received a new abuse report"
abuseReportResolved: "When resolved abuse reports"
userCreated: "When user is created"
deleteConfirm: "Are you sure you want to delete the Webhook?"
_abuseReport:
_notificationRecipient:
createRecipient: "Add a recipient for abuse reports"
modifyRecipient: "Edit a recipient for abuse reports"
recipientType: "Notification type"
_recipientType:
mail: "Email"
webhook: "Webhook"
_captions:
mail: "Send the email to moderators' email addresses when you receive abuse."
webhook: "Send a notification to SystemWebhook when you receive or resolve abuse."
keywords: "Keywords"
notifiedUser: "Users to notify"
notifiedWebhook: "Webhook to use"
deleteConfirm: "Are you sure that you want to delete the notification recipient?"
_moderationLogTypes:
createRole: "Role created"
deleteRole: "Role deleted"
@ -2763,6 +2805,12 @@ _moderationLogTypes:
deleteAvatarDecoration: "Avatar decoration deleted"
unsetUserAvatar: "Unset this user's avatar"
unsetUserBanner: "Unset this user's banner"
createSystemWebhook: "Create SystemWebhook"
updateSystemWebhook: "Update SystemWebHook"
deleteSystemWebhook: "Delete SystemWebhook"
createAbuseReportNotificationRecipient: "Create a recipient for abuse reports"
updateAbuseReportNotificationRecipient: "Update recipients for abuse reports"
deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports"
_fileViewer:
title: "File details"
type: "File type"
@ -2894,6 +2942,11 @@ _mediaControls:
pip: "Picture in Picture"
playbackRate: "Playback Speed"
loop: "Loop playback"
_contextMenu:
title: "Context menu"
app: "Application"
appWithShift: "Application with shift key"
native: "Native"
_abuse:
_resolver:
1hour: "one hour"

View File

@ -302,7 +302,7 @@ location: "Lugar"
theme: "Tema"
themeForLightMode: "Tema para usar en Modo Linterna"
themeForDarkMode: "Tema para usar en Modo Oscuro"
light: "Linterna"
light: "Claro"
dark: "Oscuro"
lightThemes: "Tema claro"
darkThemes: "Tema oscuro"
@ -1447,7 +1447,7 @@ _achievements:
_login3:
title: "Principiante I"
description: "Días desde el inicio de sesión: 3"
flavor: "Desde hoy, soy Misskero"
flavor: "Desde hoy, soy Cherrypikero"
_login7:
title: "Principiante II"
description: "Días desde el inicio de sesión: 7"
@ -2005,8 +2005,6 @@ _sfx:
notification: "Notificaciones"
chat: "Chat"
chatBg: "Chat (Fondo)"
antenna: "Antena receptora"
channel: "Notificaciones del canal"
reaction: "Al seleccionar una reacción"
_soundSettings:
driveFile: "Usar un archivo de audio en Drive"
@ -2483,6 +2481,10 @@ _webhookSettings:
renote: "Cuando reciba un \"re-note\""
reaction: "Cuando se recibe una reacción"
mention: "Cuando hay una mención"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "Correo"
_moderationLogTypes:
createRole: "Rol creado"
deleteRole: "Rol eliminado"

View File

@ -1796,8 +1796,6 @@ _sfx:
notification: "Notifications"
chat: "Discuter"
chatBg: "Discussion (arrière-plan)"
antenna: "Réception de lantenne"
channel: "Notifications de canal"
reaction: "Lors de la sélection de la réaction"
_soundSettings:
driveFile: "Utiliser un effet sonore sur le Disque"
@ -2161,6 +2159,10 @@ _drivecleaner:
_webhookSettings:
name: "Nom"
active: "Activé"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "E-mail "
_moderationLogTypes:
createRole: "Rôle créé"
deleteRole: "Rôle supprimé"

View File

@ -180,6 +180,10 @@ addAccount: "Tambahkan akun"
reloadAccountsList: "Muat ulang daftar akun"
loginFailed: "Gagal untuk masuk"
showOnRemote: "Lihat profil asli"
continueOnRemote: "Lihat di peladen asal"
chooseServerOnMisskeyHub: "Pilih peladen dari Misskey Hub"
specifyServerHost: "Tentukan domain peladen"
inputHostName: "Masukkan nama domain"
general: "Umum"
wallpaper: "Wallpaper"
setWallpaper: "Atur wallpaper"
@ -316,6 +320,7 @@ selectFile: "Pilih berkas"
selectFiles: "Pilih berkas"
selectFolder: "Pilih folder"
selectFolders: "Pilih folder"
fileNotSelected: "Tidak ada file yang dipilih"
renameFile: "Ubah nama berkas"
folderName: "Nama folder"
createFolder: "Buat folder"
@ -1253,6 +1258,7 @@ keepOriginalFilenameDescription: "Apabila pengaturan ini dimatikan, nama berkas
noDescription: "Tidak ada deskripsi"
alwaysConfirmFollow: "Selalu konfirmasi ketika mengikuti"
inquiry: "Hubungi kami"
tryAgain: "Silahkan coba lagi."
_delivery:
status: "Status pengiriman"
stop: "Ditangguhkan"
@ -1458,7 +1464,7 @@ _achievements:
_login3:
title: "Pemula I"
description: "Login selama 3 hari"
flavor: "Mulai hari ini, panggil gue Misskist"
flavor: "Mulai hari ini, panggil gue Cherrypikist"
_login7:
title: "Pemula II"
description: "Login selama 7 hari"
@ -1756,7 +1762,7 @@ _emailUnavailable:
smtp: "Peladen alamat surel ini tidak merespon"
banned: "Kamu tidak dapat mendaftar dengan alamat surel ini"
_ffVisibility:
public: "Terbitkan"
public: "Publik"
followers: "Tampil untuk pengikut saja"
private: "Tersembunyi"
_signup:
@ -2015,8 +2021,6 @@ _sfx:
notification: "Notifikasi"
chat: "Pesan"
chatBg: "Obrolan (Latar Belakang)"
antenna: "Penerimaan Antenna"
channel: "Notifikasi Kanal"
reaction: "Ketika memilih reaksi"
_soundSettings:
driveFile: "Menggunakan berkas audio dalam Drive"
@ -2483,6 +2487,7 @@ _drivecleaner:
orderByCreatedAtAsc: "Tanggal (Naik)"
_webhookSettings:
createWebhook: "Buat Webhook"
modifyWebhook: "Sunting Webhook"
name: "Nama"
secret: "Secret"
events: "Webhook Events"
@ -2495,6 +2500,11 @@ _webhookSettings:
renote: "Ketika direnote"
reaction: "Ketika menerima reaksi"
mention: "Ketika sedang disebut"
deleteConfirm: "Apakah kamu yakin ingin menghapus Webhook?"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "Surel"
_moderationLogTypes:
createRole: "Peran telah dibuat"
deleteRole: "Peran telah dihapus"

80
locales/index.d.ts vendored
View File

@ -570,6 +570,10 @@ export interface Locale extends ILocale {
*
*/
"searchUser": string;
/**
*
*/
"searchThisUsersNotes": string;
/**
*
*/
@ -958,6 +962,10 @@ export interface Locale extends ILocale {
*
*/
"editAntenna": string;
/**
*
*/
"createAntenna": string;
/**
*
*/
@ -1118,6 +1126,10 @@ export interface Locale extends ILocale {
*
*/
"host": string;
/**
*
*/
"selectSelf": string;
/**
*
*/
@ -1178,6 +1190,10 @@ export interface Locale extends ILocale {
*
*/
"silenceThisInstance": string;
/**
*
*/
"mediaSilenceThisInstance": string;
/**
*
*/
@ -1262,6 +1278,14 @@ export interface Locale extends ILocale {
*
*/
"silencedInstancesDescription": string;
/**
*
*/
"mediaSilencedInstances": string;
/**
* 使
*/
"mediaSilencedInstancesDescription": string;
/**
*
*/
@ -4883,6 +4907,14 @@ export interface Locale extends ILocale {
*
*/
"archive": string;
/**
*
*/
"archived": string;
/**
*
*/
"unarchive": string;
/**
* {name}
*/
@ -4923,6 +4955,18 @@ export interface Locale extends ILocale {
*
*/
"specifyUser": string;
/**
*
*/
"lookupConfirm": string;
/**
*
*/
"openTagPageConfirm": string;
/**
*
*/
"specifyHost": string;
/**
*
*/
@ -5471,6 +5515,14 @@ export interface Locale extends ILocale {
*
*/
"sensitiveMediaRevealConfirm": string;
/**
*
*/
"createdLists": string;
/**
*
*/
"createdAntennas": string;
/**
*
*/
@ -6618,7 +6670,7 @@ export interface Locale extends ILocale {
*/
"description": string;
/**
*
*
*/
"flavor": string;
};
@ -8811,6 +8863,10 @@ export interface Locale extends ILocale {
* 使CherryPickの使用に支障をきたす可能性があります
*/
"driveFileDurationWarnDescription": string;
/**
*
*/
"driveFileError": string;
};
"_ago": {
/**
@ -10642,6 +10698,10 @@ export interface Locale extends ILocale {
*
*/
"abuseReportResolved": string;
/**
*
*/
"userCreated": string;
};
/**
* Webhookを削除しますか
@ -11348,6 +11408,24 @@ export interface Locale extends ILocale {
*/
"loop": string;
};
"_contextMenu": {
/**
*
*/
"title": string;
/**
*
*/
"app": string;
/**
* Shiftキーでアプリケーション
*/
"appWithShift": string;
/**
* UI
*/
"native": string;
};
"_abuse": {
"_resolver": {
/**

View File

@ -108,11 +108,14 @@ enterEmoji: "Inserisci emoji"
renote: "Rinota"
unrenote: "Elimina la Rinota"
renoted: "Rinotata!"
renotedToX: "Rinota da {name}."
cantRenote: "È impossibile rinotare questa nota."
cantReRenote: "È impossibile rinotare una Rinota."
quote: "Citazione"
inChannelRenote: "Rinota nel canale"
inChannelQuote: "Cita nel canale"
renoteToChannel: "Rinota al canale"
renoteToOtherChannel: "Rinota a un altro canale"
pinnedNote: "Nota in primo piano"
pinned: "Fissa sul profilo"
you: "Tu"
@ -177,6 +180,10 @@ addAccount: "Aggiungi profilo"
reloadAccountsList: "Ricarica l'elenco dei profili"
loginFailed: "Accesso non riuscito"
showOnRemote: "Leggi sull'istanza remota"
continueOnRemote: "Continua da remoto"
chooseServerOnMisskeyHub: "Scegli l'istanza sul sito Misskey Hub"
specifyServerHost: "Indica l'indirizzo dell'istanza"
inputHostName: "Digita il nome del dominio "
general: "Generali"
wallpaper: "Sfondo"
setWallpaper: "Imposta sfondo"
@ -313,6 +320,7 @@ selectFile: "Scelta allegato"
selectFiles: "Scelta allegato"
selectFolder: "Seleziona cartella"
selectFolders: "Seleziona cartella"
fileNotSelected: "Nessun file selezionato"
renameFile: "Rinomina file"
folderName: "Nome della cartella"
createFolder: "Nuova cartella"
@ -477,10 +485,12 @@ noteOf: "Note di {user}"
inviteToGroup: "Invitare al gruppo"
quoteAttached: "Citazione allegata"
quoteQuestion: "Vuoi aggiungere una citazione?"
attachAsFileQuestion: "Il testo copiato eccede le dimensioni, vuoi allegarlo?"
noMessagesYet: "Ancora nessuna chat"
newMessageExists: "Hai ricevuto un nuovo messaggio"
onlyOneFileCanBeAttached: "È possibile allegare al messaggio soltanto uno file"
signinRequired: "Occorre avere un profilo registrato su questa istanza"
signinOrContinueOnRemote: "Per continuare, devi accedere alla tua istanza o registrarti su questa e poi accedere"
invitations: "Invita"
invitationCode: "Codice di invito"
checking: "Confermando"
@ -707,7 +717,7 @@ reporterOrigin: "Segnalazione da"
forwardReport: "Inoltro di un report a un'istanza remota."
forwardReportIsAnonymous: "L'istanza remota non vedrà le tue informazioni, apparirai come profilo di sistema, anonimo."
send: "Inviare"
abuseMarkAsResolved: "Contrassegna la segnalazione come risolta"
abuseMarkAsResolved: "Risolvi segnalazione"
openInNewTab: "Apri in una nuova scheda"
openInSideView: "Apri in vista laterale"
defaultNavigationBehaviour: "Navigazione preimpostata"
@ -844,6 +854,7 @@ administration: "Gestione"
accounts: "Profilo"
switch: "Cambia"
noMaintainerInformationWarning: "Mancano le informazioni sull'amministratore."
noInquiryUrlWarning: "Non è stata impostata la URL di contatto"
noBotProtectionWarning: "Non è stata impostata alcuna protezione dai Bot"
configure: "Imposta"
postToGallery: "Pubblicare nella galleria"
@ -1035,6 +1046,7 @@ thisPostMayBeAnnoyingHome: "Pubblica sulla timeline principale"
thisPostMayBeAnnoyingCancel: "Annulla"
thisPostMayBeAnnoyingIgnore: "Pubblica lo stesso"
collapseRenotes: "Comprimi le Rinota già viste"
collapseRenotesDescription: "Comprimi le Note con cui hai già interagito."
internalServerError: "Errore interno del server"
internalServerErrorDescription: "Si è verificato un errore imprevisto all'interno del server"
copyErrorInfo: "Copia le informazioni sull'errore"
@ -1247,10 +1259,20 @@ useNativeUIForVideoAudioPlayer: "Riprodurre audio/video usando le funzionalità
keepOriginalFilename: "Mantieni il nome file originale"
keepOriginalFilenameDescription: "Disattivandola, i file verranno caricati usando nomi casuali."
noDescription: "Manca la descrizione"
alwaysConfirmFollow: "Richiedi conferma per i Follow"
inquiry: "Contattaci"
tryAgain: "Per favore riprova"
confirmWhenRevealingSensitiveMedia: "Richiedi conferma prima di mostrare gli allegati espliciti"
sensitiveMediaRevealConfirm: "Questo allegato è esplicito, vuoi vederlo?"
_delivery:
status: "Stato della consegna"
stop: "Sospensione"
resume: "Riprendi la consegna"
_type:
none: "Pubblicazione"
manuallySuspended: "Sospesa manualmente"
goneSuspended: "Sospensione server a causa dell'eliminazione"
autoSuspendedForNotResponding: "Sospensione del server a causa di mancata risposta"
_bubbleGame:
howToPlay: "Come giocare"
hold: "Tieni"
@ -1376,6 +1398,8 @@ _serverSettings:
fanoutTimelineDescription: "Attivando questa funzionalità migliori notevolmente la capacità delle Timeline di collezionare Note, riducendo il carico sul database. Tuttavia, aumenterà l'impiego di memoria RAM per Redis. Disattiva se il tuo server ha poca RAM o la funzionalità è irregolare."
fanoutTimelineDbFallback: "Elaborazione dati alternativa"
fanoutTimelineDbFallbackDescription: "Attivando l'elaborazione alternativa, verrà interrogato ulteriormente il database se la timeline non è nella cache. \nDisattivando, si può ridurre ulteriormente il carico del server, evitando l'elaborazione alternativa, ma limitando l'intervallo recuperabile delle timeline."
inquiryUrl: "URL di contatto"
inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione."
_accountMigration:
moveFrom: "Migra un altro profilo dentro a questo"
moveFromSub: "Crea un alias verso un altro profilo remoto"
@ -1447,7 +1471,7 @@ _achievements:
_login3:
title: "Principiante I"
description: "Hai totalizzato 3 accessi!"
flavor: "Da oggi, chiamatemi Misskist"
flavor: "Da oggi, chiamatemi Cherrypikist"
_login7:
title: "Principiante II"
description: "Hai totalizzato 7 accessi!"
@ -1695,6 +1719,7 @@ _role:
canManageAvatarDecorations: "Gestisce le decorazioni di immagini del profilo"
driveCapacity: "Capienza del Drive"
alwaysMarkNsfw: "Impostare sempre come esplicito (NSFW)"
canUpdateBioMedia: "Può aggiornare foto profilo e di testata"
pinMax: "Quantità massima di Note in primo piano"
antennaMax: "Quantità massima di Antenne"
wordMuteMax: "Lunghezza massima del filtro parole"
@ -1713,6 +1738,11 @@ _role:
roleAssignedTo: "Assegnato a ruoli manualmente"
isLocal: "Profilo locale"
isRemote: "Profilo remoto"
isCat: "È un gattino"
isBot: "È un bot"
isSuspended: "È sospeso"
isLocked: "È in stato privato"
isExplorable: "Autorizza la pubblicazione nei cataloghi"
createdLessThan: "Profilo creato da meno di N"
createdMoreThan: "Profilo creato da più di N"
followersLessThanOrEq: "Profilo con N follower o meno"
@ -2001,8 +2031,6 @@ _sfx:
notification: "Notifiche"
chat: "Messaggi"
chatBg: "Chat (sfondo)"
antenna: "Ricezione dell'antenna"
channel: "Notifiche di canale"
reaction: "Quando seleziono una reazione"
_soundSettings:
driveFile: "Suoni del Drive"
@ -2431,6 +2459,7 @@ _deck:
alwaysShowMainColumn: "Mostra sempre la colonna principale"
columnAlign: "Allineare colonne"
addColumn: "Aggiungi colonna"
newNoteNotificationSettings: "Preferenze per le notifiche di nuove Note"
configureColumn: "Impostazioni colonna"
swapLeft: "Sposta a sinistra"
swapRight: "Sposta a destra"
@ -2469,6 +2498,7 @@ _drivecleaner:
orderByCreatedAtAsc: "Dal più vecchio al più recente"
_webhookSettings:
createWebhook: "Creazione Webhook"
modifyWebhook: "Modifica Webhook"
name: "Nome"
secret: "Segreto"
events: "Quando eseguire il Webhook"
@ -2481,6 +2511,25 @@ _webhookSettings:
renote: "Quando la Nota è Rinotata"
reaction: "Quando ricevo una reazione"
mention: "Quando mi menzionano"
_systemEvents:
abuseReport: "Quando arriva una segnalazione"
abuseReportResolved: "Quando una segnalazione è risolta"
deleteConfirm: "Vuoi davvero eliminare il Webhook?"
_abuseReport:
_notificationRecipient:
createRecipient: "Aggiungi destinatario della segnalazione"
modifyRecipient: "Modifica destinatario della segnalazione"
recipientType: "Tipo di notifica"
_recipientType:
mail: "Email"
webhook: "Webhook"
_captions:
mail: "Quando ricevi un abuso, notifica l'amministrazione via email"
webhook: "Spedire una notifica al SystemWebhook specificato (sia quando si riceve una segnalazione, che quando viene risolta)"
keywords: "Parole chiave"
notifiedUser: "Profili da notificare"
notifiedWebhook: "Webhook da usare"
deleteConfirm: "Vuoi davvero rimuovere il destinatario della notifica?"
_moderationLogTypes:
createRole: "Ruolo creato"
deleteRole: "Ruolo eliminato"
@ -2518,6 +2567,12 @@ _moderationLogTypes:
deleteAvatarDecoration: "Eliminazione decorazione della foto profilo"
unsetUserAvatar: "Rimossa foto profilo"
unsetUserBanner: "Rimossa intestazione profilo"
createSystemWebhook: "Crea un SystemWebhook"
updateSystemWebhook: "Modifica SystemWebhook"
deleteSystemWebhook: "Elimina SystemWebhook"
createAbuseReportNotificationRecipient: "Crea destinatario per le notifiche di segnalazioni"
updateAbuseReportNotificationRecipient: "Aggiorna destinatario notifiche di segnalazioni"
deleteAbuseReportNotificationRecipient: "Elimina destinatario notifiche di segnalazioni"
_fileViewer:
title: "Dettagli del file"
type: "Tipo di file"
@ -2643,6 +2698,8 @@ _urlPreviewSetting:
userAgent: "User-Agent"
userAgentDescription: "Definire con quale User-Agent si intende identificarsi durante l'acquisizione di un'anteprima. Se è vuoto, useremo il valore predefinito."
summaryProxy: "Endpoint proxy che genera l'anteprima"
summaryProxyDescription: "Genera anteprime utilizzando un proxy Summaly anziché CherryPick."
summaryProxyDescription2: "I parametri sono collegano al proxy come stringa query. Se il proxy non li supporta, verranno ignorati."
_mediaControls:
pip: "Sovraimpressione"
playbackRate: "Velocità di riproduzione"

View File

@ -137,6 +137,7 @@ copyFileId: "ファイルIDをコピー"
copyFolderId: "フォルダーIDをコピー"
copyProfileUrl: "プロフィールURLをコピー"
searchUser: "ユーザーを検索"
searchThisUsersNotes: "ユーザーのノートを検索"
reply: "返信"
loadMore: "もっと見る"
showMore: "もっと見る"
@ -234,6 +235,7 @@ editList: "リストを編集"
selectChannel: "チャンネルを選択"
selectAntenna: "アンテナを選択"
editAntenna: "アンテナを編集"
createAntenna: "アンテナを作成"
selectWidget: "ウィジェットを選択"
editWidgets: "ウィジェットを編集"
editWidgetsExit: "編集を終了"
@ -274,6 +276,7 @@ followConfirm: "{name}をフォローしますか?"
proxyAccount: "プロキシアカウント"
proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。"
host: "ホスト"
selectSelf: "自分を選択"
selectUser: "ユーザーを選択"
recipient: "宛先"
annotation: "注釈"
@ -289,6 +292,7 @@ perDay: "1日ごと"
stopActivityDelivery: "アクティビティの配送を停止"
blockThisInstance: "このサーバーをブロック"
silenceThisInstance: "サーバーをサイレンス"
mediaSilenceThisInstance: "サーバーをメディアサイレンス"
operations: "操作"
software: "ソフトウェア"
version: "バージョン"
@ -310,6 +314,8 @@ blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。"
silencedInstances: "サイレンスしたサーバー"
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。"
mediaSilencedInstances: "メディアサイレンスしたサーバー"
mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。"
muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー"
@ -1215,6 +1221,8 @@ preservedUsernames: "予約ユーザー名"
preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。"
createNoteFromTheFile: "このファイルからノートを作成"
archive: "アーカイブ"
archived: "アーカイブ済み"
unarchive: "アーカイブ解除"
channelArchiveConfirmTitle: "{name}をアーカイブしますか?"
channelArchiveConfirmDescription: "アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。"
thisChannelArchived: "このチャンネルはアーカイブされています。"
@ -1225,6 +1233,9 @@ preventAiLearning: "生成AIによる学習を拒否"
preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。"
options: "オプション"
specifyUser: "ユーザー指定"
lookupConfirm: "照会しますか?"
openTagPageConfirm: "ハッシュタグのページを開きますか?"
specifyHost: "ホスト指定"
failedToPreviewUrl: "プレビューできません"
update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール"
@ -1362,6 +1373,8 @@ inquiry: "お問い合わせ"
tryAgain: "もう一度お試しください。"
confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
createdLists: "作成したリスト"
createdAntennas: "作成したアンテナ"
showUnreadNotificationsCount: "未読の通知の数を表示する"
showCatOnly: "キャット付きのみ"
additionalPermissionsForFlash: "Playへの追加許可"
@ -1691,7 +1704,7 @@ _achievements:
_login3:
title: "ビギナーⅠ"
description: "通算ログイン日数が3日"
flavor: "今日からね僕は ミスキストってことで"
flavor: "今日からね僕は チェリーピキストってことで"
_login7:
title: "ビギナーⅡ"
description: "通算ログイン日数が7日"
@ -2313,6 +2326,7 @@ _soundSettings:
driveFileTypeWarnDescription: "音声ファイルを選択してください"
driveFileDurationWarn: "音声が長すぎます"
driveFileDurationWarnDescription: "長い音声を使用するとCherryPickの使用に支障をきたす可能性があります。それでも続行しますか"
driveFileError: "音声が読み込めませんでした。設定を変更してください"
_ago:
future: "未来"
@ -2820,6 +2834,7 @@ _webhookSettings:
_systemEvents:
abuseReport: "ユーザーから通報があったとき"
abuseReportResolved: "ユーザーからの通報を処理したとき"
userCreated: "ユーザーが作成されたとき"
deleteConfirm: "Webhookを削除しますか"
_abuseReport:
@ -3022,6 +3037,12 @@ _mediaControls:
playbackRate: "再生速度"
loop: "ループ再生"
_contextMenu:
title: "コンテキストメニュー"
app: "アプリケーション"
appWithShift: "Shiftキーでアプリケーション"
native: "ブラウザのUI"
_abuse:
_resolver:
1hour: "一時間"

View File

@ -108,6 +108,7 @@ enterEmoji: "絵文字を入れてや"
renote: "リノート"
unrenote: "リノートやめる"
renoted: "リノートしたで。"
renotedToX: "{name}にリノートしたで"
cantRenote: "この投稿はリノートできへんっぽい。"
cantReRenote: "リノート自体はリノートできへんで。"
quote: "引用"
@ -313,6 +314,7 @@ selectFile: "ファイル選んでや"
selectFiles: "ファイル選んでや"
selectFolder: "フォルダ選んでや"
selectFolders: "フォルダ選んでや"
fileNotSelected: "ファイルが選択されてへんで"
renameFile: "ファイル名をいらう"
folderName: "フォルダー名"
createFolder: "フォルダー作る"
@ -477,6 +479,7 @@ noteOf: "{user}はんのノート"
inviteToGroup: "グループに招く"
quoteAttached: "引用付いとるで"
quoteQuestion: "引用として添付してもええか?"
attachAsFileQuestion: "クリップボードのテキストが長すぎるからテキストファイルとして添付してもええか?"
noMessagesYet: "まだチャットはあらへんで"
newMessageExists: "新しいメッセージがきたで"
onlyOneFileCanBeAttached: "ごめんな、メッセージに添付できるファイルはひとつだけなんよ。"
@ -844,6 +847,7 @@ administration: "管理"
accounts: "アカウント"
switch: "切り替え"
noMaintainerInformationWarning: "管理者情報が設定されてへんで"
noInquiryUrlWarning: "問い合わせ先URLが設定されてへんで。"
noBotProtectionWarning: "Botプロテクションが設定されてへんで。"
configure: "設定する"
postToGallery: "ギャラリーへ投稿"
@ -1449,7 +1453,7 @@ _achievements:
_login3:
title: "ビギナーⅠ"
description: "通算3日ログインした"
flavor: "今日からワシはミスキストやで"
flavor: "今日からワシはチェリーピキストやで"
_login7:
title: "ビギナーⅡ"
description: "通算7日ログインした"
@ -2008,8 +2012,6 @@ _sfx:
notification: "通知"
chat: "チャット"
chatBg: "チャット(バックグラウンド)"
antenna: "アンテナ受信"
channel: "チャンネル通知"
reaction: "ツッコミ選んどるとき"
_soundSettings:
driveFile: "ドライブん中の音使う"
@ -2488,6 +2490,12 @@ _webhookSettings:
renote: "リノートされるとき~!"
reaction: "ツッコまれたとき~!"
mention: "メンションがあるとき~!"
deleteConfirm: "ほんまにWebhookをほかしてもええんか"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "メール"
deleteConfirm: "通知先を削除してもええか?"
_moderationLogTypes:
createRole: "ロールを追加すんで"
deleteRole: "ロールほかす"
@ -2650,7 +2658,7 @@ _urlPreviewSetting:
userAgent: "User-Agent"
userAgentDescription: "プレビュー取得時に使用されるUser-Agentを設定するで。空欄の場合、デフォルトのUser-Agentが使用されるで。"
summaryProxy: "プレビューを生成するプロキシのエンドポイント"
summaryProxyDescription: "Misskey本体やなく、サマリープロキシを使用してプレビューを生成するで。"
summaryProxyDescription: "CherryPick本体やなく、サマリープロキシを使用してプレビューを生成するで。"
summaryProxyDescription2: "プロキシには下記パラメータがクエリ文字列として連携されるで。プロキシ側がこれらをサポートせえへんときは、設定値は無視されるで。"
_mediaControls:
pip: "ピクチャインピクチャ"

View File

@ -104,3 +104,7 @@ _deck:
_columns:
notifications: "Ilɣuyen"
list: "Tibdarin"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "Imayl"

View File

@ -805,6 +805,10 @@ _deck:
mentions: "받언 멘션"
_webhookSettings:
name: "이럼"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "전자우펜"
_moderationLogTypes:
suspend: "얼우기"
deleteNote: "노트 뭉캐기"

View File

@ -129,7 +129,7 @@ copyAndEditConfirm: "이 노트를 복사하고 편집할까요? 노트에 포
addToList: "리스트에 추가"
addToAntenna: "안테나에 추가"
sendMessage: "메시지 보내기"
copyRSS: "RSS 복사"
copyRSS: "RSS 주소 복사"
copyUsername: "사용자 이름 복사"
copyUserId: "사용자 ID 복사"
copyNoteId: "노트 ID 복사"
@ -185,6 +185,7 @@ enterEmoji: "이모지 입력"
renote: "리노트"
unrenote: "리노트 취소"
renoted: "리노트했어요!"
renotedToX: "{name}명이 리노트했어요!"
quoted: "노트를 인용했어요!"
replied: "답글을 게시했어요!"
cantRenote: "이 게시물은 리노트할 수 없어요."
@ -192,6 +193,8 @@ cantReRenote: "리노트된 노트는 리노트할 수 없어요."
quote: "인용"
inChannelRenote: "채널 내 리노트"
inChannelQuote: "채널 내 인용"
renoteToChannel: "채널에 리노트"
renoteToOtherChannel: "다른 채널에 리노트"
pinnedNote: "고정된 노트"
pinned: "프로필에 고정"
you: "나"
@ -199,11 +202,11 @@ clickToShow: "클릭하여 보기"
doubleClickToShow: "두 번 탭하여 보기"
sensitive: "열람 주의"
add: "추가"
reaction: "반응"
reactions: "반응"
reaction: "리액션"
reactions: "리액션"
emojiPicker: "이모지 선택기"
pinnedEmojisForReactionSettingDescription: "리액션을 할 때 프로필에 고정해서 표시할 이모지를 설정할 수 있어요"
pinnedEmojisSettingDescription: "이모지를 입력할 때 프로필에 고정해서 표시할 이모지를 설정할 수 있어요"
pinnedEmojisForReactionSettingDescription: "리액션을 할 때 이모지 선택기 상단에 표시할 이모지를 설정할 수 있어요."
pinnedEmojisSettingDescription: "이모지를 입력할 때 이모지 선택기 상단에 표시할 이모지를 설정할 수 있어요."
emojiPickerDisplay: "선택기 표시"
overwriteFromPinnedEmojisForReaction: "리액션 설정 덮어쓰기"
overwriteFromPinnedEmojis: "일반 설정 덮어쓰기"
@ -216,7 +219,7 @@ unmarkAsSensitive: "열람 주의 해제"
enterFileName: "파일명 입력"
mute: "뮤트"
unmute: "뮤트 해제"
renoteMute: "리노트 뮤트하기"
renoteMute: "리노트 뮤트"
renoteUnmute: "리노트 뮤트 해제"
block: "차단"
unblock: "차단 해제"
@ -254,12 +257,16 @@ flagShowTimelineReplies: "타임라인에 노트의 답글을 표시하기"
flagShowTimelineRepliesDescription: "이 설정을 활성화하면 타임라인에 다른 사용자 간의 답글을 표시해요."
autoAcceptFollowed: "팔로우 중인 사용자로부터의 팔로우 요청을 자동 수락"
addAccount: "계정 추가"
reloadAccountsList: "계정 리스트 정보 갱신"
reloadAccountsList: "계정 목록 새로고침"
loginFailed: "로그인에 실패했어요.."
showOnRemote: "리모트에서 보기"
continueOnRemote: "리모트에서 계속하기"
chooseServerOnMisskeyHub: "Misskey Hub에서 서버 찾아보기"
specifyServerHost: "서버 도메인 직접 지정"
inputHostName: "도메인을 입력해 주세요"
general: "일반"
wallpaper: "배경"
setWallpaper: "배경화면 설정"
setWallpaper: "배경 이미지 설정"
removeWallpaper: "배경 제거"
searchWith: "검색: {q}"
youHaveNoLists: "리스트가 없어요"
@ -377,7 +384,7 @@ activity: "활동"
images: "이미지"
image: "이미지"
birthday: "생일"
yearsOld: "{age} 세"
yearsOld: "{age}세"
registeredDate: "등록일"
location: "장소"
theme: "테마"
@ -394,6 +401,7 @@ selectFile: "파일 선택"
selectFiles: "파일 선택"
selectFolder: "폴더 선택"
selectFolders: "폴더 선택"
fileNotSelected: "파일을 선택하지 않았어요."
renameFile: "파일 이름 변경"
folderName: "폴더 이름"
createFolder: "폴더 만들기"
@ -518,13 +526,13 @@ moderationNote: "모더레이션 노트"
addModerationNote: "모더레이션 노트 추가하기"
moderationLogs: "모더레이션 로그"
nUsersMentioned: "{n}명이 언급함"
securityKeyAndPasskey: "보안 키 또는 패스 키"
securityKeyAndPasskey: "보안 키 또는 패스키"
securityKey: "보안 키"
lastUsed: "마지막 사용"
lastUsedAt: "마지막 사용: {t}"
unregister: "등록 해제"
passwordLessLogin: "비밀번호 없이 로그인"
passwordLessLoginDescription: "비밀번호를 사용하지 않고 보안 키 또는 패스 키 등으로만 로그인합니다."
passwordLessLoginDescription: "비밀번호 없이 보안 키 또는 패스키만 사용해서 로그인할 수 있어요."
resetPassword: "비밀번호 재설정"
newPasswordIs: "새로운 비밀번호는 \"{password}\" 에요!"
reduceUiAnimation: "UI 애니메이션 줄이기"
@ -558,10 +566,12 @@ noteOf: "{user}의 노트"
inviteToGroup: "그룹에 초대하기"
quoteAttached: "인용함"
quoteQuestion: "인용해서 작성하시겠어요?"
attachAsFileQuestion: "붙여 넣으려는 글이 너무 길어요. 텍스트 파일로 첨부할까요?"
noMessagesYet: "너무.. 너무 조용해요.."
newMessageExists: "새로운 메시지가 있어요!"
onlyOneFileCanBeAttached: "메시지에는 하나의 파일만 첨부할 수 있어요!"
signinRequired: "진행하기 전에 먼저 로그인이 필요해요"
signinOrContinueOnRemote: "계속하려면 사용하는 계정이 있는 서버로 이동하거나 이 서버에 가입 또는 로그인해야 해요."
invitations: "초대"
invitationCode: "초대 코드"
checking: "확인하고 있어요.."
@ -587,8 +597,8 @@ native: "네이티브"
disableDrawer: "드로어 메뉴를 사용하지 않기"
youHaveNoGroups: "그룹이 없어요"
joinOrCreateGroup: "다른 그룹의 초대를 받거나, 직접 새 그룹을 만들 수 있어요!"
showNoteActionsOnlyHover: "노트 액션 버튼을 마우스를 올렸을 때에만 표시"
showReactionsCount: "노트의 반응 수 표시하기"
showNoteActionsOnlyHover: "노트에 커서를 올렸을 때에만 노트 동작 버튼 표시"
showReactionsCount: "노트의 리액션 수 표시하기"
noHistory: "기록이 없어요"
signinHistory: "로그인 기록"
enableAdvancedMfm: "고급 MFM 활성화"
@ -658,7 +668,7 @@ showInPage: "페이지로 보기"
popout: "새 창으로 열기"
volume: "음량"
masterVolume: "마스터 볼륨"
notUseSound: "사운드 출력 비활성화"
notUseSound: "모든 사운드 음소거"
useSoundOnlyWhenActive: "CherryPick이 활성화된 경우에만 사운드 출력"
details: "자세히"
chooseEmoji: "이모지 선택"
@ -688,7 +698,7 @@ deleteAllFiles: "모든 파일 삭제"
deleteAllFilesConfirm: "모든 파일을 삭제할까요?"
removeAllFollowing: "모든 팔로잉 해제"
removeAllFollowingDescription: "{host}(으)로부터 모든 팔로잉을 해제합니다. 해당 서버가 더 이상 존재하지 않게 된 경우 등에 실행해 주세요."
userSuspended: "이 계정은 정지된 상태에요."
userSuspended: "이 사용자는 정지되었어요."
userSilenced: "이 계정은 사일런스된 상태에요."
yourAccountSuspendedTitle: "계정이 정지되어 있어요"
yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반했거나, 기타 다른 이유로 인해 정지되었어요. 자세한 사항은 관리자에게 문의해 주시고, 계정을 새로 생성하지 마세요."
@ -824,8 +834,8 @@ repliedCount: "받은 답글 수"
renotedCount: "받은 리노트 수"
followingCount: "팔로우 수"
followersCount: "팔로워 수"
sentReactionsCount: "반응 수"
receivedReactionsCount: "받은 반응 수"
sentReactionsCount: "리액션 수"
receivedReactionsCount: "받은 리액션 수"
pollVotesCount: "투표 수"
pollVotedCount: "받은 투표 수"
yes: "예"
@ -934,6 +944,7 @@ administration: "관리"
accounts: "계정"
switch: "전환"
noMaintainerInformationWarning: "관리자 정보를 아직 설정하지 않았어요."
noInquiryUrlWarning: "문의처 주소를 아직 설정하지 않았어요."
noBotProtectionWarning: "봇 방어가 설정되지 않았어요."
configure: "설정하기"
postToGallery: "갤러리에 업로드"
@ -1126,6 +1137,7 @@ thisPostMayBeAnnoyingHome: "홈에 게시"
thisPostMayBeAnnoyingCancel: "그만두기"
thisPostMayBeAnnoyingIgnore: "이대로 게시"
collapseRenotes: "이미 본 리노트를 간략화하기"
collapseRenotesDescription: "리액션이나 리노트한 노트를 접어서 표시해요."
collapseDefault: "특정 MFM 구문이 포함된 노트 간략화하기"
internalServerError: "내부 서버 오류"
internalServerErrorDescription: "내부 서버에서 예기치 않은 오류가 발생했어요."
@ -1342,6 +1354,13 @@ useTotp: "일회용 비밀번호 사용"
useBackupCode: "백업 코드 사용"
launchApp: "앱 실행"
useNativeUIForVideoAudioPlayer: "미디어 재생 시 브라우저 UI 사용"
keepOriginalFilename: "원본 파일 이름 유지"
keepOriginalFilenameDescription: "이 설정을 끄면, 파일을 업로드할 때 파일 이름이 무작위 문자열로 자동으로 변경돼요."
noDescription: "내용에 대한 설명이 없어요"
alwaysConfirmFollow: "팔로우일 때 항상 확인하기"
inquiry: "문의하기"
tryAgain: "다시 시도해 주세요."
confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 한 번 더 확인"
showUnreadNotificationsCount: "읽지 않은 알림 수 표시"
showCatOnly: "고양이만 보기"
additionalPermissionsForFlash: "Play에 대한 추가 권한"
@ -1365,9 +1384,14 @@ _showingAnimatedImages:
_messaging:
direct: "다이렉트 메시지"
_delivery:
status: "전송 상태"
stop: "정지됨"
resume: "전송 재개"
_type:
none: "배포 중"
manuallySuspended: "수동 정지 상태"
goneSuspended: "서버 삭제로 인한 정지 상태"
autoSuspendedForNotResponding: "서버가 응답하지 않아 일시적으로 정지됨"
_bubbleGame:
howToPlay: "플레이 방법"
hold: "홀드"
@ -1427,7 +1451,7 @@ _cherrypick:
hide: "모두 숨기기"
patch: "패치"
patchDescription: "Misskey의 기능을 변경해요."
reactableRemoteReaction: "서버에 리모트 이모지와 이름이 같은 이모지가 있으면 리모트 이모지에도 반응할 수 있음"
reactableRemoteReaction: "서버에 리모트 이모지와 이름이 같은 이모지가 있으면 리모트 이모지에도 리액션할 수 있음"
showFollowingMessageInsteadOfButton: "이미 팔로우한 경우 알림 필드에 팔로우 버튼을 표시하지 않음"
mobileHeaderChange: "모바일 환경에서 헤더 디자인을 변경"
renameTheButtonInPostFormToNya: "노트 작성 화면의 '노트' 버튼을 '냥!'으로 변경"
@ -1574,6 +1598,8 @@ _serverSettings:
fanoutTimelineDescription: "활성화하면 각종 타임라인을 가져올 때의 성능을 대폭 향상하며, 데이터베이스의 부하를 줄일 수 있어요. 단, Redis의 메모리 사용량이 증가하게 되고, 서버의 메모리 용량이 작거나, 서비스가 불안정해지는 경우 해당 설정을 비활성화해 주세요."
fanoutTimelineDbFallback: "데이터베이스 폴백"
fanoutTimelineDbFallbackDescription: "활성화하면 타임라인의 캐시되어 있지 않은 부분에 대해서는 DB에 추가로 쿼리하는 폴백 처리를 수행해요. 비활성화하면 폴백 처리를 하지 않아 서버의 부하를 줄일 수 있지만, 타임라인을 가져올 수 있는 범위가 한정돼요."
inquiryUrl: "문의처 URL"
inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정할 수 있어요."
_accountMigration:
moveFrom: "다른 계정에서 이 계정으로 이사"
moveFromSub: "다른 계정에 대한 별칭을 생성"
@ -1894,14 +1920,15 @@ _role:
canManageAvatarDecorations: "아바타 장식 관리"
driveCapacity: "드라이브 용량"
alwaysMarkNsfw: "파일을 항상 NSFW로 지정"
pinMax: "고정할 수 있는 노트 수"
antennaMax: "만들 수 있는 안테나 수"
wordMuteMax: "단어 뮤트할 수 있는 문자 수"
webhookMax: "생성할 수 있는 웹훅 수"
clipMax: "생성할 수 있는 클립 수"
noteEachClipsMax: "각 클립에 추가할 수 있는 노트 수"
userListMax: "생성할 수 있는 사용자 리스트 수"
userEachUserListsMax: "사용자 리스트에 추가할 수 있는 사용자 수"
canUpdateBioMedia: "아바타 및 배너 이미지 변경 허용"
pinMax: "고정할 수 있는 최대 노트 수"
antennaMax: "만들 수 있는 최대 안테나 수"
wordMuteMax: "단어 뮤트할 수 있는 최대 문자 수"
webhookMax: "생성할 수 있는 최대 Webhook 수"
clipMax: "생성할 수 있는 최대 클립 수"
noteEachClipsMax: "각 클립에 추가할 수 있는 최대 노트 수"
userListMax: "생성할 수 있는 최대 사용자 리스트 수"
userEachUserListsMax: "사용자 리스트에 추가할 수 있는 최대 사용자 수"
rateLimitFactor: "요청 빈도 제한"
descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화돼요."
canHideAds: "광고 숨기기"
@ -1912,6 +1939,11 @@ _role:
roleAssignedTo: "수동 역할에 이미 할당됨"
isLocal: "로컬 사용자"
isRemote: "리모트 사용자"
isCat: "계정을 고양이로 설정한 사용자"
isBot: "계정을 봇으로 설정한 사용자"
isSuspended: "정지된 사용자"
isLocked: "‘팔로우를 수동으로 승인’을 활성화한 사용자"
isExplorable: "‘계정을 쉽게 발견하도록 하기’를 활성화한 사용자"
createdLessThan: "가입한 지 다음 일수 이내인 사용자"
createdMoreThan: "가입한 지 다음 일수 이상인 사용자"
followersLessThanOrEq: "팔로워 수가 다음 이하인 사용자"
@ -2226,8 +2258,6 @@ _sfx:
notification: "알림"
chat: "대화"
chatBg: "대화 (백그라운드)"
antenna: "안테나 수신"
channel: "채널 알림"
reaction: "리액션 선택"
_soundSettings:
driveFile: "드라이브에 있는 오디오 파일 사용"
@ -2288,6 +2318,7 @@ _2fa:
backupCodesDescription: "인증 앱을 사용할 수 없게 된 경우 아래 백업 코드를 사용하여 계정에 액세스 할 수 있습니다.이 코드들은 반드시 안전한 장소에 보관하십시오.각 코드는 한 번만 사용할 수 있습니다."
backupCodeUsedWarning: "백업 코드가 사용되었습니다.인증 앱을 사용할 수 없게 된 경우, 조속히 인증 앱을 다시 설정해 주십시오."
backupCodesExhaustedWarning: "백업 코드가 모두 사용되었습니다.인증 앱을 사용할 수 없는 경우 더 이상 계정에 액세스하는 것이 불가능합니다.인증 앱을 다시 등록해 주세요."
moreDetailedGuideHere: "여기에 자세한 설명이 있어요."
_permissions:
"read:account": "계정의 정보를 조회합니다"
"write:account": "계정의 정보를 변경합니다"
@ -2477,7 +2508,7 @@ _postForm:
b: "무슨 일이 일어나고 있나요?"
c: "무엇을 생각하고 있나요?"
d: "말하고 싶은 게 있나요?"
e: "여기에 적어주세요"
e: "여기에 적어 주세요"
f: "작성해주시길 기다리고 있어요..."
_profile:
name: "이름"
@ -2611,7 +2642,7 @@ _notification:
youGotMention: "{name} 님이 나를 멘션했어요!"
youGotReply: "{name} 님이 답글을 달았어요!"
youGotQuote: "{name} 님이 인용했어요!"
youGotReact: "{name} 님이 반응했어요!"
youGotReact: "{name} 님이 리액션했어요!"
youRenoted: "{name} 님이 리노트했어요!"
youWereFollowed: "새로운 팔로워가 있어요!"
youReceivedFollowRequest: "새로운 팔로우 요청이 있어요!"
@ -2627,7 +2658,7 @@ _notification:
checkNotificationBehavior: "알림 표시 확인하기"
sendTestNotification: "테스트 알림 보내기"
notificationWillBeDisplayedLikeThis: "알림이 이렇게 표시돼요!"
reactedBySomeUsers: "{n}명이 반응했어요"
reactedBySomeUsers: "{n}명이 리액션했어요"
likedBySomeUsers: "{n}명이 좋아요를 눌렀어요"
renotedBySomeUsers: "{n}명이 리노트했어요"
followedBySomeUsers: "{n}명에게 팔로우됨"
@ -2640,7 +2671,7 @@ _notification:
reply: "답글"
renote: "리노트"
quote: "인용"
reaction: "반응"
reaction: "리액션"
pollEnded: "투표가 종료됨"
receiveFollowRequest: "팔로우 요청을 받음"
followRequestAccepted: "팔로우 요청이 승인됨"
@ -2656,6 +2687,7 @@ _deck:
alwaysShowMainColumn: "메인 칼럼 항상 표시"
columnAlign: "칼럼 정렬"
addColumn: "칼럼 추가"
newNoteNotificationSettings: "새 노트 알림 설정"
configureColumn: "칼럼 설정"
swapLeft: "왼쪽으로 이동"
swapRight: "오른쪽으로 이동"
@ -2694,6 +2726,7 @@ _drivecleaner:
orderByCreatedAtAsc: "등록일이 오래된 순"
_webhookSettings:
createWebhook: "Webhook 생성"
modifyWebhook: "Webhook 수정"
name: "이름"
secret: "시크릿"
events: "Webhook을 실행할 타이밍"
@ -2706,6 +2739,26 @@ _webhookSettings:
renote: "누군가 내 글을 리노트 했을 때"
reaction: "누군가 내 노트에 리액션 했을 때"
mention: "누군가 나를 멘션 했을 때"
_systemEvents:
abuseReport: "사용자로부터 신고를 받았을 때"
abuseReportResolved: "받은 신고를 처리했을 때"
userCreated: "사용자가 생성되었을 때"
deleteConfirm: "이 Webhook을 삭제할까요?"
_abuseReport:
_notificationRecipient:
createRecipient: "신고 수신자 추가"
modifyRecipient: "신고 수신자 편집"
recipientType: "알림 수신 유형"
_recipientType:
mail: "이메일"
webhook: "Webhook"
_captions:
mail: "모더레이터 권한을 가진 사용자의 이메일 주소에 알림 보내기 (신고를 받았을 때만)"
webhook: "지정한 SystemWebhook으로 알림 보내기 (신고를 받았을 때와 해결했을 때 송신)"
keywords: "키워드"
notifiedUser: "신고 알림을 보낼 사용자"
notifiedWebhook: "사용할 Webhook"
deleteConfirm: "수신자를 삭제할까요?"
_moderationLogTypes:
createRole: "역할 생성"
deleteRole: "역할 삭제"
@ -2743,6 +2796,12 @@ _moderationLogTypes:
deleteAvatarDecoration: "아바타 장식 삭제"
unsetUserAvatar: "이 사용자의 아바타 제거"
unsetUserBanner: "이 사용자의 배너 제거"
createSystemWebhook: "SystemWebhook 생성"
updateSystemWebhook: "SystemWebhook 수정"
deleteSystemWebhook: "SystemWebhook 삭제"
createAbuseReportNotificationRecipient: "신고 알림 수신자 생성"
updateAbuseReportNotificationRecipient: "신고 알림 수신자 편집"
deleteAbuseReportNotificationRecipient: "신고 알림 수신자 삭제"
_fileViewer:
title: "파일 상세"
type: "파일 유형"

View File

@ -18,15 +18,15 @@ enterUsername: "ປ້ອນຊື່ຜູ້ໃຊ້"
renotedBy: "Renoted ໂດຍ {user}"
noNotes: "ບໍ່ມີ note"
noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ"
instance: "ອີນສະແຕນ"
settings: "ກຳນົດຄ່າ"
instance: "ເຊີຟເວີຣ໌"
settings: "ຕັ້ງຄ່າ"
notificationSettings: "ຕັ້ງຄ່າການແຈ້ງເຕືອນ"
basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ"
otherSettings: "ການຕັ້ງຄ່າອື່ນໆ"
openInWindow: "ເປີດໃນປ່ອງຢ້ຽມ"
profile: "ໂພຼຟາຍ"
openInWindow: "ເປີດໃນ window"
profile: "ໂປຣໄຟລ໌"
timeline: "ໄທມ໌ໄລນ໌"
noAccountDescription: "ຜູ້ໃຊ້ນີ້ຍັງບໍ່ໄດ້ຂຽນໃນຊີວະປະຫວັດຂອງເຂົາເຈົ້າເທື່ອ"
noAccountDescription: "ຜູ້ໃຊ້ຄົນນີ້ຍັງບໍ່ໄດ້ຂຽນຄຳແນະນຳໂຕ"
login: "ເຂົ້າ​ສູ່​ລະ​ບົບ"
loggingIn: "ກຳລັງເຂົ້າສູ່ລະບົບ..."
logout: "ອອກ​ຈາກ​ລະ​ບົບ"
@ -37,7 +37,7 @@ users: "ຜູ້ໃຊ້"
addUser: "ເພີ່ມຜູ້ໃຊ້"
favorite: "ເພີ່ມໃສ່ລາຍການທີ່ມັກ"
favorites: "ລາຍການທີ່ມັກ"
unfavorite: "ລຶບອອກຈາກລາຍການທີ່ມັກ"
unfavorite: "ເອົາອອກຈາກລາຍການທີ່ມັກ"
favorited: "ເພີ່ມໃສ່ລາຍການທີ່ມັກແລ້ວ"
alreadyFavorited: "ເພີ່ມເຂົ້າໃນລາຍການທີ່ມັກແລ້ວ."
cantFavorite: "ບໍ່ສາມາດເພີ່ມໃສ່ລາຍການທີ່ມັກໄດ້."
@ -48,41 +48,41 @@ copyLink: "ຄັດລອກລິ້ງ"
copyLinkRenote: "ຄັດລອກລິ້ງຂອງ renote"
delete: "ລຶບ"
deleteAndEdit: "ລຶບ​ແລະ​ແກ້​ໄຂ​"
deleteAndEditConfirm: "ເຈົ້າ​ແນ່​ໃຈ​ບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບ note ນີ້ ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍ reaction, renote, ແລະການຕອບກັບທັງໝົດ"
deleteAndEditConfirm: "ຕ້ອງການລຶບ note ນີ້ແລະແກ້ໄຂໃໝ່ແມ່ນບໍ່? reaction, renote ແລະການຕອບກັບຕໍ່ note ນີ້ ທັງເບິດຈະຖືກລຶບອອກ"
addToList: "ເພີ່ມໃສ່ລາຍຊື່"
addToAntenna: "ເພີ່ມໃສ່ເສົາອາກາດ"
sendMessage: "ສົ່ງຂໍ້ຄວາມ"
copyRSS: "ສຳເນົາ RSS"
copyUsername: "ສຳເນົາຊື່ຜູ້ໃຊ້"
copyUserId: "ສຳເນົາ ID ຜູ້ໃຊ້"
copyNoteId: "ສຳເນົາ ID ບັນທຶກ"
copyFileId: "ສຳເນົາ ID ໄຟລ໌"
copyFolderId: "ສຳເນົາ ID ໂຟນເດີ"
copyProfileUrl: "ສຳເນົາ URL ໂປຣໄຟລ໌"
copyRSS: "ຄັດລອກ RSS"
copyUsername: "ຄັດລອກຊື່ຜູ້ໃຊ້"
copyUserId: "ຄັດລອກ ID ຜູ້ໃຊ້"
copyNoteId: "ຄັດລອກ ID ຂອງ note"
copyFileId: "ຄັດລອກ ID ໄຟລ໌"
copyFolderId: "ຄັດລອກ ID ໂຟລ໌ເດີຣ໌"
copyProfileUrl: "ຄັດລອກ URL ໂປຣໄຟລ໌"
searchUser: "ຄົ້ນຫາຜູ້ໃຊ້"
reply: "ຕອບ​ໄປ​ທີ"
reply: "ຕອບ​ກັບ"
loadMore: "ໂຫຼດເພີ່ມເຕີມ"
showMore: "ໂຫຼດເພີ່ມເຕີມ"
showLess: "ປິດ"
youGotNewFollower: "ໄດ້ຕິດຕາມທ່ານ"
receiveFollowRequest: "ປະຕິບັດຕາມຄໍາຮ້ອງຂໍທີ່ໄດ້ຮັບ"
followRequestAccepted: "ຜູ້ຕິດຕາມໄດ້ຍອມຮັບຄໍາຮ້ອງຂໍຂອງທ່ານ"
mention: "ກ່າວຖືງ"
mentions: "ກ່າວເຖິງ"
youGotNewFollower: "ໄດ້ຕິດຕາມເຈົ້າ"
receiveFollowRequest: "ມີຄຳຂໍຕິດຕາມສົ່ງມາ"
followRequestAccepted: "ການຕິດຕາມໄດ້ຮັບອນຸຍາດແລ້ວ"
mention: "ເວົ້າເຖີງ"
mentions: "ເວົ້າເຖີງເຈົ້າ"
directNotes: "ໂພສ Direct note"
importAndExport: "ນໍາເຂົ້າ / ສົ່ງອອກ"
import: "ນຳເຂົ້າ"
export: "ສົ່ງອອກ"
files: "ໄຟລ໌"
download: "ດາວໂຫລດ"
driveFileDeleteConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການລຶບໄຟລ໌ \"{name}\"? note ທີ່ມີໄຟລ໌ແນບນີ້ຈະຖືກລຶບຖິ້ມ"
unfollowConfirm: "ທ່ານແນ່ໃຈບໍ່ວ່າຕ້ອງການເຊົາຕິດຕາມ {name}?"
exportRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການສົ່ງອອກ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ ແລະມັນຈະຖືກເພີ່ມໃສ່ drive ຂອງທ່ານເມື່ອມັນສຳເລັດແລ້ວ"
importRequested: "ໃນເວລາທີ່ທ່ານໄດ້ຮ້ອງຂໍການນໍາເຂົ້າ ມັນອາດຈະໃຊ້ເວລາບາງເວລາ"
driveFileDeleteConfirm: "ຕ້ອງການລຶບໄຟລ໌ “{name}” ແມ່ນບໍ່? Note ທີ່ແນບມາກັບໄຟລ໌ນີ້ຈະຖືກລຶບອອກ"
unfollowConfirm: "ຕ້ອງການເລີກຕິດຕາມ {name} ແມ່ນບໍ່?"
exportRequested: "ເຈົ້າໄດ້ຮ້ອງຂໍການສົ່ງອອກ ອາດໃຊ້ເວລາຈັກໜ່ອຍ ເມື່ອແລ້ວຈະຖືກເພີ່ມໃສ່ drive"
importRequested: "ເຈົ້າໄດ້ຮ້ອງຂໍການນຳເຂົ້າ ການດຳເນິນການນີ້ອາດໃຊ້ເວລາຈັກໜ່ອຍ"
lists: "ລາຍການ"
noLists: "ທ່ານ​ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​"
note: "ບັນທຶກ"
notes: "ບັນທຶກ"
noLists: "ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​"
note: "Note"
notes: "Note"
following: "ກຳລັງຕິດຕາມ"
followers: "ຜູ້ຕິດຕາມ"
followsYou: "ຕິດ​ຕາມ​ເຈົ້າ"
@ -124,11 +124,11 @@ reactions: "reaction"
attachCancel: "ເອົາໄຟລ໌ແນບ"
mute: "ປີດສຽງ"
unmute: "ເປີດສຽງ"
block: "ບອກ"
unblock: "ຍົກເລີກກາຮົບລັອກ"
block: "ບລັອກ"
unblock: "ເລີກບລັອກ"
suspend: "ລະງັບ"
unsuspend: "ເຊົາ​ລະ​ງັບ"
selectList: "ເລືອກບັນຊີລາຍການ"
selectList: "ເລືອກລາຍຊື່"
editList: "ແກ້ໄຂລາຍຊື່"
selectChannel: "ເລືອກຊ່ອງ"
selectAntenna: "ເລືອກເສົາອາກາດ"
@ -151,30 +151,30 @@ flagShowTimelineRepliesDescription: "ສະແດງການຕອບກັບ
autoAcceptFollowed: "ອະນຸມັດອັດຕະໂນມັດຕາມຄຳຮ້ອງຂໍຈາກຜູ້ໃຊ້ທີ່ທ່ານກຳລັງຕິດຕາມຢູ່"
addAccount: "ເພີ່ມບັນຊີ"
loginFailed: "ການເຂົ້າສູ່ລະບົບບໍ່ສຳເລັດ"
showOnRemote: "ເບິ່ງຢູ່ໃນຕົວຢ່າງໄລຍະໄກ"
showOnRemote: "ເບິ່ງໃນເຊີຟເວີຣ໌ໄລຍະໄກ"
general: "ທົ່ວໄປ"
wallpaper: "ພາບພື້ນຫລັງ"
setWallpaper: "ຕັ້ງເປັນພາບພື້ນຫຼັງ"
removeWallpaper: "ລຶບຮູບວໍເປເປີອອກ"
searchWith: "ຊອກຫາ: {q}"
youHaveNoLists: "ທ່ານ​ບໍ່​ມີ​ລາຍ​ການ​ໃດໆ​"
youHaveNoLists: "ເຈົ້າບໍ່ມີລາຍຊື່ໃດໆ"
proxyAccount: "ບັນຊີພຣັອກຊີ"
host: "ໂຮສ"
host: "ໂຮຕ໌"
selectUser: "ເລືອກຜູ້ໃຊ້"
recipient: "ເຖິງ"
annotation: "ຄຳເຫັນ"
federation: "ສະຫະພັນ"
instances: "ອີນສະແຕນ"
instances: "ເຊີຟເວີຣ໌"
registeredAt: "ລົງທະບຽນຢູ່"
storageUsage: "ບ່ອນ​ຈັດ​ເກັບ​ຂໍ້​ມູນທີ່ໃຊ້"
charts: "ອັນດັບເພງ"
charts: "ແຜນພູມ"
perHour: "ຕໍ່ຊົ່ວໂມງ"
perDay: "ຕໍ່​ມື້"
stopActivityDelivery: "ຢຸດເຊົາການສົ່ງກິດຈະກໍາ"
blockThisInstance: "ຂັດຂວາງຕົວຢ່າງນີ້"
operations: "ການດຳເນີນງານ"
software: "ຊອບແວ"
version: "ສະບັບ"
version: "ເວີຣ໌ຊັນ"
metadata: "Metadata"
withNFiles: "{n} ໄຟລ໌(s)"
monitor: "ຈໍພາບ"
@ -199,15 +199,15 @@ federating: "ສະຫະພັນ"
blocked: "ບລັອກແລ້ວ "
suspended: "ໂຈະ"
all: "ທັງໝົດ"
subscribing: "ສະໝັກສະມາຊິກແລັວ"
publishing: "ການ​ພິມ​ເຜີຍ​ແຜ່"
subscribing: "ກຳລັງສະມັກສະມາຊິກ"
publishing: "ກຳລັງ​ເຜີຍ​ແພ່"
notResponding: "ບໍ່ຕອບສະໜອງ"
instanceFollowing: "ກຳລັງຕິດຕາມສຸດຕົວຢ່າງ"
instanceFollowers: "ຜູ້ຕິດຕາມຕົວຢ່າງ"
instanceUsers: "ຜູ້​ຊົມ​ໃຊ້​ຂອງ​ຕົວ​ຢ່າງ​ນີ້​"
instanceFollowing: "ກຳລັງຕິດຕາມບົນເຊີຟເວີຣ໌"
instanceFollowers: "ຜູ້ຕິດຕາມຂອງເຊີຟເວີຣ໌"
instanceUsers: "ຜູ້​ໃຊ້​ຂອງ​ເຊີຟເວີຣ໌ນີ້"
changePassword: "ປ່ຽນ​ລະ​ຫັດ​ຜ່ານ"
security: "ຄວາມປອດໄພ"
retypedNotMatch: "ວັດສະດຸປ້ອນບໍ່ກົງກັນ"
retypedNotMatch: "ປ້ອນຂໍ້ມູນບໍ່ກົງກັນ"
currentPassword: "ລະຫັດຜ່ານປະຈຸບັນ"
newPassword: "ລະຫັດຜ່ານໃໝ່"
newPasswordRetype: "ໃສ່ລະຫັດຜ່ານໃໝ່ອີກເທື່ອໜຶ່ງ"
@ -223,14 +223,14 @@ remove: "ລຶບ"
removed: "ລຶບແລ້ວ"
resetAreYouSure: "ຣີ​ເຊັດບໍ?"
saved: "ບັນທຶກແລ້ວ"
messaging: "ແຊ໋ດ"
messaging: "ແຊັຕ"
upload: "ອັບໂຫຼດ"
keepOriginalUploading: "ຮັກສາຮູບພາບຕົ້ນສະບັບ"
fromDrive: "ຈາກ Drive"
fromUrl: "ຈາກ URL"
uploadFromUrl: "ອັບໂຫຼດຈາກ URL"
uploadFromUrlDescription: "URL ຂອງໄຟລ໌ທີ່ທ່ານຕ້ອງການອັບໂຫລດ"
uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດ"
uploadFromUrlRequested: "ຮ້ອງຂໍການອັບໂຫລດແລ້ວ"
explore: "ສຳຫຼວດ"
messageRead: "ອ່ານແລ້ວ"
startMessaging: "ເລີ່ມການສົນທະນາໃໝ່"
@ -244,47 +244,47 @@ images: "ຮູບພາບ"
image: "ຮູບພາບ"
birthday: "ວັນເກີດ"
yearsOld: "{age} ປີ"
registeredDate: "ວັນທີ່ເປັນສະມາຊິກ"
registeredDate: "ວັນທີ່ລົງທະບຽນ"
location: "ທີ່ຕັ້ງ"
theme: "ແທ໋ມ"
themeForLightMode: "ຮູບແບບສີສັນເພື່ອໃຊ້ໃນໂໝດແສງ"
themeForDarkMode: "ຮູບແບບສີສັນທີ່ຈະໃຊ້ຢູ່ໃນໂໝດມືດ"
theme: "Theme"
themeForLightMode: "Theme ໃຊ້ໃນໂໝດສະຫວ່າງ"
themeForDarkMode: "Theme ໃຊ້ໃນໂໝດມືດ"
light: "ສະຫວ່າງ"
dark: "ມືດ"
lightThemes: "ຊຸດຮູບແບບສະຫວ່າງ"
darkThemes: "ຮູບແບບສີສັນມືດ"
syncDeviceDarkMode: "ຊິງຄ໌ໂໝດມືດກັບການຕັ້ງຄ່າທົ່ວອຸປະກອນ"
drive: "ຂັບ"
drive: "Drive"
fileName: "ຊື່ໄຟລ໌"
selectFile: "ເລືອກໄຟລ໌"
selectFiles: "ເລືອກໄຟລ໌"
selectFolder: "ເລືອກໂຟລເດີ"
selectFolders: "ເລືອກໂຟລເດີ"
renameFile: "ປ່ຽນຊື່ໄຟລ໌"
folderName: "ຊື່ໂຟນເດີ"
folderName: "ຊື່ໂຟລເດີຣ໌"
createFolder: "​ສ້າງ​ໂຟ​ລ​ເດີ"
renameFolder: "ປ່ຽນຊື່ໂຟນເດີນີ້"
deleteFolder: "ລົບໂຟ​ລ​ເດີ​"
addFile: "ເພີ່ມໄຟລ໌"
emptyDrive: "Drive ຂອງທ່ານຫວ່າງເປົ່າ"
emptyFolder: "ໂຟນເດີນີ້ເປົ່າຫວ່າງ"
emptyFolder: "ໂຟລເດີຣ໌ນີ້ວ່າງເປົ່າ"
unableToDelete: "ບໍ່​ສາ​ມາດລົບໄດ້"
inputNewFileName: "ໃສ່ຊື່ໄຟລ໌ໃໝ່"
inputNewDescription: "ໃສ່ຄຳບັນຍາຍໃໝ່"
inputNewFolderName: "ໃສ່ຊື່ໂຟນເດີໃໝ່"
circularReferenceFolder: "ໂຟນເດີປາຍທາງແມ່ນໂຟນເດີຍ່ອຍຂອງໂຟນເດີທີ່ທ່ານຕ້ອງການຍ້າຍ"
rename: "ປ່ຽນຊື່"
doNothing: "ບໍ່ສົນໃຈ"
watch: "ເບິ່ງ"
unwatch: "ຢຸດເບິ່ງ"
doNothing: "ຢ່າມັນ"
watch: "ເພັ່ງເລັງ"
unwatch: "ຢຸດເພັ່ງເລັງ"
accept: "ອະນຸຍາດ"
reject: "ປະຕິເສດ"
normal: "ປົກກະຕິ"
instanceName: "ຊື່ເຊີເວີ້"
instanceDescription: "ຄໍາອະທິບາຍຕົວຢ່າງ"
instanceDescription: "ຄຳອະທິບາຍແນະນຳເຊີຟເວີຣ໌"
maintainerName: "ຜູ້ດູແລ"
maintainerEmail: "ອີເມວ admin"
tosUrl: "ເງື່ອນໄຂການໃຫ້ບໍລິການ URL"
maintainerEmail: "ອີເມລຜູ້ດູແລ"
tosUrl: " URL ເງື່ອນໄຂການໃຫ້ບໍລິການ"
thisYear: "ປີນີ້"
thisMonth: "ເດືອນນີ້"
today: "ມື້ນີ້"
@ -292,34 +292,34 @@ dayX: "ວັນ {day}"
monthX: "ເດືອນ {month}"
yearX: "ປີ {year}"
pages: "ໜ້າ"
integration: "ຄວາມສຳພັນຂອງ"
integration: "ເຊື່ອມໂຍງ"
connectService: "ເຊື່ອມຕໍ່"
disconnectService: "ຕັດການເຊື່ອມຕໍ່"
enableLocalTimeline: "ເປີດໃຊ້ທາມລາຍທ້ອງຖິ່ນ"
enableGlobalTimeline: "ເປີດໃຊ້ທາມລາຍທົ່ວໂລກ"
disablingTimelinesInfo: "ຜູ້ເບິ່ງແຍງລະບົບ ແລະຜູ້ຄວບຄຸມຈະມີການເຂົ້າເຖິງທຸກກຳນົດເວລາ, ເຖິງແມ່ນວ່າຈະບໍ່ໄດ້ເປີດໃຊ້ງານກໍຕາມ"
disablingTimelinesInfo: "ຜູ້ດູແລລະບບແລະຜູ້ຄວບຄຸມຈະສາມາດເຂົ້າເຖີງໄທມ໌ໄລນ໌ທັ້ງເບີດ ເຖີງວ່າຈະບໍ່ໄດ້ເປີດໃຊ້ງານກໍຕາມ"
registration: "ລົງທະບຽນ"
enableRegistration: "ເປີດໃຊ້ການລົງທະບຽນຜູ້ໃຊ້ໃໝ່"
invite: "ເຊີນ"
driveCapacityPerLocalAccount: "ຄວາມອາດສາມາດຂັບຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ"
driveCapacityPerRemoteAccount: "ໄດຣຟ໌ຄວາມອາດສາມາດຕໍ່ຜູ້ໃຊ້ທາງໄກ"
driveCapacityPerLocalAccount: "ຄວາມຈຸຂອງ drive ຕໍ່ຜູ້ໃຊ້ທ້ອງຖິ່ນ"
driveCapacityPerRemoteAccount: "ຄວາມຈຸຂອງ drive ຕໍ່ຜູ້ໃຊ້ໄລຍະໄກ"
basicInfo: "ຂໍ້ມຸນເບື້ອງຕົ້ນ"
pinnedNotes: "ບັນທຶກທີ່ປັກໝຸດໄວ້"
hcaptchaSiteKey: "ກະແຈໄຊທ໌"
hcaptchaSecretKey: "ກະແຈລັບ"
mcaptchaSiteKey: "ກະແຈໄຊທ໌"
mcaptchaSecretKey: "ກະແຈລັບ"
pinnedNotes: "Note ທີ່ປັກໝຸດໄວ້"
hcaptchaSiteKey: "Site key"
hcaptchaSecretKey: "Secret key"
mcaptchaSiteKey: "Site key"
mcaptchaSecretKey: "Secret Key"
recaptcha: "reCAPTCHA"
enableRecaptcha: "ເປີດໃຊ້ງານລີແຄ໋ບຈາ"
recaptchaSiteKey: "ກະແຈໄຊທ໌"
recaptchaSecretKey: "ກະແຈລັບ"
turnstileSiteKey: "ກະແຈໄຊທ໌"
turnstileSecretKey: "ກະແຈລັບ"
enableRecaptcha: "ເປີດໃຊ້ງານ reCAPTCHA"
recaptchaSiteKey: "Site key"
recaptchaSecretKey: "Secret key"
turnstileSiteKey: "Site key"
turnstileSecretKey: "Secret key"
name: "ຊື່"
userList: "ລາຍການ"
about: "ກ່ຽວກັບ"
aboutMisskey: "ກ່ຽວກັບ CherryPick"
administrator: "ຜູ້ບໍລິຫານ"
administrator: "ຜູ້ດູແລ"
token: "ໂທເຄັນ"
share: "ແບ່ງປັນ"
notFound: "ບໍ່ພົບ"
@ -332,27 +332,27 @@ title: "ຫົວຂໍ້"
text: "ຂໍ້ຄວາມ"
enable: "ເປີດໃຊ້"
next: "ຕໍ່ໄປ"
retype: "ເຂົ້າໄປອີກຄັ້ງ"
quoteAttached: "ວົງຢືມ"
retype: "ລອງພິມລະຫັດອີກເທື່ອໜຶ່ງ"
quoteAttached: "ອ້າງອິງ"
invitations: "ເຊີນ"
unavailable: "ບໍ່​ສາ​ມາດ​ໃຊ້​ໄດ້"
language: "ພາສາ"
aboutX: "ກ່ຽວກັບ {x}"
emojiStyle: "ຮູບແບບອີໂມຈິ"
native: "ພາ​ສາ​ແມ່"
noHistory: "​ບໍ່​ມີ​ລາຍ​ການ​ຢູ່​ບ່ອນ​ນີ້"
noHistory: "​ບໍ່​ມີປະຫວັດ"
doing: "ກຳລັງປະມວນຜົນ..."
category: "ຫມວດຫມູ່"
tags: "ແທ໋ກ"
tags: "Aliases"
createAccount: "ສ້າງບັນຊີ"
existingAccount: "ທີ່ມີຢູ່"
dashboard: "ໜ້າປັດ"
existingAccount: "ບັນຊີທີ່ມີຢູ່ແລ້ວ"
dashboard: "Dashboard"
local: "ທ້ອງຖິ່ນ"
numberOfDays: "ຈຳນວນມື້"
objectStorageBucket: "Bucket"
objectStoragePrefix: "Prefix"
objectStorageEndpoint: "Endpoint"
objectStorageRegion: "ພາກ​ພື້ນ"
objectStorageRegion: "ພູມິພາກ"
deleteAll: "ລຶບທັງໝົດ"
sounds: "ສຽງ"
sound: "ສຽງ"
@ -365,11 +365,11 @@ state: "ສະຖານະ"
sort: "ຈັດຮຽງໂດຍ"
ascendingOrder: "ນ້ອຍໄປຫາໃຫຍ່"
descendingOrder: "ໃຫຍ່ຫານ້ອຍ"
output: "ຜົນຜະລິດ"
script: "ບົດ​ຄວາມ"
output: "Output"
script: "Script"
menu: "ເມນູ"
rearrange: "ຈັດລຽງຄືນ"
poll: "ການພູນ"
rearrange: "ຈັດລຽງໃໝ່"
poll: "Poll"
description: "ລາຍລະອຽດ"
author: "ຜູ້ຂຽນ"
manage: "ການຈັດການ"
@ -383,7 +383,7 @@ permission: "ການອະນຸຍາດ"
notificationType: "​ປະເພດການ​ແຈ້ງ​ເຕືອນ"
edit: "ແກ້ໄຂ"
email: "ອີເມວ"
smtpHost: "ໂຮສ"
smtpHost: "ໂຮຕ໌"
smtpUser: "ຊື່ຜູ້ໃຊ້"
smtpPass: "ລະຫັດຜ່ານ"
clearCache: "ລຶບລ້າງແຄສ"
@ -393,12 +393,12 @@ administration: "ການຈັດການ"
middle: "ປານກາງ"
searchByGoogle: "ຄົ້ນຫາ"
file: "ໄຟລ໌"
replies: "ຕອບ​ໄປ​ທີ"
replies: "ຕອບ​ກັບ"
renotes: "Renote"
_delivery:
stop: "ໂຈະ"
_type:
none: "ການ​ພິມ​ເຜີຍ​ແຜ່"
none: "ກຳລັງ​ເຜີຍ​ແພ່"
_role:
_priority:
middle: "ປານກາງ"
@ -417,8 +417,8 @@ _sfx:
_2fa:
renewTOTPCancel: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້"
_widgets:
profile: "ໂພຼຟາຍ"
instanceInfo: "ອີນສະແຕນ"
profile: "ໂປຣໄຟລ໌"
instanceInfo: "ຂໍ້ມູລເຊີຟເວີຣ໌"
notifications: "ການແຈ້ງເຕືອນ"
timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
activity: "ກິດຈະກຳ"
@ -437,28 +437,28 @@ _profile:
_exportOrImport:
followingList: "ກຳລັງຕິດຕາມ"
muteList: "ປີດສຽງ"
blockingList: "ບອກ"
blockingList: "ບລັອກ"
userLists: "ລາຍການ"
_charts:
federation: "ສະຫະພັນ"
_timelines:
home: "ໜ້າຫຼັກ"
_play:
script: "ບົດ​ຄວາມ"
script: "Script"
summary: "ລາຍລະອຽດ"
_pages:
blocks:
image: "ຮູບພາບ"
_notification:
youWereFollowed: "ໄດ້ຕິດຕາມທ່ານ"
youWereFollowed: "ໄດ້ຕິດຕາມເຈົ້າ"
_types:
follow: "ກຳລັງຕິດຕາມ"
mention: "ໄດ້ກ່າວມາ"
mention: "ໄດ້ກ່າວເຖິງ"
renote: "Renote"
quote: "ລວມຂໍ້ຄວາມອ້າງອີງ"
reaction: "ປະຕິກິລິຍາ"
quote: "ອ້າງອີງ"
reaction: "Reaction"
_actions:
reply: "ຕອບ​ໄປ​ທີ"
reply: "ຕອບ​ກັບ"
renote: "Renote"
_deck:
_columns:
@ -466,8 +466,12 @@ _deck:
tl: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
list: "ລາຍການ"
channel: "ຊ່ອງ"
mentions: "ກ່າວເຖິງ"
mentions: "ກ່າວເຖິງເຈົ້າ"
_webhookSettings:
name: "ຊື່"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "ອີເມວ"
_moderationLogTypes:
suspend: "ລະງັບ"

View File

@ -723,5 +723,9 @@ _deck:
direct: "Direkte"
_webhookSettings:
name: "Navn"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "E-post"
_moderationLogTypes:
suspend: "Suspender"

View File

@ -1299,8 +1299,6 @@ _sfx:
notification: "Powiadomienia"
chat: "Wiadomości"
chatBg: "Rozmowy (tło)"
antenna: "Anteny"
channel: "Powiadomienia kanału"
_ago:
future: "W przyszłości"
justNow: "Przed chwilą"
@ -1636,6 +1634,10 @@ _webhookSettings:
renote: "Po udostępnieniu wpisu"
reaction: "Po otrzymaniu reakcji"
mention: "Po zostaniu wspomnianym"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "Adres e-mail"
_moderationLogTypes:
suspend: "Zawieś"
resetPassword: "Zresetuj hasło"

View File

@ -1082,7 +1082,7 @@ _achievements:
_login3:
title: "Iniciante I"
description: "Fez login por um total de 3 dias"
flavor: "De hoje em diante, me chame apenas de Misskist"
flavor: "De hoje em diante, me chame apenas de Cherrypikist"
_login7:
title: "Iniciante II"
description: "Fez login por um total de 7 dias"
@ -1501,6 +1501,10 @@ _webhookSettings:
follow: "Quando seguindo um usuário"
followed: "Quando sendo seguido"
renote: "Quando repostado"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "E-mail"
_moderationLogTypes:
suspend: "Suspender"
resetPassword: "Redefinir senha"

View File

@ -729,6 +729,10 @@ _deck:
mentions: "Mențiuni"
_webhookSettings:
name: "Nume"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "Email"
_moderationLogTypes:
suspend: "Suspendă"
resetPassword: "Resetează parola"

View File

@ -1190,7 +1190,7 @@ _achievements:
_login3:
title: "Новичок "
description: "3 дня на сайте"
flavor: "С сегодняшнего дня зовите меня просто мискиец"
flavor: "С сегодняшнего дня зовите меня просто Черрипикиец"
_login7:
title: "Новичок Ⅱ"
description: "Неделя на сайте"
@ -1697,8 +1697,6 @@ _sfx:
notification: "Уведомления"
chat: "Сообщения"
chatBg: "Сообщения (фон)"
antenna: "Антенна"
channel: "Канал"
_ago:
future: "Из будущего"
justNow: "Только что"
@ -2072,6 +2070,10 @@ _webhookSettings:
createWebhook: "Создать вебхук"
name: "Название"
active: "Вкл."
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "Электронная почта"
_moderationLogTypes:
suspend: "Заморозить"
addCustomEmoji: "Добавлено эмодзи"

View File

@ -1206,8 +1206,6 @@ _sfx:
notification: "Oznámenia"
chat: "Chat"
chatBg: "Chat (pozadie)"
antenna: "Antény"
channel: "Upozornenia kanála"
_ago:
future: "Budúcnosť"
justNow: "Teraz"
@ -1533,6 +1531,10 @@ _deck:
_webhookSettings:
name: "Názov"
active: "Zapnuté"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "Email"
_moderationLogTypes:
suspend: "Zmraziť"
resetPassword: "Resetovať heslo"

View File

@ -513,7 +513,6 @@ _sfx:
note: "Noter"
notification: "Notifikationer"
chat: "Chatt"
antenna: "Antenner"
_2fa:
renewTOTPCancel: "Nej tack"
_antennaSources:
@ -578,6 +577,10 @@ _deck:
_webhookSettings:
name: "Namn"
active: "Aktiverad"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "E-post"
_moderationLogTypes:
suspend: "Suspendera"
resetPassword: "Återställ Lösenord"

File diff suppressed because it is too large Load Diff

View File

@ -988,7 +988,7 @@ _achievements:
_login3:
title: "Новачок I"
description: "3 дні користування загально"
flavor: "Відсьогодні називайте мене \"Місскіст\""
flavor: "Відсьогодні називайте мене \"Черрипикіст\""
_login7:
title: "Новачок II"
description: "7 днів користування загально"
@ -1396,8 +1396,6 @@ _sfx:
notification: "Сповіщення"
chat: "Чати"
chatBg: "Чати (фон)"
antenna: "Прийом антени"
channel: "Повідомлення каналу"
_ago:
future: "Майбутнє"
justNow: "Щойно"
@ -1703,6 +1701,10 @@ _deck:
_webhookSettings:
name: "Ім'я"
active: "Увімкнено"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "E-mail"
_moderationLogTypes:
suspend: "Призупинити"
resetPassword: "Скинути пароль"

View File

@ -1090,6 +1090,10 @@ _webhookSettings:
_events:
renote: "Qayta qayd qilinganda"
mention: "Eslanganda"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "Email"
_moderationLogTypes:
suspend: "To'xtatish"
resetPassword: "Parolni tiklash"

View File

@ -376,6 +376,7 @@ mcaptcha: "mCaptcha"
enableMcaptcha: "Bật mCaptcha"
mcaptchaSiteKey: "Khóa của trang"
mcaptchaSecretKey: "Khóa bí mật"
mcaptchaInstanceUrl: "URL mCaptcha máy chủ"
recaptcha: "reCAPTCHA"
enableRecaptcha: "Bật reCAPTCHA"
recaptchaSiteKey: "Khóa của trang"
@ -426,6 +427,7 @@ moderator: "Kiểm duyệt viên"
moderation: "Kiểm duyệt"
moderationNote: "Ghi chú kiểm duyệt"
addModerationNote: "Thêm ghi chú kiểm duyệt"
moderationLogs: "Nhật kí quản trị"
nUsersMentioned: "Dùng bởi {n} người"
securityKeyAndPasskey: "Mã bảo mật・Passkey"
securityKey: "Khóa bảo mật"
@ -467,6 +469,7 @@ noteOf: "Tút của {user}"
inviteToGroup: "Mời vào nhóm"
quoteAttached: "Trích dẫn"
quoteQuestion: "Trích dẫn lại?"
attachAsFileQuestion: "Văn bản ở trong bộ nhớ tạm rất dài. Bạn có muốn đăng nó dưới dạng một tệp văn bản không?"
noMessagesYet: "Chưa có tin nhắn"
newMessageExists: "Bạn có tin nhắn mới"
onlyOneFileCanBeAttached: "Bạn chỉ có thể đính kèm một tập tin"
@ -1214,7 +1217,7 @@ _achievements:
_login3:
title: "Sơ cấp I"
description: "Tổng số ngày đăng nhập đạt 3 ngày"
flavor: "Từ nay các bạn cứ xem như mình là một Misskist đó"
flavor: "Từ nay các bạn cứ xem như mình là một Cherrypikist đó"
_login7:
title: "Sơ cấp II"
description: "Tổng số ngày đăng nhập đạt 7 ngày"
@ -1644,8 +1647,6 @@ _sfx:
notification: "Thông báo"
chat: "Trò chuyện"
chatBg: "Chat (Nền)"
antenna: "Trạm phát sóng"
channel: "Kênh"
_ago:
future: "Tương lai"
justNow: "Vừa xong"
@ -2011,6 +2012,10 @@ _webhookSettings:
_events:
reaction: "Khi nhận được sự kiện"
mention: "Khi có người nhắc tới bạn"
_abuseReport:
_notificationRecipient:
_recipientType:
mail: "Email"
_moderationLogTypes:
suspend: "Vô hiệu hóa"
resetPassword: "Đặt lại mật khẩu"

View File

@ -60,6 +60,7 @@ copyFileId: "复制文件ID"
copyFolderId: "复制文件夹ID"
copyProfileUrl: "复制个人资料URL"
searchUser: "搜索用户"
searchThisUsersNotes: "搜索用户帖子"
reply: "回复"
loadMore: "查看更多"
showMore: "查看更多"
@ -154,6 +155,7 @@ editList: "编辑列表"
selectChannel: "选择频道"
selectAntenna: "选择天线"
editAntenna: "编辑天线"
createAntenna: "创建天线"
selectWidget: "选择小工具"
editWidgets: "编辑部件"
editWidgetsExit: "完成编辑"
@ -180,6 +182,10 @@ addAccount: "添加账户"
reloadAccountsList: "更新账户列表"
loginFailed: "登录失败"
showOnRemote: "转到所在服务器显示"
continueOnRemote: "转到所在服务器继续"
chooseServerOnMisskeyHub: "从 Misskey Hub 选择服务器"
specifyServerHost: "直接输入服务器域名"
inputHostName: "请输入域名"
general: "常规设置"
wallpaper: "壁纸"
setWallpaper: "设置壁纸"
@ -190,6 +196,7 @@ followConfirm: "你确定要关注 {name} 吗?"
proxyAccount: "代理账户"
proxyAccountDescription: "代理账户是在某些情况下替代用户进行远程关注用的账户。 例如说,当用户将一位远程用户放入一个列表中时,如果本地服务器上没有任何人关注这位远程用户,则这位远程用户的账户活动将不会被送到本地服务器上。作为替代,此时将使用代理账户进行关注。"
host: "主机名"
selectSelf: "选择自己"
selectUser: "选择用户"
recipient: "收件人"
annotation: "注解"
@ -351,7 +358,7 @@ instanceName: "服务器名称"
instanceDescription: "服务器简介"
maintainerName: "管理员名称"
maintainerEmail: "管理员电子邮箱"
tosUrl: "服务条款 URL"
tosUrl: "服务条款地址"
thisYear: "今年"
thisMonth: "本月"
today: "今天"
@ -433,8 +440,8 @@ administrator: "管理员"
token: "Token (令牌)"
2fa: "双因素认证"
setupOf2fa: "设置双因素认证"
totp: "身份验证应用"
totpDescription: "使用认证应用输入一次性密码。"
totp: "验证器"
totpDescription: "使用验证器输入一次性密码"
moderator: "监察员"
moderation: "管理"
moderationNote: "管理笔记"
@ -486,6 +493,7 @@ noMessagesYet: "现在没有新的聊天"
newMessageExists: "新信息"
onlyOneFileCanBeAttached: "只能添加一个附件"
signinRequired: "请先登录"
signinOrContinueOnRemote: "若要继续,需要转到您所使用的实例,或者在此服务器上注册或登录。"
invitations: "邀请"
invitationCode: "邀请码"
checking: "正在确认"
@ -849,6 +857,7 @@ administration: "管理"
accounts: "账户"
switch: "切换"
noMaintainerInformationWarning: "管理人员信息未设置。"
noInquiryUrlWarning: "尚未设置联络地址。"
noBotProtectionWarning: "Bot 防御未设置。"
configure: "设置"
postToGallery: "发送到图库"
@ -860,7 +869,7 @@ shareWithNote: "在帖子中分享"
ads: "广告"
expiration: "截止时间"
startingperiod: "开始时间"
memo: "便笺"
memo: "备注"
priority: "优先级"
high: "高"
middle: "中"
@ -1114,6 +1123,8 @@ preservedUsernames: "保留的用户名"
preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。"
createNoteFromTheFile: "从文件创建帖子"
archive: "归档"
archived: "已归档"
unarchive: "取消归档"
channelArchiveConfirmTitle: "要将 {name} 归档吗?"
channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。"
thisChannelArchived: "该频道已被归档。"
@ -1124,6 +1135,7 @@ preventAiLearning: "拒绝接受生成式 AI 的学习"
preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。"
options: "选项"
specifyUser: "用户指定"
specifyHost: "指定主机名"
failedToPreviewUrl: "无法预览"
update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "可以使用表情作为回应的角色"
@ -1194,7 +1206,7 @@ externalServices: "外部服务"
sourceCode: "源代码"
sourceCodeIsNotYetProvided: "还未提供源代码。要解决此问题请联系管理员。"
repositoryUrl: "仓库地址"
repositoryUrlDescription: "若源代码所在的仓库是公开的,请填入对应的 URL。若是按原样使用 CherryPick并未追加或者修改代码的情况请填入 https://github.com/kokonect-link/cherrypick。"
repositoryUrlDescription: "若源代码所在的仓库是公开的,请填入对应的 URL。若并未追加或者修改 CherryPick 的代码,请填入 https://github.com/kokonect-link/cherrypick。"
repositoryUrlOrTarballRequired: "若仓库并未公开,则需要提供 tarball 作为替代。详情请看 .config/example.yml。"
feedback: "反馈"
feedbackUrl: "反馈地址"
@ -1255,6 +1267,11 @@ keepOriginalFilenameDescription: "若关闭此设置,上传文件时文件名
noDescription: "没有描述"
alwaysConfirmFollow: "总是确认关注"
inquiry: "联系我们"
tryAgain: "请再试一次"
confirmWhenRevealingSensitiveMedia: "显示敏感内容前需要确认"
sensitiveMediaRevealConfirm: "这是敏感内容。是否显示?"
createdLists: "已创建的列表"
createdAntennas: "已创建的天线"
_delivery:
status: "投递状态"
stop: "停止投递"
@ -1389,6 +1406,8 @@ _serverSettings:
fanoutTimelineDescription: "当启用时,可显著提高获取各种时间线时的性能,并减轻数据库的负荷。但是相对的 Redis 的内存使用量将会增加。如果服务器的内存不是很大,又或者运行不稳定的话可以把它关掉。"
fanoutTimelineDbFallback: "回退到数据库"
fanoutTimelineDbFallbackDescription: "当启用时,若时间线未被缓存,则将额外查询数据库。禁用该功能可通过不执行回退处理进一步减少服务器负载,但会限制可检索的时间线范围。"
inquiryUrl: "联络地址"
inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。"
_accountMigration:
moveFrom: "从别的账号迁移到此账户"
moveFromSub: "为另一个账户建立别名"
@ -1460,7 +1479,7 @@ _achievements:
_login3:
title: "初学者 I"
description: "累计登录 3 天"
flavor: "今天开始我就是 Misskist"
flavor: "今天开始我就是 Cherrypikist"
_login7:
title: "初学者 II"
description: "累计登录 7 天"
@ -1687,8 +1706,8 @@ _role:
descriptionOfIsExplorable: "打开后将公开角色时间线。如果角色不是公开的,就无法公开时间线。"
displayOrder: "显示顺序"
descriptionOfDisplayOrder: "数字越大,显示位置越靠前。"
canEditMembersByModerator: "允许监察编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,监察和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
canEditMembersByModerator: "允许监察编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,监察和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
priority: "优先级"
_priority:
low: "低"
@ -1708,6 +1727,7 @@ _role:
canManageAvatarDecorations: "管理头像挂件"
driveCapacity: "网盘容量"
alwaysMarkNsfw: "总是将文件标记为 NSFW"
canUpdateBioMedia: "可以更新头像和横幅"
pinMax: "帖子置顶数量限制"
antennaMax: "可创建的最大天线数量"
wordMuteMax: "屏蔽词的字数限制"
@ -2019,8 +2039,6 @@ _sfx:
notification: "通知"
chat: "聊天"
chatBg: "聊天背景"
antenna: "天线接收"
channel: "频道通知"
reaction: "选择回应时"
_soundSettings:
driveFile: "使用网盘内的音频"
@ -2055,7 +2073,7 @@ _time:
day: "日"
_2fa:
alreadyRegistered: "此设备已被注册"
registerTOTP: "开始设置认证应用"
registerTOTP: "开始设置验证器"
step1: "首先,在您的设备上安装验证应用,例如 {a} 或 {b}。"
step2: "然后,扫描屏幕上显示的二维码。"
step2Uri: "如果使用桌面应用程序的话,请输入下面的 URI"
@ -2064,23 +2082,23 @@ _2fa:
setupCompleted: "设置完成"
step4: "从现在开始,任何登录操作都将要求您提供动态口令。"
securityKeyNotSupported: "您的浏览器不支持安全密钥。"
registerTOTPBeforeKey: "要注册安全密钥或 Passkey请先设置验证器应用程序。"
registerTOTPBeforeKey: "要注册安全密钥或 Passkey请先设置验证器。"
securityKeyInfo: "注册兼容 WebAuthn 的密钥,例如支持 FIDO2 的硬件安全密钥、设备上的生物识别功能、PIN 码以及 Passkey 等。"
registerSecurityKey: "注册安全密钥或 Passkey"
securityKeyName: "输入密钥名称"
tapSecurityKey: "请按照浏览器说明操作来注册安全密钥或 Passkey。"
removeKey: "删除安全密钥"
removeKeyConfirm: "您确定要删除 {name} 吗?"
whyTOTPOnlyRenew: "如果注册了安全密钥,则无法取消验证器应用程序上的设置。"
renewTOTP: "重置验证器应用程序"
renewTOTPConfirm: "当前验证器应用程序的验证码将不再有效"
whyTOTPOnlyRenew: "当注册了安全密钥时,无法取消使用验证器。"
renewTOTP: "重置验证器"
renewTOTPConfirm: "当前验证器的验证码及备用代码已失效"
renewTOTPOk: "重新配置"
renewTOTPCancel: "不用,谢谢"
checkBackupCodesBeforeCloseThisWizard: "在关闭此窗口前,请确认下面的备用代码"
backupCodes: "备用代码"
backupCodesDescription: "如果无法使用认证应用,可以使用以下的备用代码来访问账户。请务必将这些代码保存在安全的地方。每个代码仅可使用一次。"
backupCodeUsedWarning: "已使用备用代码。如果无法使用认证应用,请尽快重新设定。"
backupCodesExhaustedWarning: "已使用完所有的备用代码。如果无法使用认证应用,将无法再访问您的账户。请再次设定认证应用。"
backupCodesDescription: "如果无法使用验证器,可以使用以下的备用代码来访问账户。请务必将这些代码保存在安全的地方。每个代码仅可使用一次。"
backupCodeUsedWarning: "已使用备用代码。若验证器无法使用,请尽快重置验证器。"
backupCodesExhaustedWarning: "已使用完所有的备用代码。若验证器无法使用,则无法再访问您的账户。请重置验证器。"
moreDetailedGuideHere: "此处为详细指南"
_permissions:
"read:account": "查看账户信息"
@ -2488,6 +2506,7 @@ _drivecleaner:
orderByCreatedAtAsc: "按添加日期降序排列"
_webhookSettings:
createWebhook: "创建 Webhook"
modifyWebhook: "编辑 webhook"
name: "名称"
secret: "密钥"
events: "何时运行 Webhook"
@ -2500,6 +2519,26 @@ _webhookSettings:
renote: "被转发时"
reaction: "被回应时"
mention: "被提及时"
_systemEvents:
abuseReport: "当收到举报时"
abuseReportResolved: "当举报被处理时"
userCreated: "当用户被创建时"
deleteConfirm: "要删除 webhook 吗?"
_abuseReport:
_notificationRecipient:
createRecipient: "新建举报通知"
modifyRecipient: "编辑举报通知"
recipientType: "通知类型"
_recipientType:
mail: "邮箱"
webhook: "Webhook"
_captions:
mail: "当收到新举报时,向持有监察员权限的用户发送通知邮件"
webhook: "当收到新举报及举报被处理时,使用指定的 SystemWebhook 发送通知"
keywords: "关键字"
notifiedUser: "通知的用户"
notifiedWebhook: "使用的 webhook"
deleteConfirm: "要删除通知吗?"
_moderationLogTypes:
createRole: "创建角色"
deleteRole: "删除角色"
@ -2537,6 +2576,12 @@ _moderationLogTypes:
deleteAvatarDecoration: "删除头像挂件"
unsetUserAvatar: "清除用户头像"
unsetUserBanner: "清除用户横幅"
createSystemWebhook: "新建了 SystemWebhook"
updateSystemWebhook: "更新了 SystemWebhook"
deleteSystemWebhook: "删除了 SystemWebhook"
createAbuseReportNotificationRecipient: "新建了举报通知"
updateAbuseReportNotificationRecipient: "更新了举报通知"
deleteAbuseReportNotificationRecipient: "删除了举报通知"
_fileViewer:
title: "文件信息"
type: "文件类型"
@ -2668,3 +2713,8 @@ _mediaControls:
pip: "画中画"
playbackRate: "播放速度"
loop: "循环播放"
_contextMenu:
title: "上下文菜单"
app: "应用"
appWithShift: "Shift 键应用"
native: "浏览器的用户界面"

View File

@ -60,6 +60,7 @@ copyFileId: "複製檔案 ID"
copyFolderId: "複製資料夾ID"
copyProfileUrl: "複製個人資料網址"
searchUser: "搜尋使用者"
searchThisUsersNotes: "搜尋這個使用者的貼文"
reply: "回覆"
loadMore: "載入更多"
showMore: "載入更多"
@ -154,6 +155,7 @@ editList: "編輯清單"
selectChannel: "選擇頻道"
selectAntenna: "選擇天線"
editAntenna: "編輯天線"
createAntenna: "建立天線"
selectWidget: "選擇小工具"
editWidgets: "編輯小工具"
editWidgetsExit: "完成"
@ -180,6 +182,10 @@ addAccount: "新增帳戶"
reloadAccountsList: "更新帳戶清單的資訊"
loginFailed: "登入失敗"
showOnRemote: "轉到所在實例顯示"
continueOnRemote: "在遠端伺服器繼續"
chooseServerOnMisskeyHub: "從 Misskey Hub 選擇伺服器"
specifyServerHost: "直接指定伺服器網域"
inputHostName: "請輸入域名"
general: "一般"
wallpaper: "桌布"
setWallpaper: "設定桌布"
@ -190,6 +196,7 @@ followConfirm: "你真的要追隨{name}嗎?"
proxyAccount: "代理帳戶"
proxyAccountDescription: "代理帳戶是在特定條件下充當遠端追隨者的帳戶。例如,當使用者新增遠端使用者至其列表時,若沒有本地使用者追隨該遠端使用者,則其活動將不會傳送至伺服器,此時便會由代理帳戶代為追隨以解決問題。"
host: "主機"
selectSelf: "選擇自己"
selectUser: "選取使用者"
recipient: "收件人"
annotation: "註解"
@ -243,10 +250,10 @@ noCustomEmojis: "沒有自訂的表情符號"
noJobs: "沒有任務"
federating: "聯邦運作中"
blocked: "已封鎖"
suspended: "已凍結"
suspended: "停止發送"
all: "全部"
subscribing: "訂閱中"
publishing: "直播中"
publishing: "發送中"
notResponding: "沒有回應"
instanceFollowing: "追隨的伺服器"
instanceFollowers: "伺服器的追隨者"
@ -343,7 +350,7 @@ reload: "重新整理"
doNothing: "無視"
reloadConfirm: "確定要重新整理嗎?"
watch: "關注"
unwatch: "取消追隨"
unwatch: "取消關注"
accept: "接受"
reject: "拒絕"
normal: "正常"
@ -486,6 +493,7 @@ noMessagesYet: "沒有訊息"
newMessageExists: "有新的訊息"
onlyOneFileCanBeAttached: "只能加入一個附件"
signinRequired: "請先登入"
signinOrContinueOnRemote: "若要繼續,需前往您所在的伺服器,或者註冊並登入此伺服器"
invitations: "邀請"
invitationCode: "邀請碼"
checking: "確認中"
@ -849,6 +857,7 @@ administration: "管理"
accounts: "帳戶"
switch: "切換"
noMaintainerInformationWarning: "尚未設定管理員訊息。"
noInquiryUrlWarning: "尚未設定聯絡表單網址。"
noBotProtectionWarning: "尚未設定 Bot 防護。"
configure: "設定"
postToGallery: "發佈到相簿"
@ -1114,6 +1123,8 @@ preservedUsernames: "保留的使用者名稱"
preservedUsernamesDescription: "換行列舉要保留的使用者名稱。此處出現的名稱將在註冊時禁用,但由管理者建立帳戶則不受此限。此外,既有的帳戶也不受影響。"
createNoteFromTheFile: "由此檔案建立貼文"
archive: "封存"
archived: "已封存"
unarchive: "取消封存"
channelArchiveConfirmTitle: "要封存{name}嗎?"
channelArchiveConfirmDescription: "封存後,將不會在頻道列表與搜尋結果中顯示,也無法發佈新貼文。"
thisChannelArchived: "這個頻道已被封存。"
@ -1124,6 +1135,7 @@ preventAiLearning: "拒絕接受生成式AI的訓練"
preventAiLearningDescription: "要求站外生成式 AI 不使用您發佈的內容訓練模型。此功能會使伺服器於 HTML 回應新增「noai」標籤而因為要視乎 AI 會否遵守該標籤,所以此功能無法完全阻止所有 AI 使用您的內容。"
options: "選項"
specifyUser: "指定使用者"
specifyHost: "指定主機"
failedToPreviewUrl: "無法預覽"
update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "可以使用此表情符號為反應的角色"
@ -1255,12 +1267,17 @@ keepOriginalFilenameDescription: "如果關閉此設置,上傳時檔案名稱
noDescription: "沒有說明文字"
alwaysConfirmFollow: "點擊追隨時總是顯示確認訊息"
inquiry: "聯絡我們"
tryAgain: "請再試一次。"
confirmWhenRevealingSensitiveMedia: "要顯示敏感媒體時需確認"
sensitiveMediaRevealConfirm: "這是敏感媒體。確定要顯示嗎?"
createdLists: "已建立的清單"
createdAntennas: "已建立的天線"
_delivery:
status: "傳送狀態"
stop: "停止傳送"
resume: "恢復傳送"
stop: "停止送"
resume: "恢復送"
_type:
none: "直播中"
none: "發送中"
manuallySuspended: "手動暫停中"
goneSuspended: "因為伺服器刪除所以暫停中"
autoSuspendedForNotResponding: "因為伺服器沒有回應所以暫停中"
@ -1390,7 +1407,7 @@ _serverSettings:
fanoutTimelineDbFallback: "資料庫的回退"
fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。"
inquiryUrl: "聯絡表單網址"
inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址或包含運營者聯絡資訊網頁的網址。"
inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址或包含運營者聯絡資訊網頁的網址。"
_accountMigration:
moveFrom: "從其他帳戶遷移到這個帳戶"
moveFromSub: "為另一個帳戶建立別名"
@ -1462,7 +1479,7 @@ _achievements:
_login3:
title: "初學者Ⅰ"
description: "總登入天數為三天"
flavor: "從今天開始,我就是 Misskist"
flavor: "從今天開始,我就是 Cherrypikist"
_login7:
title: "初學者ⅠⅠ"
description: "總登入天數為七天"
@ -1711,6 +1728,7 @@ _role:
canManageAvatarDecorations: "管理頭像裝飾"
driveCapacity: "雲端硬碟容量"
alwaysMarkNsfw: "總是將檔案標記為NSFW"
canUpdateBioMedia: "允許更新大頭貼和橫幅"
pinMax: "置頂貼文的最大數量"
antennaMax: "可建立的天線數量"
wordMuteMax: "靜音文字的最大字數"
@ -2022,8 +2040,6 @@ _sfx:
notification: "通知"
chat: "聊天"
chatBg: "聊天背景"
antenna: "天線接收"
channel: "頻道通知"
reaction: "選擇反應時"
_soundSettings:
driveFile: "使用雲端硬碟的音效檔案"
@ -2304,7 +2320,7 @@ _charts:
federation: "聯邦宇宙"
apRequest: "請求"
usersIncDec: "使用者增減"
usersTotal: "使用者總數"
usersTotal: "使用者合計"
activeUsers: "活躍使用者"
notesIncDec: "貼文増減"
localNotesIncDec: "本地貼文増減"
@ -2491,6 +2507,7 @@ _drivecleaner:
orderByCreatedAtAsc: "按新增日期降序排列"
_webhookSettings:
createWebhook: "建立 Webhook"
modifyWebhook: "編輯 Webhook"
name: "名字"
secret: "密鑰"
events: "何時運行 Webhook"
@ -2503,6 +2520,25 @@ _webhookSettings:
renote: "當被轉發時"
reaction: "當獲得反應時"
mention: "當被提到時"
_systemEvents:
abuseReport: "當使用者檢舉時"
abuseReportResolved: "當處理了使用者的檢舉時"
deleteConfirm: "請問是否要刪除 Webhook"
_abuseReport:
_notificationRecipient:
createRecipient: "新增接收檢舉的通知對象"
modifyRecipient: "編輯接收檢舉的通知對象"
recipientType: "通知對象的種類"
_recipientType:
mail: "電子郵件"
webhook: "Webhook"
_captions:
mail: "寄送到擁有監察員權限的使用者電子郵件地址(僅在收到檢舉時)"
webhook: "向指定的 SystemWebhook 發送通知(在收到檢舉和解決檢舉時發送)"
keywords: "關鍵字"
notifiedUser: "被通知的使用者"
notifiedWebhook: "使用的 Webhook"
deleteConfirm: "確定要刪除通知對象嗎?"
_moderationLogTypes:
createRole: "新增角色"
deleteRole: "刪除角色 "
@ -2540,6 +2576,12 @@ _moderationLogTypes:
deleteAvatarDecoration: "刪除頭像裝飾"
unsetUserAvatar: "移除使用者的大頭貼"
unsetUserBanner: "移除使用者的橫幅圖像"
createSystemWebhook: "建立 SystemWebhook"
updateSystemWebhook: "更新 SystemWebhook"
deleteSystemWebhook: "刪除 SystemWebhook"
createAbuseReportNotificationRecipient: "建立接收檢舉的通知對象"
updateAbuseReportNotificationRecipient: "更新接收檢舉的通知對象"
deleteAbuseReportNotificationRecipient: "刪除接收檢舉的通知對象"
_fileViewer:
title: "檔案詳細資訊"
type: "檔案類型 "
@ -2671,3 +2713,7 @@ _mediaControls:
pip: "畫中畫"
playbackRate: "播放速度"
loop: "循環播放"
_contextMenu:
title: "內容功能表"
app: "應用程式"
native: "瀏覽器的使用者介面"

View File

@ -1,13 +1,13 @@
{
"name": "cherrypick",
"version": "4.10.0-beta.1",
"basedMisskeyVersion": "2024.7.0-beta.2",
"basedMisskeyVersion": "2024.7.0-rc.8",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/kokonect-link/cherrypick.git"
},
"packageManager": "pnpm@9.5.0",
"packageManager": "pnpm@9.6.0",
"workspaces": [
"packages/frontend",
"packages/backend",
@ -55,24 +55,24 @@
"cssnano": "6.1.2",
"execa": "8.0.1",
"fast-glob": "3.3.2",
"ignore-walk": "6.0.4",
"ignore-walk": "6.0.5",
"js-yaml": "4.1.0",
"postcss": "8.4.38",
"postcss": "8.4.40",
"tar": "6.2.1",
"terser": "5.31.1",
"typescript": "5.5.3",
"esbuild": "0.22.0",
"glob": "10.3.12"
"terser": "5.31.3",
"typescript": "5.5.4",
"esbuild": "0.23.0",
"glob": "11.0.0"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "2.0.2",
"@types/node": "20.14.9",
"@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"@types/node": "20.14.12",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"cross-env": "7.0.3",
"cypress": "13.13.0",
"eslint": "9.6.0",
"globals": "15.7.0",
"cypress": "13.13.1",
"eslint": "9.8.0",
"globals": "15.8.0",
"ncp": "2.0.0",
"start-server-and-test": "2.0.4"
},

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class MediaSilenceForHosts1716197366117 {
name = 'MediaSilenceForHosts1716197366117'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "mediaSilencedHosts" character varying(1024) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "mediaSilencedHosts"`);
}
}

View File

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class FixDriveUrl1721666053703 {
name = 'FixDriveUrl1721666053703'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "url" TYPE character varying(1024), ALTER COLUMN "url" SET NOT NULL`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`);
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "uri" TYPE character varying(1024)`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`);
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "src" TYPE character varying(1024)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "src" TYPE character varying(512)`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."uri" IS 'The URI of the DriveFile. it will be null when the DriveFile is local.'`);
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "uri" TYPE character varying(512)`);
await queryRunner.query(`COMMENT ON COLUMN "drive_file"."url" IS 'The URL of the DriveFile.'`);
await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "url" TYPE character varying(512), ALTER COLUMN "url" SET NOT NULL`);
}
}

View File

@ -66,11 +66,11 @@
"utf-8-validate": "6.0.3"
},
"dependencies": {
"@aws-sdk/client-s3": "3.600.0",
"@aws-sdk/lib-storage": "3.600.0",
"@bull-board/api": "5.20.5",
"@bull-board/fastify": "5.20.5",
"@bull-board/ui": "5.20.5",
"@aws-sdk/client-s3": "3.620.0",
"@aws-sdk/lib-storage": "3.620.0",
"@bull-board/api": "5.21.1",
"@bull-board/fastify": "5.21.1",
"@bull-board/ui": "5.21.1",
"@discordapp/twemoji": "15.0.3",
"@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.3.1",
@ -89,9 +89,9 @@
"@nestjs/core": "10.3.10",
"@nestjs/testing": "10.3.10",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "8.13.0",
"@sentry/profiling-node": "8.13.0",
"@simplewebauthn/server": "10.0.0",
"@sentry/node": "8.20.0",
"@sentry/profiling-node": "8.20.0",
"@simplewebauthn/server": "10.0.1",
"@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.3.12",
@ -99,13 +99,13 @@
"@twemoji/parser": "15.1.1",
"@vitalets/google-translate-api": "9.2.0",
"accepts": "1.3.8",
"ajv": "8.16.0",
"ajv": "8.17.1",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "5.8.3",
"bullmq": "5.10.4",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.3.0",
@ -121,10 +121,10 @@
"fastify": "4.28.1",
"fastify-raw-body": "4.3.0",
"feed": "4.2.2",
"file-type": "19.0.0",
"file-type": "19.3.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0",
"got": "14.4.1",
"got": "14.4.2",
"happy-dom": "10.0.3",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
@ -134,7 +134,7 @@
"ipaddr.js": "2.2.0",
"is-svg": "5.0.1",
"js-yaml": "4.1.0",
"jsdom": "24.1.0",
"jsdom": "24.1.1",
"json5": "2.2.3",
"jsonld": "8.3.2",
"jsrsasign": "11.1.0",
@ -182,11 +182,11 @@
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.20",
"typescript": "5.5.3",
"typescript": "5.5.4",
"ulid": "2.3.0",
"vary": "1.1.2",
"web-push": "3.6.7",
"ws": "8.17.1",
"ws": "8.18.0",
"xev": "3.0.2"
},
"devDependencies": {
@ -206,11 +206,11 @@
"@types/jest": "29.5.12",
"@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.14",
"@types/jsonld": "1.5.15",
"@types/jsrsasign": "10.5.14",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "20.14.9",
"@types/node": "20.14.12",
"@types/nodemailer": "6.4.15",
"@types/oauth": "0.9.5",
"@types/oauth2orize": "1.11.5",
@ -230,18 +230,18 @@
"@types/tmp": "0.2.6",
"@types/vary": "1.1.3",
"@types/web-push": "3.6.3",
"@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"@types/ws": "8.5.11",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"aws-sdk-client-mock": "4.0.1",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.29.1",
"execa": "9.2.0",
"execa": "9.3.0",
"fkill": "9.0.0",
"jest": "29.7.0",
"jest-mock": "29.7.0",
"nodemon": "3.1.4",
"pid-port": "1.0.0",
"simple-oauth2": "5.0.1"
"simple-oauth2": "5.1.0"
}
}

View File

@ -3,7 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
// dummy
export const MAX_NOTE_TEXT_LENGTH = 3000;
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min

View File

@ -44,7 +44,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
/**
* Redisイベントを用いて{@link abuseReports}.
* {@link RoleService.getModeratorIds}.
* {@link getModeratorIds}.
*
* @see RoleService.getModeratorIds
* @see GlobalEventService.publishAdminStream

View File

@ -43,6 +43,7 @@ import { RoleService } from '@/core/RoleService.js';
import { correctFilename } from '@/misc/correct-filename.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { UtilityService } from '@/core/UtilityService.js';
type AddFileArgs = {
/** User who wish to add file */
@ -127,6 +128,7 @@ export class DriveService {
private driveChart: DriveChart,
private perUserDriveChart: PerUserDriveChart,
private instanceChart: InstanceChart,
private utilityService: UtilityService,
) {
const logger = new Logger('drive', 'blue');
this.registerLogger = logger.createSubLogger('register', 'yellow');
@ -600,6 +602,7 @@ export class DriveService {
sensitive ?? false
: false;
if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true;
if (info.sensitive && profile!.autoSensitive) file.isSensitive = true;
if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true;
if (userRoleNSFW) file.isSensitive = true;

View File

@ -370,6 +370,9 @@ export class NoteCreateService implements OnApplicationShutdown {
mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens);
}
// if the host is media-silenced, custom emojis are not allowed
if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = [];
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {

View File

@ -105,6 +105,8 @@ export class ReactionService {
@bindThis
public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) {
const meta = await this.metaService.fetch();
// Check blocking
if (note.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
@ -148,6 +150,11 @@ export class ReactionService {
if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) {
reaction = FALLBACK;
}
// for media silenced host, custom emoji reactions are not allowed
if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) {
reaction = FALLBACK;
}
} else {
// リアクションとして使う権限がない
reaction = FALLBACK;
@ -220,8 +227,6 @@ export class ReactionService {
}
}
const meta = await this.metaService.fetch();
if (meta.enableChartsForRemoteUser || (user.host == null)) {
this.perUserReactionsChart.update(user, note);
}

View File

@ -161,7 +161,6 @@ export class SearchService {
userId?: MiNote['userId'] | null;
channelId?: MiNote['channelId'] | null;
host?: string | null;
origin?: string | null;
}, pagination: {
untilId?: MiNote['id'];
sinceId?: MiNote['id'];
@ -176,11 +175,6 @@ export class SearchService {
if (pagination.sinceId) filter.qs.push({ op: '>', k: 'createdAt', v: this.idService.parse(pagination.sinceId).date.getTime() });
if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId });
if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId });
if (opts.origin === 'local') {
filter.qs.push({ op: 'is null', k: 'userHost' });
} else if (opts.origin === 'remote') {
filter.qs.push({ op: 'is not null', k: 'userHost' });
}
if (opts.host) {
if (opts.host === '.') {
filter.qs.push({ op: 'is null', k: 'userHost' });
@ -214,12 +208,6 @@ export class SearchService {
} else {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), pagination.sinceId, pagination.untilId);
if (opts.origin === 'local') {
query.andWhere('note.userHost IS NULL');
} else if (opts.origin === 'remote') {
query.andWhere('note.userHost IS NOT NULL');
}
if (opts.userId) {
query.andWhere('note.userId = :userId', { userId: opts.userId });
} else if (opts.channelId) {

View File

@ -21,6 +21,7 @@ import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
import { MetaService } from '@/core/MetaService.js';
import { UserService } from '@/core/UserService.js';
@Injectable()
export class SignupService {
@ -35,6 +36,7 @@ export class SignupService {
private usedUsernamesRepository: UsedUsernamesRepository,
private utilityService: UtilityService,
private userService: UserService,
private userEntityService: UserEntityService,
private idService: IdService,
private metaService: MetaService,
@ -148,7 +150,8 @@ export class SignupService {
}));
});
this.usersChart.update(account, true);
this.usersChart.update(account, true).then();
this.userService.notifySystemWebhook(account, 'userCreated').then();
return { account, secret };
}

View File

@ -8,15 +8,18 @@ import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@Injectable()
export class UserService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.followingsRepository)
private followingsRepository: FollowingsRepository,
private systemWebhookService: SystemWebhookService,
private userEntityService: UserEntityService,
) {
}
@ -50,4 +53,23 @@ export class UserService {
});
}
}
/**
* SystemWebhookを用いてユーザに関する操作内容を管理者各位に通知する.
* JobQueueへのエンキューのみを行うため.
*
* @see SystemWebhookService.enqueueSystemWebhook
*/
@bindThis
public async notifySystemWebhook(user: MiUser, type: 'userCreated') {
const packedUser = await this.userEntityService.pack(user, null, { schema: 'UserLite' });
const recipientWebhookIds = await this.systemWebhookService.fetchSystemWebhooks({ isActive: true, on: [type] });
for (const webhookId of recipientWebhookIds) {
await this.systemWebhookService.enqueueSystemWebhook(
webhookId,
type,
packedUser,
);
}
}
}

View File

@ -42,6 +42,12 @@ export class UtilityService {
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}
@bindThis
public isMediaSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean {
if (!silencedHosts || host == null) return false;
return silencedHosts.some(x => host.toLowerCase() === x);
}
@bindThis
public concatNoteContentsForKeyWordCheck(content: {
cw?: string | null;

View File

@ -50,6 +50,7 @@ export class InstanceEntityService {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host),
isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host),
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,

View File

@ -129,6 +129,7 @@ export class MetaEntityService {
mediaProxy: this.config.mediaProxy,
enableUrlPreview: instance.urlPreviewEnabled,
noteSearchableScope: (this.config.meilisearch == null || this.config.meilisearch.scope !== 'local') ? 'global' : 'local',
};
return packed;

View File

@ -82,7 +82,7 @@ export class MiDriveFile {
public storedInternal: boolean;
@Column('varchar', {
length: 512,
length: 1024,
comment: 'The URL of the DriveFile.',
})
public url: string;
@ -124,13 +124,13 @@ export class MiDriveFile {
@Index()
@Column('varchar', {
length: 512, nullable: true,
length: 1024, nullable: true,
comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.',
})
public uri: string | null;
@Column('varchar', {
length: 512, nullable: true,
length: 1024, nullable: true,
})
public src: string | null;

View File

@ -86,6 +86,11 @@ export class MiMeta {
})
public silencedHosts: string[];
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public mediaSilencedHosts: string[];
@Column('varchar', {
length: 1024,
nullable: true,

View File

@ -12,6 +12,8 @@ export const systemWebhookEventTypes = [
'abuseReport',
// 通報を処理したとき
'abuseReportResolved',
// ユーザが作成された時
'userCreated',
] as const;
export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];

View File

@ -88,6 +88,10 @@ export const packedFederationInstanceSchema = {
type: 'boolean',
optional: false, nullable: false,
},
isMediaSilenced: {
type: 'boolean',
optional: false, nullable: false,
},
iconUrl: {
type: 'string',
optional: false, nullable: true,

View File

@ -251,6 +251,12 @@ export const packedMetaLiteSchema = {
optional: false, nullable: false,
ref: 'RolePolicies',
},
noteSearchableScope: {
type: 'string',
enum: ['local', 'global'],
optional: false, nullable: false,
default: 'local',
},
},
} as const;

View File

@ -230,6 +230,7 @@ export const packedNoteSchema = {
reactionAcceptance: {
type: 'string',
optional: false, nullable: true,
enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null],
},
reactionEmojis: {
type: 'object',

View File

@ -69,6 +69,7 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id', nullable: true },
status: { type: 'string', enum: ['all', 'active', 'archived'], default: 'active' },
},
required: [],
} as const;
@ -87,7 +88,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
query.andWhere('announcement.isActive = true');
if (ps.status === 'archived') {
query.andWhere('announcement.isActive = false');
} else if (ps.status === 'active') {
query.andWhere('announcement.isActive = true');
}
if (ps.userId) {
query.andWhere('announcement.userId = :userId', { userId: ps.userId });
} else {

View File

@ -132,6 +132,16 @@ export const meta = {
nullable: false,
},
},
mediaSilencedHosts: {
type: 'array',
optional: false,
nullable: false,
items: {
type: 'string',
optional: false,
nullable: false,
},
},
pinnedUsers: {
type: 'array',
optional: false, nullable: false,
@ -633,6 +643,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts,
silencedHosts: instance.silencedHosts,
mediaSilencedHosts: instance.mediaSilencedHosts,
sensitiveWords: instance.sensitiveWords,
prohibitedWords: instance.prohibitedWords,
preservedUsernames: instance.preservedUsernames,

View File

@ -171,6 +171,13 @@ export const paramDef = {
type: 'string',
},
},
mediaSilencedHosts: {
type: 'array',
nullable: true,
items: {
type: 'string',
},
},
summalyProxy: {
type: 'string', nullable: true,
description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
@ -230,6 +237,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
});
}
if (Array.isArray(ps.mediaSilencedHosts)) {
let lastValue = '';
set.mediaSilencedHosts = ps.mediaSilencedHosts.sort().filter((h) => {
const lv = lastValue;
lastValue = h;
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
});
}
if (ps.themeColor !== undefined) {
set.themeColor = ps.themeColor;
}

View File

@ -145,6 +145,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
];
}
const [
followings,
] = await Promise.all([
this.cacheService.userFollowingsCache.fetch(me.id),
]);
const redisTimeline = await this.fanoutTimelineEndpointService.timeline({
untilId,
sinceId,
@ -156,6 +162,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes,
withCats: ps.withCats,
noteFilter: note => {
if (note.reply && note.reply.visibility === 'followers') {
if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false;
}
return true;
},
dbFallback: async (untilId, sinceId, limit) => await this.getFromDb({
untilId,
sinceId,

View File

@ -117,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
withCats: ps.withCats,
noteFilter: note => {
if (note.reply && note.reply.visibility === 'followers') {
if (!Object.hasOwn(followings, note.reply.userId)) return false;
if (!Object.hasOwn(followings, note.reply.userId) && note.reply.userId !== me.id) return false;
}
return true;

View File

@ -57,88 +57,66 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
ps.query = ps.query.trim();
const isUsername = ps.query.startsWith('@');
const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1;
let users: MiUser[] = [];
if (isUsername) {
const usernameQuery = this.usersRepository.createQueryBuilder('user')
.where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' })
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
const nameQuery = this.usersRepository.createQueryBuilder('user')
.where(new Brackets(qb => {
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
usernameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
usernameQuery.andWhere('user.host IS NOT NULL');
}
users = await usernameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
} else {
const nameQuery = this.usersRepository.createQueryBuilder('user')
.where(new Brackets(qb => {
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
// Also search username if it qualifies as username
if (this.userEntityService.validateLocalUsername(ps.query)) {
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
}
}))
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
if (ps.origin === 'local') {
nameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
nameQuery.andWhere('user.host IS NOT NULL');
}
users = await nameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
if (users.length < ps.limit) {
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
.select('prof.userId')
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
profQuery.andWhere('prof.userHost IS NULL');
} else if (ps.origin === 'remote') {
profQuery.andWhere('prof.userHost IS NOT NULL');
if (isUsername) {
qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' });
} else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
}
}))
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
const query = this.usersRepository.createQueryBuilder('user')
.where(`user.id IN (${ profQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE')
.setParameters(profQuery.getParameters());
if (ps.origin === 'local') {
nameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
nameQuery.andWhere('user.host IS NOT NULL');
}
users = users.concat(await query
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany(),
);
users = await nameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
if (users.length < ps.limit) {
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
.select('prof.userId')
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
profQuery.andWhere('prof.userHost IS NULL');
} else if (ps.origin === 'remote') {
profQuery.andWhere('prof.userHost IS NOT NULL');
}
const query = this.usersRepository.createQueryBuilder('user')
.where(`user.id IN (${ profQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE')
.setParameters(profQuery.getParameters());
users = users.concat(await query
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany(),
);
}
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });

View File

@ -60,7 +60,7 @@ class HomeTimelineChannel extends Channel {
const reply = note.reply;
if (this.following[note.userId]?.withReplies) {
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
} else {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
@ -73,7 +73,7 @@ class HomeTimelineChannel extends Channel {
if (note.renote.reply) {
const reply = note.renote.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
}
}

View File

@ -76,14 +76,22 @@ class HybridTimelineChannel extends Channel {
const reply = note.reply;
if ((this.following[note.userId]?.withReplies ?? false) || this.withReplies) {
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信は弾く
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId)) return;
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
} else {
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
}
}
if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
// 純粋なリノート(引用リノートでないリノート)の場合
if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
if (!this.withRenotes) return;
if (note.renote.reply) {
const reply = note.renote.reply;
// 自分のフォローしていないユーザーの visibility: followers な投稿への返信のリノートは弾く
if (reply.visibility === 'followers' && !Object.hasOwn(this.following, reply.userId) && reply.userId !== this.user!.id) return;
}
}
if (this.user && note.renoteId && !note.text) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {

View File

@ -34,6 +34,7 @@ describe('Streaming', () => {
let kyoko: misskey.entities.SignupResponse;
let chitose: misskey.entities.SignupResponse;
let kanako: misskey.entities.SignupResponse;
let erin: misskey.entities.SignupResponse;
// Remote users
let akari: misskey.entities.SignupResponse;
@ -53,6 +54,7 @@ describe('Streaming', () => {
kyoko = await signup({ username: 'kyoko' });
chitose = await signup({ username: 'chitose' });
kanako = await signup({ username: 'kanako' });
erin = await signup({ username: 'erin' }); // erin: A generic fifth participant
akari = await signup({ username: 'akari', host: 'example.com' });
chinatsu = await signup({ username: 'chinatsu', host: 'example.com' });
@ -71,6 +73,12 @@ describe('Streaming', () => {
// Follow: kyoko => chitose
await api('following/create', { userId: chitose.id }, kyoko);
// Follow: erin <=> ayano each other.
// erin => ayano: withReplies: true
await api('following/create', { userId: ayano.id, withReplies: true }, erin);
// ayano => erin: withReplies: false
await api('following/create', { userId: erin.id, withReplies: false }, ayano);
// Mute: chitose => kanako
await api('mute/create', { userId: kanako.id }, chitose);
@ -297,6 +305,28 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => {
const erinNote = await post(erin, { text: 'hi', visibility: 'followers' });
const fired = await waitFire(
erin, 'homeTimeline', // erin:home
() => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano), // ayano reply to erin's followers post
msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano
);
assert.strictEqual(fired, true);
});
test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => {
const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' });
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin), // erin reply to ayano's followers post
msg => msg.type === 'note' && msg.body.userId === erin.id, // wait erin
);
assert.strictEqual(fired, true);
});
}); // Home
describe('Local Timeline', () => {
@ -475,6 +505,38 @@ describe('Streaming', () => {
assert.strictEqual(fired, false);
});
test('withReplies: true のとき自分のfollowers投稿に対するリプライが流れる', async () => {
const erinNote = await post(erin, { text: 'hi', visibility: 'followers' });
const fired = await waitFire(
erin, 'homeTimeline', // erin:home
() => api('notes/create', { text: 'hello', replyId: erinNote.id }, ayano), // ayano reply to erin's followers post
msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano
);
assert.strictEqual(fired, true);
});
test('withReplies: false でも自分の投稿に対するリプライが流れる', async () => {
const ayanoNote = await post(ayano, { text: 'hi', visibility: 'followers' });
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'hello', replyId: ayanoNote.id }, erin), // erin reply to ayano's followers post
msg => msg.type === 'note' && msg.body.userId === erin.id, // wait erin
);
assert.strictEqual(fired, true);
});
test('withReplies: true のフォローしていない人のfollowersートに対するリプライが流れない', async () => {
const fired = await waitFire(
erin, 'homeTimeline', // erin:home
() => api('notes/create', { text: 'hello', replyId: chitose.id }, ayano), // ayano reply to chitose's post
msg => msg.type === 'note' && msg.body.userId === ayano.id, // wait ayano
);
assert.strictEqual(fired, false);
});
});
describe('Global Timeline', () => {

View File

@ -5,65 +5,24 @@
import { entities } from 'cherrypick-js';
import { beforeEach, describe, test } from '@jest/globals';
import Fastify from 'fastify';
import { api, randomString, role, signup, startJobQueue, UserToken } from '../../utils.js';
import {
api,
captureWebhook,
randomString,
role,
signup,
startJobQueue,
UserToken,
WEBHOOK_HOST,
} from '../../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
const WEBHOOK_HOST = 'http://localhost:15080';
const WEBHOOK_PORT = 15080;
process.env.NODE_ENV = 'test';
describe('[シナリオ] ユーザ通報', () => {
let queue: INestApplicationContext;
let admin: entities.SignupResponse;
let alice: entities.SignupResponse;
let bob: entities.SignupResponse;
type SystemWebhookPayload = {
server: string;
hookId: string;
eventId: string;
createdAt: string;
type: string;
body: any;
}
// -------------------------------------------------------------------------------------------
async function captureWebhook<T = SystemWebhookPayload>(postAction: () => Promise<void>): Promise<T> {
const fastify = Fastify();
let timeoutHandle: NodeJS.Timeout | null = null;
const result = await new Promise<string>(async (resolve, reject) => {
fastify.all('/', async (req, res) => {
timeoutHandle && clearTimeout(timeoutHandle);
const body = JSON.stringify(req.body);
res.status(200).send('ok');
await fastify.close();
resolve(body);
});
await fastify.listen({ port: WEBHOOK_PORT });
timeoutHandle = setTimeout(async () => {
await fastify.close();
reject(new Error('timeout'));
}, 3000);
try {
await postAction();
} catch (e) {
await fastify.close();
reject(e);
}
});
await fastify.close();
return JSON.parse(result) as T;
}
async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> {
const res = await api(
'admin/system-webhook/create',

View File

@ -0,0 +1,130 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { setTimeout } from 'node:timers/promises';
import { entities } from 'cherrypick-js';
import { beforeEach, describe, test } from '@jest/globals';
import {
api,
captureWebhook,
randomString,
role,
signup,
startJobQueue,
UserToken,
WEBHOOK_HOST,
} from '../../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
describe('[シナリオ] ユーザ作成', () => {
let queue: INestApplicationContext;
let admin: entities.SignupResponse;
async function createSystemWebhook(args?: Partial<entities.AdminSystemWebhookCreateRequest>, credential?: UserToken): Promise<entities.AdminSystemWebhookCreateResponse> {
const res = await api(
'admin/system-webhook/create',
{
isActive: true,
name: randomString(),
on: ['userCreated'],
url: WEBHOOK_HOST,
secret: randomString(),
...args,
},
credential ?? admin,
);
return res.body;
}
// -------------------------------------------------------------------------------------------
beforeAll(async () => {
queue = await startJobQueue();
admin = await signup({ username: 'admin' });
await role(admin, { isAdministrator: true });
}, 1000 * 60 * 2);
afterAll(async () => {
await queue.close();
});
// -------------------------------------------------------------------------------------------
describe('SystemWebhook', () => {
beforeEach(async () => {
const webhooks = await api('admin/system-webhook/list', {}, admin);
for (const webhook of webhooks.body) {
await api('admin/system-webhook/delete', { id: webhook.id }, admin);
}
});
test('ユーザが作成された -> userCreatedが送出される', async () => {
const webhook = await createSystemWebhook({
on: ['userCreated'],
isActive: true,
});
let alice: any = null;
const webhookBody = await captureWebhook(async () => {
alice = await signup({ username: 'alice' });
});
// webhookの送出後にいろいろやってるのでちょっと待つ必要がある
await setTimeout(2000);
console.log(alice);
console.log(JSON.stringify(webhookBody, null, 2));
expect(webhookBody.hookId).toBe(webhook.id);
expect(webhookBody.type).toBe('userCreated');
const body = webhookBody.body as entities.UserLite;
expect(alice.id).toBe(body.id);
expect(alice.name).toBe(body.name);
expect(alice.username).toBe(body.username);
expect(alice.host).toBe(body.host);
expect(alice.avatarUrl).toBe(body.avatarUrl);
expect(alice.avatarBlurhash).toBe(body.avatarBlurhash);
expect(alice.avatarDecorations).toEqual(body.avatarDecorations);
expect(alice.isBot).toBe(body.isBot);
expect(alice.isCat).toBe(body.isCat);
expect(alice.instance).toEqual(body.instance);
expect(alice.emojis).toEqual(body.emojis);
expect(alice.onlineStatus).toBe(body.onlineStatus);
expect(alice.badgeRoles).toEqual(body.badgeRoles);
});
test('ユーザ作成 -> userCreatedが未許可の場合は送出されない', async () => {
await createSystemWebhook({
on: [],
isActive: true,
});
let alice: any = null;
const webhookBody = await captureWebhook(async () => {
alice = await signup({ username: 'alice' });
}).catch(e => e.message);
expect(webhookBody).toBe('timeout');
expect(alice.id).not.toBeNull();
});
test('ユーザ作成 -> Webhookが無効の場合は送出されない', async () => {
await createSystemWebhook({
on: ['userCreated'],
isActive: false,
});
let alice: any = null;
const webhookBody = await captureWebhook(async () => {
alice = await signup({ username: 'alice' });
}).catch(e => e.message);
expect(webhookBody).toBe('timeout');
expect(alice.id).not.toBeNull();
});
});
});

View File

@ -127,6 +127,7 @@ describe('Timelines', () => {
test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: carol.id }, bob);
await api('following/create', { userId: bob.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice);
await setTimeout(1000);
@ -161,6 +162,24 @@ describe('Timelines', () => {
assert.strictEqual(res.body.find(note => note.id === carolNote.id)?.text, 'hi');
});
test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice);
await api('following/create', { userId: alice.id }, bob);
await api('following/update', { userId: bob.id, withReplies: true }, alice);
await setTimeout(1000);
const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
await waitForPushToTl();
const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
});
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの投稿への visibility: specified な返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
@ -768,6 +787,62 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
});
test.concurrent('withReplies: true でフォローしているユーザーの他人の visibility: followers な投稿への返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: carol.id }, bob);
await api('following/create', { userId: bob.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await waitForPushToTl();
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
});
test.concurrent('withReplies: true でフォローしているユーザーの行った別のフォローしているユーザーの visibility: followers な投稿への返信が含まれる', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice);
await api('following/create', { userId: carol.id }, alice);
await api('following/create', { userId: carol.id }, bob);
await api('following/update', { userId: bob.id, withReplies: true }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
await waitForPushToTl();
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === carolNote.id)?.text, 'hi');
});
test.concurrent('withReplies: true でフォローしているユーザーの自分の visibility: followers な投稿への返信が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice);
await api('following/create', { userId: alice.id }, bob);
await api('following/update', { userId: bob.id, withReplies: true }, alice);
await setTimeout(1000);
const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
await waitForPushToTl();
const res = await api('notes/hybrid-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === aliceNote.id), true);
});
test.concurrent('他人の他人への返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);

View File

@ -12,13 +12,14 @@ import WebSocket, { ClientOptions } from 'ws';
import fetch, { File, RequestInit, type Headers } from 'node-fetch';
import { DataSource } from 'typeorm';
import { JSDOM } from 'jsdom';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { type Response } from 'node-fetch';
import Fastify from 'fastify';
import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js';
import type * as misskey from 'cherrypick-js';
import { type Response } from 'node-fetch';
import { ApiError } from "@/server/api/error.js";
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
import { ApiError } from '@/server/api/error.js';
export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js';
@ -27,11 +28,23 @@ export interface UserToken {
bearer?: boolean;
}
export type SystemWebhookPayload = {
server: string;
hookId: string;
eventId: string;
createdAt: string;
type: string;
body: any;
}
const config = loadConfig();
export const port = config.port;
export const origin = config.url;
export const host = new URL(config.url).host;
export const WEBHOOK_HOST = 'http://localhost:15080';
export const WEBHOOK_PORT = 15080;
export const cookie = (me: UserToken): string => {
return `token=${me.token};`;
};
@ -645,3 +658,37 @@ export async function sendEnvResetRequest() {
export function castAsError(obj: Record<string, unknown>): { error: ApiError } {
return obj as { error: ApiError };
}
export async function captureWebhook<T = SystemWebhookPayload>(postAction: () => Promise<void>, port = WEBHOOK_PORT): Promise<T> {
const fastify = Fastify();
let timeoutHandle: NodeJS.Timeout | null = null;
const result = await new Promise<string>(async (resolve, reject) => {
fastify.all('/', async (req, res) => {
timeoutHandle && clearTimeout(timeoutHandle);
const body = JSON.stringify(req.body);
res.status(200).send('ok');
await fastify.close();
resolve(body);
});
await fastify.listen({ port });
timeoutHandle = setTimeout(async () => {
await fastify.close();
reject(new Error('timeout'));
}, 3000);
try {
await postAction();
} catch (e) {
await fastify.close();
reject(e);
}
});
await fastify.close();
return JSON.parse(result) as T;
}

View File

@ -2622,7 +2622,7 @@ type ModerationLog = {
});
// @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"];
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient"];
// @public (undocumented)
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];

View File

@ -2,7 +2,7 @@
"type": "module",
"name": "cherrypick-js",
"version": "4.10.0-beta.1",
"basedMisskeyVersion": "2024.7.0-beta.2",
"basedMisskeyVersion": "2024.7.0-rc.8",
"description": "CherryPick SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@ -36,23 +36,23 @@
"directory": "packages/cherrypick-js"
},
"devDependencies": {
"@microsoft/api-extractor": "7.47.0",
"@microsoft/api-extractor": "7.47.4",
"@swc/jest": "0.2.36",
"@types/jest": "29.5.12",
"@types/node": "20.14.9",
"@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"@types/node": "20.14.12",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"jest": "29.7.0",
"jest-fetch-mock": "3.0.3",
"jest-websocket-mock": "2.5.0",
"mock-socket": "9.3.1",
"ncp": "2.0.0",
"nodemon": "3.1.4",
"execa": "9.2.0",
"execa": "9.3.0",
"tsd": "0.31.1",
"typescript": "5.5.3",
"esbuild": "0.22.0",
"glob": "10.4.2"
"typescript": "5.5.4",
"esbuild": "0.23.0",
"glob": "11.0.0"
},
"files": [
"built"

View File

@ -4428,7 +4428,8 @@ export type components = {
userId: string | null;
}) | null;
localOnly?: boolean;
reactionAcceptance: string | null;
/** @enum {string|null} */
reactionAcceptance: 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null;
reactionEmojis: {
[key: string]: string;
};
@ -4948,6 +4949,7 @@ export type components = {
maintainerName: string | null;
maintainerEmail: string | null;
isSilenced: boolean;
isMediaSilenced: boolean;
/** Format: url */
iconUrl: string | null;
/** Format: url */
@ -5290,6 +5292,11 @@ export type components = {
serverRules: string[];
themeColor: string | null;
policies: components['schemas']['RolePolicies'];
/**
* @default local
* @enum {string}
*/
noteSearchableScope: 'local' | 'global';
};
MetaDetailedOnly: {
features?: {
@ -5321,7 +5328,7 @@ export type components = {
latestSentAt: string | null;
latestStatus: number | null;
name: string;
on: ('abuseReport' | 'abuseReportResolved')[];
on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
url: string;
secret: string;
};
@ -5391,6 +5398,7 @@ export type operations = {
translatorAvailable: boolean;
translatorType: string | null;
silencedHosts?: string[];
mediaSilencedHosts: string[];
pinnedUsers: string[];
hiddenTags: string[];
blockedHosts: string[];
@ -6707,6 +6715,11 @@ export type operations = {
untilId?: string;
/** Format: misskey:id */
userId?: string | null;
/**
* @default active
* @enum {string}
*/
status?: 'all' | 'active' | 'archived';
};
};
};
@ -10168,6 +10181,7 @@ export type operations = {
perUserListTimelineCacheMax?: number;
notesPerOneAd?: number;
silencedHosts?: string[] | null;
mediaSilencedHosts?: string[] | null;
/** @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. */
summalyProxy?: string | null;
urlPreviewEnabled?: boolean;
@ -10854,7 +10868,7 @@ export type operations = {
'application/json': {
isActive: boolean;
name: string;
on: ('abuseReport' | 'abuseReportResolved')[];
on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
url: string;
secret: string;
};
@ -10964,7 +10978,7 @@ export type operations = {
content: {
'application/json': {
isActive?: boolean;
on?: ('abuseReport' | 'abuseReportResolved')[];
on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
};
};
};
@ -11077,7 +11091,7 @@ export type operations = {
id: string;
isActive: boolean;
name: string;
on: ('abuseReport' | 'abuseReportResolved')[];
on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
url: string;
secret: string;
};

View File

@ -143,6 +143,12 @@ export const moderationLogTypes = [
'deleteAvatarDecoration',
'unsetUserAvatar',
'unsetUserBanner',
'createSystemWebhook',
'updateSystemWebhook',
'deleteSystemWebhook',
'createAbuseReportNotificationRecipient',
'updateAbuseReportNotificationRecipient',
'deleteAbuseReportNotificationRecipient',
] as const;
// See: packages/backend/src/core/ReversiService.ts@L410

View File

@ -47,18 +47,7 @@ export function clip(id = 'someclipid', name = 'Some Clip'): entities.Clip {
createdAt: '2016-12-28T22:49:51.000Z',
lastClippedAt: null,
userId: 'someuserid',
user: {
id: 'someuserid',
name: 'Misskey User',
username: 'miskist',
host: 'misskey-hub.net',
avatarUrl: 'https://github.com/kokonect-link/cherrypick/blob/master/packages/frontend/assets/about-icon.png?raw=true',
avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay',
avatarDecorations: [],
emojis: {},
badgeRoles: [],
onlineStatus: 'unknown',
},
user: userLite(),
notesCount: undefined,
name,
description: 'Some clip description',
@ -125,6 +114,15 @@ export function file(isSensitive = false) {
};
}
export function folder(id = 'somefolderid', name = 'Some Folder', parentId: string | null = null): entities.DriveFolder {
return {
id,
createdAt: '2016-12-28T22:49:51.000Z',
name,
parentId,
};
}
export function federationInstance(): entities.FederationInstance {
return {
id: 'someinstanceid',
@ -154,7 +152,27 @@ export function federationInstance(): entities.FederationInstance {
};
}
export function userDetailed(id = 'someuserid', username = 'cherrypikist', host = 'misskey-hub.net', name = 'CherryPick User'): entities.UserDetailed {
export function note(id = 'somenoteid'): entities.Note {
return {
id,
createdAt: '2016-12-28T22:49:51.000Z',
deletedAt: null,
text: 'some note',
cw: null,
userId: 'someuserid',
user: userLite(),
visibility: 'public',
reactionAcceptance: 'nonSensitiveOnly',
reactionEmojis: {},
reactions: {},
myReaction: null,
reactionCount: 0,
renoteCount: 0,
repliesCount: 0,
};
}
export function userLite(id = 'someuserid', username = 'cherrypikist', host: entities.UserDetailed['host'] = 'misskey-hub.net', name: entities.UserDetailed['name'] = 'CherryPick User'): entities.UserLite {
return {
id,
username,
@ -165,6 +183,12 @@ export function userDetailed(id = 'someuserid', username = 'cherrypikist', host
avatarBlurhash: 'eQFRshof5NWBRi},juayfPju53WB?0ofs;s*a{ofjuay^SoMEJR%ay',
avatarDecorations: [],
emojis: {},
};
}
export function userDetailed(id = 'someuserid', username = 'cherrypikist', host: entities.UserDetailed['host'] = 'misskey-hub.net', name: entities.UserDetailed['name'] = 'CherryPick User'): entities.UserDetailed {
return {
...userLite(id, username, host, name),
bannerBlurhash: 'eQA^IW^-MH8w9tE8I=S^o{$*R4RikXtSxutRozjEnNR.RQadoyozog',
bannerUrl: 'https://github.com/kokonect-link/cherrypick/blob/master/packages/frontend/assets/fedi.jpg?raw=true',
birthday: '2014-06-20',
@ -215,7 +239,7 @@ export function userDetailed(id = 'someuserid', username = 'cherrypikist', host
movedTo: null,
alsoKnownAs: null,
notify: 'none',
memo: null
memo: null,
};
}

View File

@ -397,15 +397,14 @@ function toStories(component: string): Promise<string> {
const globs = await Promise.all([
glob('src/components/global/Mk*.vue'),
glob('src/components/global/RouterView.vue'),
glob('src/components/Mk[A-C]*.vue'),
glob('src/components/MkDigitalClock.vue'),
glob('src/components/MkEvent.vue'),
glob('src/components/Mk[A-E]*.vue'),
glob('src/components/MkGalleryPostPreview.vue'),
glob('src/components/MkSignupServerRules.vue'),
glob('src/components/MkUserSetupDialog.vue'),
glob('src/components/MkUserSetupDialog.*.vue'),
glob('src/components/MkInstanceCardMini.vue'),
glob('src/components/MkInviteCode.vue'),
glob('src/pages/search.vue'),
glob('src/pages/user/home.vue'),
glob('src/components/global/CP*.vue'),
]);

View File

@ -27,8 +27,8 @@
"@syuilo/aiscript": "0.19.0",
"@tabler/icons-webfont": "3.3.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.0.5",
"@vue/compiler-sfc": "3.4.31",
"@vitejs/plugin-vue": "5.1.0",
"@vue/compiler-sfc": "3.4.34",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
"astring": "1.8.6",
"autosize": "6.0.1",
@ -42,9 +42,9 @@
"chartjs-plugin-zoom": "2.0.1",
"cherrypick-js": "workspace:*",
"cherrypick-mfm-js": "0.24.0-cherrypick.4",
"chromatic": "11.5.4",
"compare-versions": "6.1.0",
"cropperjs": "2.0.0-beta.5",
"chromatic": "11.5.6",
"compare-versions": "6.1.1",
"cropperjs": "2.0.0-rc.1",
"date-fns": "2.30.0",
"escape-regexp": "0.0.1",
"estree-walker": "3.0.3",
@ -59,54 +59,54 @@
"photoswipe": "5.4.4",
"prismjs": "1.29.0",
"punycode": "2.3.1",
"rollup": "4.18.0",
"rollup": "4.19.1",
"sanitize-html": "2.13.0",
"sass": "1.77.6",
"shiki": "1.10.0",
"sass": "1.77.8",
"shiki": "1.12.0",
"strict-event-emitter-types": "2.0.0",
"temml": "0.10.20",
"textarea-caret": "3.1.0",
"three": "0.165.0",
"three": "0.167.0",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tinyld": "^1.3.4",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
"typescript": "5.5.3",
"typescript": "5.5.4",
"uuid": "10.0.0",
"v-code-diff": "1.12.0",
"vite": "5.3.2",
"vue": "3.4.31",
"vite": "5.3.5",
"vue": "3.4.34",
"vue-prism-editor": "2.0.0-alpha.2",
"vuedraggable": "next"
},
"devDependencies": {
"@misskey-dev/summaly": "5.1.0",
"@storybook/addon-actions": "8.1.11",
"@storybook/addon-essentials": "8.1.11",
"@storybook/addon-interactions": "8.1.11",
"@storybook/addon-links": "8.1.11",
"@storybook/addon-mdx-gfm": "8.1.11",
"@storybook/addon-storysource": "8.1.11",
"@storybook/blocks": "8.1.11",
"@storybook/components": "8.1.11",
"@storybook/core-events": "8.1.11",
"@storybook/manager-api": "8.1.11",
"@storybook/preview-api": "8.1.11",
"@storybook/react": "8.1.11",
"@storybook/react-vite": "8.1.11",
"@storybook/test": "8.1.11",
"@storybook/theming": "8.1.11",
"@storybook/types": "8.1.11",
"@storybook/vue3": "8.1.11",
"@storybook/addon-actions": "8.2.6",
"@storybook/addon-essentials": "8.2.6",
"@storybook/addon-interactions": "8.2.6",
"@storybook/addon-links": "8.2.6",
"@storybook/addon-mdx-gfm": "8.2.6",
"@storybook/addon-storysource": "8.2.6",
"@storybook/blocks": "8.2.6",
"@storybook/components": "8.2.6",
"@storybook/core-events": "8.2.6",
"@storybook/manager-api": "8.2.6",
"@storybook/preview-api": "8.2.6",
"@storybook/react": "8.2.6",
"@storybook/react-vite": "8.2.6",
"@storybook/test": "8.2.6",
"@storybook/theming": "8.2.6",
"@storybook/types": "8.2.6",
"@storybook/vue3": "8.2.6",
"@storybook/vue3-vite": "8.1.11",
"@testing-library/vue": "8.1.0",
"@types/autosize": "^4.0.1",
"@types/escape-regexp": "0.0.3",
"@types/estree": "1.0.5",
"@types/matter-js": "0.19.6",
"@types/matter-js": "0.19.7",
"@types/micromatch": "4.0.9",
"@types/node": "20.14.9",
"@types/node": "20.14.12",
"@types/prismjs": "^1.26.0",
"@types/punycode": "2.1.4",
"@types/sanitize-html": "2.11.0",
@ -114,36 +114,36 @@
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/uuid": "10.0.0",
"@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"@types/ws": "8.5.11",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"@vitest/coverage-v8": "1.6.0",
"@vue/runtime-core": "3.4.31",
"acorn": "8.12.0",
"@vue/runtime-core": "3.4.34",
"acorn": "8.12.1",
"cross-env": "7.0.3",
"cypress": "13.13.0",
"cypress": "13.13.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-storybook": "^0.6.13",
"eslint-plugin-vue": "9.26.0",
"eslint-plugin-vue": "9.27.0",
"fast-glob": "3.3.2",
"happy-dom": "10.0.3",
"intersection-observer": "0.12.2",
"micromatch": "4.0.7",
"msw": "2.3.1",
"msw-storybook-addon": "2.0.2",
"msw": "2.3.4",
"msw-storybook-addon": "2.0.3",
"nodemon": "3.1.4",
"prettier": "3.3.2",
"prettier": "3.3.3",
"react": "18.3.1",
"react-dom": "18.3.1",
"seedrandom": "3.0.5",
"start-server-and-test": "2.0.4",
"storybook": "8.1.11",
"storybook": "8.2.6",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "1.6.0",
"vitest-fetch-mock": "0.2.2",
"vue-component-type-helpers": "2.0.24",
"vue-component-type-helpers": "2.0.29",
"vue-eslint-parser": "9.4.3",
"vue-tsc": "2.0.24"
"vue-tsc": "2.0.29"
}
}

View File

@ -0,0 +1,62 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAntennaEditor from './MkAntennaEditor.vue';
export const Default = {
render(args) {
return {
components: {
MkAntennaEditor,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
events() {
return {
created: action('created'),
updated: action('updated'),
deleted: action('deleted'),
};
},
},
template: '<MkAntennaEditor v-bind="props" v-on="events" />',
};
},
args: {
},
parameters: {
layout: 'fullscreen',
msw: {
handlers: [
...commonHandlers,
http.post('/api/antennas/create', async ({ request }) => {
action('POST /api/antennas/create')(await request.json());
return HttpResponse.json({});
}),
http.post('/api/antennas/update', async ({ request }) => {
action('POST /api/antennas/update')(await request.json());
return HttpResponse.json({});
}),
http.post('/api/antennas/delete', async ({ request }) => {
action('POST /api/antennas/delete')(await request.json());
return HttpResponse.json();
}),
],
},
},
} satisfies StoryObj<typeof MkAntennaEditor>;

View File

@ -50,9 +50,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.actions">
<div class="_buttons">
<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="antenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
<MkButton v-if="initialAntenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
</div>
</MkSpacer>
</MkStickyContainer>
@ -69,29 +69,55 @@ import MkSwitch from '@/components/MkSwitch.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { deepMerge } from '@/scripts/merge.js';
import type { DeepPartial } from '@/scripts/merge.js';
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
id?: string;
createdAt?: string;
updatedAt?: string;
};
const props = defineProps<{
antenna: Misskey.entities.Antenna
antenna?: DeepPartial<PartialAllowedAntenna>;
}>();
const initialAntenna = deepMerge<PartialAllowedAntenna>(props.antenna ?? {}, {
name: '',
src: 'all',
userListId: null,
userGroupId: null,
users: [],
keywords: [],
excludeKeywords: [],
excludeBots: false,
withReplies: false,
caseSensitive: false,
localOnly: false,
withFile: false,
isActive: true,
hasUnreadNote: false,
notify: false,
});
const emit = defineEmits<{
(ev: 'created'): void,
(ev: 'updated'): void,
(ev: 'created', newAntenna: Misskey.entities.Antenna): void,
(ev: 'updated', editedAntenna: Misskey.entities.Antenna): void,
(ev: 'deleted'): void,
}>();
const name = ref<string>(props.antenna.name);
const src = ref<Misskey.entities.AntennasCreateRequest['src']>(props.antenna.src);
const userListId = ref<string | null>(props.antenna.userListId);
const userGroupId = ref<string | null>(props.antenna.userGroupId);
const users = ref<string>(props.antenna.users.join('\n'));
const keywords = ref<string>(props.antenna.keywords.map(x => x.join(' ')).join('\n'));
const excludeKeywords = ref<string>(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
const caseSensitive = ref<boolean>(props.antenna.caseSensitive);
const localOnly = ref<boolean>(props.antenna.localOnly);
const excludeBots = ref<boolean>(props.antenna.excludeBots);
const withReplies = ref<boolean>(props.antenna.withReplies);
const withFile = ref<boolean>(props.antenna.withFile);
const name = ref<string>(initialAntenna.name);
const src = ref<Misskey.entities.AntennasCreateRequest['src']>(initialAntenna.src);
const userListId = ref<string | null>(initialAntenna.userListId);
const userGroupId = ref<string | null>(initialAntenna.userGroupId);
const users = ref<string>(initialAntenna.users.join('\n'));
const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n'));
const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
const caseSensitive = ref<boolean>(initialAntenna.caseSensitive);
const localOnly = ref<boolean>(initialAntenna.localOnly);
const excludeBots = ref<boolean>(initialAntenna.excludeBots);
const withReplies = ref<boolean>(initialAntenna.withReplies);
const withFile = ref<boolean>(initialAntenna.withFile);
const userLists = ref<Misskey.entities.UserList[] | null>(null);
const userGroups = ref<Misskey.entities.UserGroup[] | null>(null);
@ -128,24 +154,26 @@ async function saveAntenna() {
excludeKeywords: excludeKeywords.value.trim().split('\n').map(x => x.trim().split(' ')),
};
if (props.antenna.id == null) {
await os.apiWithDialog('antennas/create', antennaData);
emit('created');
if (initialAntenna.id == null) {
const res = await os.apiWithDialog('antennas/create', antennaData);
emit('created', res);
} else {
await os.apiWithDialog('antennas/update', { ...antennaData, antennaId: props.antenna.id });
emit('updated');
const res = await os.apiWithDialog('antennas/update', { ...antennaData, antennaId: initialAntenna.id });
emit('updated', res);
}
}
async function deleteAntenna() {
if (initialAntenna.id == null) return;
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: props.antenna.name }),
text: i18n.tsx.removeAreYouSure({ x: initialAntenna.name }),
});
if (canceled) return;
await misskeyApi('antennas/delete', {
antennaId: props.antenna.id,
antennaId: initialAntenna.id,
});
os.success();

View File

@ -0,0 +1,63 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAntennaEditorDialog from './MkAntennaEditorDialog.vue';
export const Default = {
render(args) {
return {
components: {
MkAntennaEditorDialog,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
events() {
return {
created: action('created'),
updated: action('updated'),
deleted: action('deleted'),
closed: action('closed'),
};
},
},
template: '<MkAntennaEditorDialog v-bind="props" v-on="events" />',
};
},
args: {
},
parameters: {
layout: 'centered',
msw: {
handlers: [
...commonHandlers,
http.post('/api/antennas/create', async ({ request }) => {
action('POST /api/antennas/create')(await request.json());
return HttpResponse.json({});
}),
http.post('/api/antennas/update', async ({ request }) => {
action('POST /api/antennas/update')(await request.json());
return HttpResponse.json({});
}),
http.post('/api/antennas/delete', async ({ request }) => {
action('POST /api/antennas/delete')(await request.json());
return HttpResponse.json();
}),
],
},
},
} satisfies StoryObj<typeof MkAntennaEditorDialog>;

View File

@ -0,0 +1,63 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:withOkButton="false"
:width="500"
:height="550"
@close="close()"
@closed="emit('closed')"
>
<template #header>{{ antenna == null ? i18n.ts.createAntenna : i18n.ts.editAntenna }}</template>
<XAntennaEditor
:antenna="antenna"
@created="onAntennaCreated"
@updated="onAntennaUpdated"
@deleted="onAntennaDeleted"
/>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { shallowRef } from 'vue';
import * as Misskey from 'cherrypick-js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import XAntennaEditor from '@/components/MkAntennaEditor.vue';
import { i18n } from '@/i18n.js';
defineProps<{
antenna?: Misskey.entities.Antenna;
}>();
const emit = defineEmits<{
(ev: 'created', newAntenna: Misskey.entities.Antenna): void,
(ev: 'updated', editedAntenna: Misskey.entities.Antenna): void,
(ev: 'deleted'): void,
(ev: 'closed'): void,
}>();
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
function onAntennaCreated(newAntenna: Misskey.entities.Antenna) {
emit('created', newAntenna);
dialog.value?.close();
}
function onAntennaUpdated(editedAntenna: Misskey.entities.Antenna) {
emit('updated', editedAntenna);
dialog.value?.close();
}
function onAntennaDeleted() {
emit('deleted');
dialog.value?.close();
}
function close() {
dialog.value?.close();
}
</script>

View File

@ -31,6 +31,7 @@ export const Default = {
},
args: {
clip: clip(),
noUserInfo: false,
},
parameters: {
layout: 'fullscreen',

View File

@ -12,10 +12,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="clip.lastClippedAt">{{ i18n.ts.updatedAt }}: <MkTime :time="clip.lastClippedAt" mode="detail"/></div>
<div v-if="clip.notesCount != null">{{ i18n.ts.notesCount }}: {{ number(clip.notesCount) }} / {{ $i?.policies.noteEachClipsLimit }} ({{ i18n.tsx.remainingN({ n: remaining }) }})</div>
</div>
<div :class="$style.divider"></div>
<div>
<MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
</div>
<template v-if="!props.noUserInfo">
<div :class="$style.divider"></div>
<div>
<MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
</div>
</template>
</div>
</MkA>
</template>
@ -27,9 +29,12 @@ import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import number from '@/filters/number.js';
const props = defineProps<{
const props = withDefaults(defineProps<{
clip: Misskey.entities.Clip;
}>();
noUserInfo?: boolean;
}>(), {
noUserInfo: false,
});
const remaining = computed(() => {
return ($i?.policies && props.clip.notesCount != null) ? ($i.policies.noteEachClipsLimit - props.clip.notesCount) : i18n.ts.unknown;

View File

@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import MkDateSeparatedList from './MkDateSeparatedList.vue';
void MkDateSeparatedList;

View File

@ -0,0 +1,159 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { action } from '@storybook/addon-actions';
import { expect, userEvent, waitFor, within } from '@storybook/test';
import { StoryObj } from '@storybook/vue3';
import { i18n } from '@/i18n.js';
import MkDialog from './MkDialog.vue';
const Base = {
render(args) {
return {
components: {
MkDialog,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
events() {
return {
done: action('done'),
closed: action('closed'),
};
},
},
template: '<MkDialog v-bind="props" v-on="events" />',
};
},
args: {
text: 'Hello, world!',
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkDialog>;
export const Success = {
...Base,
args: {
...Base.args,
type: 'success',
},
} satisfies StoryObj<typeof MkDialog>;
export const Error = {
...Base,
args: {
...Base.args,
type: 'error',
},
} satisfies StoryObj<typeof MkDialog>;
export const Warning = {
...Base,
args: {
...Base.args,
type: 'warning',
},
} satisfies StoryObj<typeof MkDialog>;
export const Info = {
...Base,
args: {
...Base.args,
type: 'info',
},
} satisfies StoryObj<typeof MkDialog>;
export const Question = {
...Base,
args: {
...Base.args,
type: 'question',
},
} satisfies StoryObj<typeof MkDialog>;
export const Waiting = {
...Base,
args: {
...Base.args,
type: 'waiting',
},
} satisfies StoryObj<typeof MkDialog>;
export const DialogWithActions = {
...Question,
args: {
...Question.args,
text: i18n.ts.areYouSure,
actions: [
{
text: i18n.ts.yes,
primary: true,
callback() {
action('YES')();
},
},
{
text: i18n.ts.no,
callback() {
action('NO')();
},
},
],
},
} satisfies StoryObj<typeof MkDialog>;
export const DialogWithDangerActions = {
...Warning,
args: {
...Warning.args,
text: i18n.ts.resetAreYouSure,
actions: [
{
text: i18n.ts.yes,
danger: true,
primary: true,
callback() {
action('YES')();
},
},
{
text: i18n.ts.no,
callback() {
action('NO')();
},
},
],
},
} satisfies StoryObj<typeof MkDialog>;
export const DialogWithInput = {
...Question,
args: {
...Question.args,
title: 'Hello, world!',
text: undefined,
input: {
placeholder: i18n.ts.inputMessageHere,
type: 'text',
default: null,
minLength: 2,
maxLength: 3,
},
},
async play({ canvasElement }) {
const canvas = within(canvasElement);
await expect(canvasElement).toHaveTextContent(i18n.tsx._dialog.charactersBelow({ current: 0, min: 2 }));
const okButton = canvas.getByRole('button', { name: i18n.ts.ok });
await expect(okButton).toBeDisabled();
const input = canvas.getByRole<HTMLInputElement>('combobox');
await waitFor(() => userEvent.hover(input));
await waitFor(() => userEvent.click(input));
await waitFor(() => userEvent.type(input, 'M'));
await expect(canvasElement).toHaveTextContent(i18n.tsx._dialog.charactersBelow({ current: 1, min: 2 }));
await waitFor(() => userEvent.type(input, 'i'));
await expect(okButton).toBeEnabled();
},
} satisfies StoryObj<typeof MkDialog>;

View File

@ -37,7 +37,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
<MkSelect v-if="select" v-model="selectedValue" autofocus>
<template v-if="select.items">
<option v-for="item in select.items" :value="item.value">{{ item.text }}</option>
<template v-for="item in select.items">
<optgroup v-if="'sectionTitle' in item" :label="item.sectionTitle">
<option v-for="subItem in item.items" :value="subItem.value">{{ subItem.text }}</option>
</optgroup>
<option v-else :value="item.value">{{ item.text }}</option>
</template>
</template>
</MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
@ -68,11 +73,16 @@ type Input = {
maxLength?: number;
};
type SelectItem = {
value: any;
text: string;
};
type Select = {
items: {
value: any;
text: string;
}[];
items: (SelectItem | {
sectionTitle: string;
items: SelectItem[];
})[];
default: string | null;
};

View File

@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import MkDivider from './MkDivider.vue';
void MkDivider;

View File

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { onBeforeUnmount } from 'vue';
import MkDonation from './MkDonation.vue';
import { instance } from '@/instance.js';
export const Default = {
render(args) {
return {
components: {
MkDonation,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
events() {
return {
closed: action('closed'),
};
},
},
template: '<MkDonation v-bind="props" v-on="events" />',
};
},
args: {
// @ts-expect-error name is used for mocking instance
name: 'Misskey Hub',
},
decorators: [
(_, { args }) => ({
setup() {
// @ts-expect-error name is used for mocking instance
instance.name = args.name;
onBeforeUnmount(() => instance.name = null);
},
template: '<story/>',
}),
],
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkDonation>;

View File

@ -0,0 +1,48 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import MkDrive_file from './MkDrive.file.vue';
import { file } from '../../.storybook/fakes.js';
export const Default = {
render(args) {
return {
components: {
MkDrive_file,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
events() {
return {
chosen: action('chosen'),
dragstart: action('dragstart'),
dragend: action('dragend'),
};
},
},
template: '<MkDrive_file v-bind="props" v-on="events" />',
};
},
args: {
file: file(),
},
parameters: {
chromatic: {
// NOTE: ロードが終わるまで待つ
delay: 3000,
},
layout: 'centered',
},
} satisfies StoryObj<typeof MkDrive_file>;

View File

@ -0,0 +1,70 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { http, HttpResponse } from 'msw';
import * as Misskey from 'cherrypick-js';
import MkDrive_folder from './MkDrive.folder.vue';
import { folder } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
export const Default = {
render(args) {
return {
components: {
MkDrive_folder,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
events() {
return {
chosen: action('chosen'),
move: action('move'),
upload: action('upload'),
removeFile: action('removeFile'),
removeFolder: action('removeFolder'),
dragstart: action('dragstart'),
dragend: action('dragend'),
};
},
},
template: '<MkDrive_folder v-bind="props" v-on="events" />',
};
},
args: {
folder: folder(),
},
parameters: {
layout: 'centered',
msw: {
handlers: [
...commonHandlers,
http.post('/api/drive/folders/delete', async ({ request }) => {
action('POST /api/drive/folders/delete')(await request.json());
return HttpResponse.json(undefined, { status: 204 });
}),
http.post('/api/drive/folders/update', async ({ request }) => {
const req = await request.json() as Misskey.entities.DriveFoldersUpdateRequest;
action('POST /api/drive/folders/update')(req);
return HttpResponse.json({
...folder(),
id: req.folderId,
name: req.name ?? folder().name,
parentId: req.parentId ?? folder().parentId,
});
}),
],
},
},
} satisfies StoryObj<typeof MkDrive_folder>;

View File

@ -27,7 +27,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload">
{{ i18n.ts.uploadFolder }}
</p>
<button v-if="selectMode" class="_button" :class="[$style.checkbox, { [$style.checked]: isSelected }]" @click.prevent.stop="checkboxClicked"></button>
<button v-if="selectMode" class="_button" :class="$style.checkboxWrapper" @click.prevent.stop="checkboxClicked">
<div :class="[$style.checkbox, { [$style.checked]: isSelected }]"></div>
</button>
</div>
</template>
@ -53,6 +55,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{
(ev: 'chosen', v: Misskey.entities.DriveFolder): void;
(ev: 'unchose', v: Misskey.entities.DriveFolder): void;
(ev: 'move', v: Misskey.entities.DriveFolder): void;
(ev: 'upload', file: File, folder: Misskey.entities.DriveFolder);
(ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
@ -68,7 +71,11 @@ const isDragging = ref(false);
const title = computed(() => props.folder.name);
function checkboxClicked() {
emit('chosen', props.folder);
if (props.isSelected) {
emit('unchose', props.folder);
} else {
emit('chosen', props.folder);
}
}
function onClick() {
@ -222,6 +229,17 @@ function rename() {
});
}
function move() {
os.selectDriveFolder(false).then(folder => {
if (folder[0] && folder[0].id === props.folder.id) return;
misskeyApi('drive/folders/update', {
folderId: props.folder.id,
parentId: folder[0] ? folder[0].id : null,
});
});
}
function deleteFolder() {
misskeyApi('drive/folders/delete', {
folderId: props.folder.id,
@ -267,6 +285,10 @@ function onContextmenu(ev: MouseEvent) {
text: i18n.ts.rename,
icon: 'ti ti-forms',
action: rename,
}, {
text: i18n.ts.move,
icon: 'ti ti ti-folder-symlink',
action: move,
}, { type: 'divider' }, {
text: i18n.ts.delete,
icon: 'ti ti-trash',
@ -311,17 +333,43 @@ function onContextmenu(ev: MouseEvent) {
}
}
.checkbox {
.checkboxWrapper {
position: absolute;
bottom: 8px;
right: 8px;
width: 16px;
height: 16px;
background: #fff;
border: solid 1px #000;
border-radius: 50%;
bottom: 2px;
right: 2px;
padding: 8px;
box-sizing: border-box;
&.checked {
background: var(--accent);
> .checkbox {
position: relative;
width: 18px;
height: 18px;
background: #fff;
border: solid 2px var(--divider);
border-radius: 4px;
box-sizing: border-box;
&.checked {
border-color: var(--accent);
background: var(--accent);
&::after {
content: "\ea5e";
font-family: 'tabler-icons';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 12px;
line-height: 22px;
}
}
}
&:hover {
background: var(--accentedBg);
}
}

View File

@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import MkDrive_navFolder from './MkDrive.navFolder.vue';
void MkDrive_navFolder;

View File

@ -0,0 +1,82 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { http, HttpResponse } from 'msw';
import * as Misskey from 'cherrypick-js';
import MkDrive from './MkDrive.vue';
import { file, folder } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
export const Default = {
render(args) {
return {
components: {
MkDrive,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
events() {
return {
selected: action('selected'),
'change-selection': action('change-selection'),
'move-root': action('move-root'),
cd: action('cd'),
'open-folder': action('open-folder'),
};
},
},
template: '<MkDrive v-bind="props" v-on="events" />',
};
},
parameters: {
chromatic: {
// NOTE: ロードが終わるまで待つ
delay: 3000,
},
layout: 'centered',
msw: {
handlers: [
...commonHandlers,
http.post('/api/drive/files', async ({ request }) => {
action('POST /api/drive/files')(await request.json());
return HttpResponse.json([file()]);
}),
http.post('/api/drive/folders', async ({ request }) => {
action('POST /api/drive/folders')(await request.json());
return HttpResponse.json([folder(crypto.randomUUID())]);
}),
http.post('/api/drive/folders/create', async ({ request }) => {
const req = await request.json() as Misskey.entities.DriveFoldersCreateRequest;
action('POST /api/drive/folders/create')(req);
return HttpResponse.json(folder(crypto.randomUUID(), req.name, req.parentId));
}),
http.post('/api/drive/folders/delete', async ({ request }) => {
action('POST /api/drive/folders/delete')(await request.json());
return HttpResponse.json(undefined, { status: 204 });
}),
http.post('/api/drive/folders/update', async ({ request }) => {
const req = await request.json() as Misskey.entities.DriveFoldersUpdateRequest;
action('POST /api/drive/folders/update')(req);
return HttpResponse.json({
...folder(),
id: req.folderId,
name: req.name ?? folder().name,
parentId: req.parentId ?? folder().parentId,
});
}),
]
},
},
} satisfies StoryObj<typeof MkDrive>;

View File

@ -52,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:selectMode="select === 'folder'"
:isSelected="selectedFolders.some(x => x.id === f.id)"
@chosen="chooseFolder"
@unchose="unchoseFolder"
@move="move"
@upload="upload"
@removeFile="removeFile"
@ -428,6 +429,11 @@ function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
}
}
function unchoseFolder(folderToUnchose: Misskey.entities.DriveFolder) {
selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToUnchose.id);
emit('change-selection', selectedFolders.value);
}
function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) {
if (!target) {
goRoot();

Some files were not shown because too many files have changed in this diff Show More