Merge branch 'develop' into enh-i-update-0

This commit is contained in:
かっこかり 2024-07-30 17:30:44 +09:00 committed by GitHub
commit 104b27ef19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 1199 additions and 318 deletions

View File

@ -9,7 +9,6 @@ on:
paths: paths:
- packages/backend/** - packages/backend/**
- .github/workflows/get-api-diff.yml - .github/workflows/get-api-diff.yml
- .github/workflows/get-api-diff.yml
jobs: jobs:
get-from-misskey: get-from-misskey:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -9,11 +9,15 @@
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に - Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
- 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
- Feat: ユーザ作成時にSystemWebhookを送信可能に #14281 - Feat: ユーザ作成時にSystemWebhookを送信可能に #14281
- Enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正 - Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題 - Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正 - Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
### Client ### Client
- Feat: ユーザーページから「このユーザーのノートを検索」できるように (#14128)
- Feat: 検索ページはクエリを受け付けるようになりました (#14128)
- Enhance: 検索ページのUI改善 (#14128)
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善 - Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
- Enhance: 非ログイン時に他サーバーに遷移するアクションを追加 - Enhance: 非ログイン時に他サーバーに遷移するアクションを追加
- Enhance: 非ログイン時のハイライトTLのデザインを改善 - Enhance: 非ログイン時のハイライトTLのデザインを改善
@ -24,6 +28,11 @@
- Enhance: AiScriptを0.19.0にアップデート - Enhance: AiScriptを0.19.0にアップデート
- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`) - Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
- Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように - Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように
- 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: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968) - Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
- Fix: リバーシの対局を正しく共有できないことがある問題を修正 - Fix: リバーシの対局を正しく共有できないことがある問題を修正
@ -46,6 +55,7 @@
- Fix: ダイレクト投稿の"削除して編集"において、宛先が保持されていなかった問題を修正 - Fix: ダイレクト投稿の"削除して編集"において、宛先が保持されていなかった問題を修正
- Fix: 投稿フォームへのURL貼り付けによる引用が下書きに保存されていなかった問題を修正 - Fix: 投稿フォームへのURL貼り付けによる引用が下書きに保存されていなかった問題を修正
- Fix: "削除して編集"や下書きにおいて、リアクションの受け入れ設定が保持/保存されていなかった問題を修正 - Fix: "削除して編集"や下書きにおいて、リアクションの受け入れ設定が保持/保存されていなかった問題を修正
- Fix: 投稿フォームにートのURLを貼り付けて"引用として添付"した場合、投稿文を空にすることによるRenote化が出来なかった問題を修正
### Server ### Server
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
@ -56,6 +66,7 @@
- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに - Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに - Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
- Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように - Enhance: `default.yml`内の`url`, `db.db`, `db.user`, `db.pass`を環境変数から読み込めるように
- Enhance: エンドポイント`api/meta`にプロパティ`noteSearchableScope`が増え、`string`値`local`または`global`を返却します
- Enhance: 連合する必要のないプロフィール項目しか更新されなかった場合には連合先にUpdateアクティビティを発行しないように - Enhance: 連合する必要のないプロフィール項目しか更新されなかった場合には連合先にUpdateアクティビティを発行しないように
- Fix: チャート生成時にinstance.suspensionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正 - Fix: チャート生成時にinstance.suspensionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006) - Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
@ -84,6 +95,11 @@
- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正 - Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正
(Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1) (Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1)
- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251 - Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251
- 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)で確認可能です
### Misskey.js ### Misskey.js
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応) - Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)

View File

@ -1384,7 +1384,7 @@ _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." 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" 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." 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: "Query URL" inquiryUrl: "Inquiry URL"
inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information." inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information."
_accountMigration: _accountMigration:
moveFrom: "Migrate another account to this one" moveFrom: "Migrate another account to this one"

50
locales/index.d.ts vendored
View File

@ -256,6 +256,10 @@ export interface Locale extends ILocale {
* *
*/ */
"searchUser": string; "searchUser": string;
/**
*
*/
"searchThisUsersNotes": string;
/** /**
* *
*/ */
@ -632,6 +636,10 @@ export interface Locale extends ILocale {
* *
*/ */
"editAntenna": string; "editAntenna": string;
/**
*
*/
"createAntenna": string;
/** /**
* *
*/ */
@ -792,6 +800,10 @@ export interface Locale extends ILocale {
* *
*/ */
"host": string; "host": string;
/**
*
*/
"selectSelf": string;
/** /**
* *
*/ */
@ -4440,6 +4452,14 @@ export interface Locale extends ILocale {
* *
*/ */
"archive": string; "archive": string;
/**
*
*/
"archived": string;
/**
*
*/
"unarchive": string;
/** /**
* {name} * {name}
*/ */
@ -4480,6 +4500,10 @@ export interface Locale extends ILocale {
* *
*/ */
"specifyUser": string; "specifyUser": string;
/**
*
*/
"specifyHost": string;
/** /**
* *
*/ */
@ -5016,6 +5040,14 @@ export interface Locale extends ILocale {
* *
*/ */
"sensitiveMediaRevealConfirm": string; "sensitiveMediaRevealConfirm": string;
/**
*
*/
"createdLists": string;
/**
*
*/
"createdAntennas": string;
"_delivery": { "_delivery": {
/** /**
* *
@ -10098,6 +10130,24 @@ export interface Locale extends ILocale {
*/ */
"loop": string; "loop": string;
}; };
"_contextMenu": {
/**
*
*/
"title": string;
/**
*
*/
"app": string;
/**
* Shiftキーでアプリケーション
*/
"appWithShift": string;
/**
* UI
*/
"native": string;
};
} }
declare const locales: { declare const locales: {
[lang: string]: Locale; [lang: string]: Locale;

View File

@ -60,6 +60,7 @@ copyFileId: "ファイルIDをコピー"
copyFolderId: "フォルダーIDをコピー" copyFolderId: "フォルダーIDをコピー"
copyProfileUrl: "プロフィールURLをコピー" copyProfileUrl: "プロフィールURLをコピー"
searchUser: "ユーザーを検索" searchUser: "ユーザーを検索"
searchThisUsersNotes: "ユーザーのノートを検索"
reply: "返信" reply: "返信"
loadMore: "もっと見る" loadMore: "もっと見る"
showMore: "もっと見る" showMore: "もっと見る"
@ -154,6 +155,7 @@ editList: "リストを編集"
selectChannel: "チャンネルを選択" selectChannel: "チャンネルを選択"
selectAntenna: "アンテナを選択" selectAntenna: "アンテナを選択"
editAntenna: "アンテナを編集" editAntenna: "アンテナを編集"
createAntenna: "アンテナを作成"
selectWidget: "ウィジェットを選択" selectWidget: "ウィジェットを選択"
editWidgets: "ウィジェットを編集" editWidgets: "ウィジェットを編集"
editWidgetsExit: "編集を終了" editWidgetsExit: "編集を終了"
@ -194,6 +196,7 @@ followConfirm: "{name}をフォローしますか?"
proxyAccount: "プロキシアカウント" proxyAccount: "プロキシアカウント"
proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。" proxyAccountDescription: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。"
host: "ホスト" host: "ホスト"
selectSelf: "自分を選択"
selectUser: "ユーザーを選択" selectUser: "ユーザーを選択"
recipient: "宛先" recipient: "宛先"
annotation: "注釈" annotation: "注釈"
@ -1106,6 +1109,8 @@ preservedUsernames: "予約ユーザー名"
preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。" preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。"
createNoteFromTheFile: "このファイルからノートを作成" createNoteFromTheFile: "このファイルからノートを作成"
archive: "アーカイブ" archive: "アーカイブ"
archived: "アーカイブ済み"
unarchive: "アーカイブ解除"
channelArchiveConfirmTitle: "{name}をアーカイブしますか?" channelArchiveConfirmTitle: "{name}をアーカイブしますか?"
channelArchiveConfirmDescription: "アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。" channelArchiveConfirmDescription: "アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。"
thisChannelArchived: "このチャンネルはアーカイブされています。" thisChannelArchived: "このチャンネルはアーカイブされています。"
@ -1116,6 +1121,7 @@ preventAiLearning: "生成AIによる学習を拒否"
preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。" preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。"
options: "オプション" options: "オプション"
specifyUser: "ユーザー指定" specifyUser: "ユーザー指定"
specifyHost: "ホスト指定"
failedToPreviewUrl: "プレビューできません" failedToPreviewUrl: "プレビューできません"
update: "更新" update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール" rolesThatCanBeUsedThisEmojiAsReaction: "リアクションとして使えるロール"
@ -1250,6 +1256,8 @@ inquiry: "お問い合わせ"
tryAgain: "もう一度お試しください。" tryAgain: "もう一度お試しください。"
confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する" confirmWhenRevealingSensitiveMedia: "センシティブなメディアを表示するとき確認する"
sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?" sensitiveMediaRevealConfirm: "センシティブなメディアです。表示しますか?"
createdLists: "作成したリスト"
createdAntennas: "作成したアンテナ"
_delivery: _delivery:
status: "配信状態" status: "配信状態"
@ -2692,3 +2700,9 @@ _mediaControls:
pip: "ピクチャインピクチャ" pip: "ピクチャインピクチャ"
playbackRate: "再生速度" playbackRate: "再生速度"
loop: "ループ再生" loop: "ループ再生"
_contextMenu:
title: "コンテキストメニュー"
app: "アプリケーション"
appWithShift: "Shiftキーでアプリケーション"
native: "ブラウザのUI"

View File

@ -180,6 +180,10 @@ addAccount: "계정 추가"
reloadAccountsList: "계정 목록 새로고침" reloadAccountsList: "계정 목록 새로고침"
loginFailed: "로그인에 실패했습니다" loginFailed: "로그인에 실패했습니다"
showOnRemote: "리모트에서 보기" showOnRemote: "리모트에서 보기"
continueOnRemote: "리모트에서 계속"
chooseServerOnMisskeyHub: "Misskey Hub에서 서버 찾아보기"
specifyServerHost: "서버 도메인 직접 지정"
inputHostName: "도메인을 입력하세요"
general: "일반" general: "일반"
wallpaper: "배경" wallpaper: "배경"
setWallpaper: "배경 설정" setWallpaper: "배경 설정"
@ -477,6 +481,7 @@ noMessagesYet: "아직 대화가 없습니다"
newMessageExists: "새 메시지가 있습니다" newMessageExists: "새 메시지가 있습니다"
onlyOneFileCanBeAttached: "메시지에 첨부할 수 있는 파일은 하나까지입니다" onlyOneFileCanBeAttached: "메시지에 첨부할 수 있는 파일은 하나까지입니다"
signinRequired: "진행하기 전에 로그인을 해 주세요" signinRequired: "진행하기 전에 로그인을 해 주세요"
signinOrContinueOnRemote: "계속하려면 사용하는 서버로 이동하거나 이 서버에 로그인해야 합니다."
invitations: "초대" invitations: "초대"
invitationCode: "초대 코드" invitationCode: "초대 코드"
checking: "확인하는 중입니다" checking: "확인하는 중입니다"
@ -1242,6 +1247,8 @@ keepOriginalFilenameDescription: "이 설정을 끄면 업로드를 할 때 파
noDescription: "설명문이 없습니다" noDescription: "설명문이 없습니다"
alwaysConfirmFollow: "팔로우일 때 항상 확인하기" alwaysConfirmFollow: "팔로우일 때 항상 확인하기"
inquiry: "문의하기" inquiry: "문의하기"
tryAgain: "다시 시도해 주세요."
confirmWhenRevealingSensitiveMedia: "민감한 미디어를 열 때 두 번 확인"
_delivery: _delivery:
status: "전송 상태" status: "전송 상태"
stop: "정지됨" stop: "정지됨"
@ -1694,6 +1701,7 @@ _role:
canManageAvatarDecorations: "아바타 꾸미기 관리" canManageAvatarDecorations: "아바타 꾸미기 관리"
driveCapacity: "드라이브 용량" driveCapacity: "드라이브 용량"
alwaysMarkNsfw: "파일을 항상 NSFW로 지정" alwaysMarkNsfw: "파일을 항상 NSFW로 지정"
canUpdateBioMedia: "아바타 및 배너 이미지 변경 허용"
pinMax: "고정할 수 있는 노트 수" pinMax: "고정할 수 있는 노트 수"
antennaMax: "만들 수 있는 안테나 수" antennaMax: "만들 수 있는 안테나 수"
wordMuteMax: "단어 뮤트할 수 있는 문자 수" wordMuteMax: "단어 뮤트할 수 있는 문자 수"
@ -2416,6 +2424,7 @@ _webhookSettings:
_systemEvents: _systemEvents:
abuseReport: "유저로부터 신고를 받았을 때" abuseReport: "유저로부터 신고를 받았을 때"
abuseReportResolved: "받은 신고를 처리했을 때" abuseReportResolved: "받은 신고를 처리했을 때"
userCreated: "유저가 생성되었을 때"
deleteConfirm: "Webhook을 삭제할까요?" deleteConfirm: "Webhook을 삭제할까요?"
_abuseReport: _abuseReport:
_notificationRecipient: _notificationRecipient:

View File

@ -443,7 +443,7 @@ moderator: "ผู้ควบคุม"
moderation: "การกลั่นกรอง" moderation: "การกลั่นกรอง"
moderationNote: "โน้ตการกลั่นกรอง" moderationNote: "โน้ตการกลั่นกรอง"
addModerationNote: "เพิ่มโน้ตการกลั่นกรอง" addModerationNote: "เพิ่มโน้ตการกลั่นกรอง"
moderationLogs: "ปูมการแก้ไข" moderationLogs: "ปูมการควบคุมดูแล"
nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} ราย" nUsersMentioned: "กล่าวถึงโดยผู้ใช้ {n} ราย"
securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน" securityKeyAndPasskey: "ความปลอดภัยและรหัสผ่าน"
securityKey: "กุญแจความปลอดภัย" securityKey: "กุญแจความปลอดภัย"
@ -509,7 +509,7 @@ showReactionsCount: "แสดงจำนวนรีแอกชั่นใ
noHistory: "ไม่มีประวัติ" noHistory: "ไม่มีประวัติ"
signinHistory: "ประวัติการเข้าสู่ระบบ" signinHistory: "ประวัติการเข้าสู่ระบบ"
enableAdvancedMfm: "เปิดใช้งาน MFM ขั้นสูง" enableAdvancedMfm: "เปิดใช้งาน MFM ขั้นสูง"
enableAnimatedMfm: "เปิดการใช้งาน MFM ด้วยแอนิเมชั่น" enableAnimatedMfm: "เปิดการใช้งาน MFM แบบเคลื่อนไหว"
doing: "กำลังประมวลผล......" doing: "กำลังประมวลผล......"
category: "หมวดหมู่" category: "หมวดหมู่"
tags: "นามแฝง" tags: "นามแฝง"
@ -625,7 +625,7 @@ enablePlayer: "เปิดเครื่องเล่นวิดีโอ"
disablePlayer: "ปิดเครื่องเล่นวิดีโอ" disablePlayer: "ปิดเครื่องเล่นวิดีโอ"
expandTweet: "ขยายทวีต" expandTweet: "ขยายทวีต"
themeEditor: "ตัวแก้ไขธีม" themeEditor: "ตัวแก้ไขธีม"
description: "รายละเอียด" description: "คำอธิบาย"
describeFile: "เพิ่มแคปชั่น" describeFile: "เพิ่มแคปชั่น"
enterFileDescription: "ใส่แคปชั่น" enterFileDescription: "ใส่แคปชั่น"
author: "ผู้เขียน" author: "ผู้เขียน"
@ -666,7 +666,7 @@ smtpSecure: "ใช้โดยนัย SSL/TLS สำหรับการเ
smtpSecureInfo: "ปิดสิ่งนี้เมื่อใช้ STARTTLS" smtpSecureInfo: "ปิดสิ่งนี้เมื่อใช้ STARTTLS"
testEmail: "ทดสอบการส่งอีเมล" testEmail: "ทดสอบการส่งอีเมล"
wordMute: "ปิดเสียงคำ" wordMute: "ปิดเสียงคำ"
hardWordMute: "ปิดเสียงคำยาก" hardWordMute: "ปิดเสียงคำแบบแข็งโป๊ก"
regexpError: "เกิดข้อผิดพลาดใน regular expression" regexpError: "เกิดข้อผิดพลาดใน regular expression"
regexpErrorDescription: "เกิดข้อผิดพลาดใน regular expression บรรทัดที่ {line} ของการปิดเสียงคำ {tab} :" regexpErrorDescription: "เกิดข้อผิดพลาดใน regular expression บรรทัดที่ {line} ของการปิดเสียงคำ {tab} :"
instanceMute: "ปิดเสียงเซิร์ฟเวอร์" instanceMute: "ปิดเสียงเซิร์ฟเวอร์"
@ -1034,7 +1034,7 @@ thisPostMayBeAnnoyingIgnore: "โพสต์ยังไงก็แล้ว
collapseRenotes: "ยุบรีโน้ตที่คุณเคยเห็นแล้ว" collapseRenotes: "ยุบรีโน้ตที่คุณเคยเห็นแล้ว"
collapseRenotesDescription: "พับย่อโน้ตที่เคยตอบสนองหรือรีโน้ตแล้ว" collapseRenotesDescription: "พับย่อโน้ตที่เคยตอบสนองหรือรีโน้ตแล้ว"
internalServerError: "เซิร์ฟเวอร์ภายในเกิดข้อผิดพลาด" internalServerError: "เซิร์ฟเวอร์ภายในเกิดข้อผิดพลาด"
internalServerErrorDescription: "เซิร์ฟเวอร์รันค้นพบข้อผิดพลาดที่ไม่คาดคิด" internalServerErrorDescription: "เกิดข้อผิดพลาดที่ไม่คาดคิดภายในเซิร์ฟเวอร์"
copyErrorInfo: "คัดลอกรายละเอียดข้อผิดพลาด" copyErrorInfo: "คัดลอกรายละเอียดข้อผิดพลาด"
joinThisServer: "ลงทะเบียนบนเซิร์ฟเวอร์นี้" joinThisServer: "ลงทะเบียนบนเซิร์ฟเวอร์นี้"
exploreOtherServers: "มองหาเซิร์ฟเวอร์อื่น" exploreOtherServers: "มองหาเซิร์ฟเวอร์อื่น"
@ -1042,7 +1042,7 @@ letsLookAtTimeline: "มาดูไทม์ไลน์กัน"
disableFederationConfirm: "ปิดใช้งานสหพันธ์เลยใช่ไหม?" disableFederationConfirm: "ปิดใช้งานสหพันธ์เลยใช่ไหม?"
disableFederationConfirmWarn: "โพสต์จะยังคงเป็นสาธารณะต่อไป เว้นแต่จะตั้งค่าเป็นอย่างอื่น" disableFederationConfirmWarn: "โพสต์จะยังคงเป็นสาธารณะต่อไป เว้นแต่จะตั้งค่าเป็นอย่างอื่น"
disableFederationOk: "ปิดการใช้งาน" disableFederationOk: "ปิดการใช้งาน"
invitationRequiredToRegister: "ขณะนี้เซิร์ฟเวอร์นี้เปิดรับสมาชิกใหม่ผ่านระบบเชิญเท่านั้น ผู้ที่มีรหัสเชิญสามารถลงทะเบียนได้" invitationRequiredToRegister: "เซิร์ฟเวอร์นี้เป็นแบบรับเชิญ เฉพาะผู้มีรหัสเชิญเท่านั้นถึงสามารถลงทะเบียนได้"
emailNotSupported: "เซิร์ฟเวอร์นี้ไม่รองรับการส่งอีเมล" emailNotSupported: "เซิร์ฟเวอร์นี้ไม่รองรับการส่งอีเมล"
postToTheChannel: "โพสต์ลงช่อง" postToTheChannel: "โพสต์ลงช่อง"
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ" cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
@ -1052,7 +1052,7 @@ likeOnlyForRemote: "ทั้งหมด (เฉพาะการถูกใ
nonSensitiveOnly: "เฉพาะไม่มีเนื้อหาละเอียดอ่อน" nonSensitiveOnly: "เฉพาะไม่มีเนื้อหาละเอียดอ่อน"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "เฉพาะไม่มีเนื้อหาละเอียดอ่อน (เฉพาะการถูกใจจากระยะไกลเท่านั้น)" nonSensitiveOnlyForLocalLikeOnlyForRemote: "เฉพาะไม่มีเนื้อหาละเอียดอ่อน (เฉพาะการถูกใจจากระยะไกลเท่านั้น)"
rolesAssignedToMe: "บทบาทที่ได้รับมอบหมายให้ฉัน" rolesAssignedToMe: "บทบาทที่ได้รับมอบหมายให้ฉัน"
resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?" resetPasswordConfirm: "ต้องการรีเซ็ตรหัสผ่านใช่ไหม?"
sensitiveWords: "คำที่มีเนื้อหาละเอียดอ่อน" sensitiveWords: "คำที่มีเนื้อหาละเอียดอ่อน"
sensitiveWordsDescription: "โน้ตที่มีคำที่ระบุไว้จะถูกตั้งค่าการมองเห็นของให้แสดงเฉพาะในหน้าหลักเท่านั้น คั่นคำด้วยการขึ้นบรรทัดใหม่" sensitiveWordsDescription: "โน้ตที่มีคำที่ระบุไว้จะถูกตั้งค่าการมองเห็นของให้แสดงเฉพาะในหน้าหลักเท่านั้น คั่นคำด้วยการขึ้นบรรทัดใหม่"
sensitiveWordsDescription2: "ถ้าแยกด้วยเว้นวรรคจะเป็นการระบุ AND และถ้าล้อมคำด้วยสแลช (/) จะเป็นการใช้ regular expression" sensitiveWordsDescription2: "ถ้าแยกด้วยเว้นวรรคจะเป็นการระบุ AND และถ้าล้อมคำด้วยสแลช (/) จะเป็นการใช้ regular expression"
@ -1169,7 +1169,7 @@ notifyNotes: "แจ้งเตือนเกี่ยวกับโพสต
unnotifyNotes: "หยุดการแจ้งเตือนเกี่ยวกับโน้ตใหม่" unnotifyNotes: "หยุดการแจ้งเตือนเกี่ยวกับโน้ตใหม่"
authentication: "การตรวจสอบสิทธิ์" authentication: "การตรวจสอบสิทธิ์"
authenticationRequiredToContinue: "กรุณายืนยันตัวตนทางอิเล็กทรอนิกส์เพื่อดำเนินการต่อ" authenticationRequiredToContinue: "กรุณายืนยันตัวตนทางอิเล็กทรอนิกส์เพื่อดำเนินการต่อ"
dateAndTime: "เวลาประทับ" dateAndTime: "วันเวลา"
showRenotes: "แสดงรีโน้ต" showRenotes: "แสดงรีโน้ต"
edited: "แก้ไขแล้ว" edited: "แก้ไขแล้ว"
notificationRecieveConfig: "การตั้งค่าการแจ้งเตือน" notificationRecieveConfig: "การตั้งค่าการแจ้งเตือน"
@ -1184,7 +1184,7 @@ confirmShowRepliesAll: "การดำเนินการนี้ไม่
confirmHideRepliesAll: "การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณต้องการซ่อนการตอบกลับผู้อื่นจากผู้ใช้ทุกคนที่คุณติดตามอยู่ ไปจากไทม์ไลน์ใช่ไหม?" confirmHideRepliesAll: "การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณต้องการซ่อนการตอบกลับผู้อื่นจากผู้ใช้ทุกคนที่คุณติดตามอยู่ ไปจากไทม์ไลน์ใช่ไหม?"
externalServices: "บริการภายนอก" externalServices: "บริการภายนอก"
sourceCode: "ซอร์สโค้ด" sourceCode: "ซอร์สโค้ด"
sourceCodeIsNotYetProvided: "ซอร์สโค้ดยังไม่พร้อมใช้งาน โปรดติดต่อผู้ดูแลระบบของคุณเพื่อแก้ไขปัญหานี้" sourceCodeIsNotYetProvided: "ซอร์สโค้ดยังไม่พร้อมใช้งาน โปรดติดต่อผู้ดูแลระบบเพื่อแก้ไขปัญหานี้"
repositoryUrl: "URL ของ repository" repositoryUrl: "URL ของ repository"
repositoryUrlDescription: "หากมีที่เก็บซอร์สโค้ดที่เปิดเผยต่อสาธารณะ ให้ป้อน URL ที่เก็บซอร์สโค้ดนั้น แต่หากคุณใช้ Misskey ตามต้นฉบับ (ไม่มีการเปลี่ยนแปลงซอร์สโค้ด) ให้ป้อน https://github.com/misskey-dev/misskey" repositoryUrlDescription: "หากมีที่เก็บซอร์สโค้ดที่เปิดเผยต่อสาธารณะ ให้ป้อน URL ที่เก็บซอร์สโค้ดนั้น แต่หากคุณใช้ Misskey ตามต้นฉบับ (ไม่มีการเปลี่ยนแปลงซอร์สโค้ด) ให้ป้อน https://github.com/misskey-dev/misskey"
repositoryUrlOrTarballRequired: "หากคุณไม่มี repository สาธารณะ คุณจะต้องจัดเตรียม tarball แทน ดู .config/example.yml สำหรับรายละเอียด" repositoryUrlOrTarballRequired: "หากคุณไม่มี repository สาธารณะ คุณจะต้องจัดเตรียม tarball แทน ดู .config/example.yml สำหรับรายละเอียด"
@ -1275,20 +1275,20 @@ _bubbleGame:
section2: "เมื่อวัตถุประเภทเดียวกันมารวมกัน พวกมันจะกลายเป็นวัตถุใหม่และคุณจะได้รับคะแนน" section2: "เมื่อวัตถุประเภทเดียวกันมารวมกัน พวกมันจะกลายเป็นวัตถุใหม่และคุณจะได้รับคะแนน"
section3: "หากวัตถุล้นออกมาจากกล่อง เกมก็จะจบลง ตั้งเป้าทำคะแนนให้สูงด้วยการหลอมวัตถุต่าง ๆ โดยไม่ทำให้ล้นกล่อง!" section3: "หากวัตถุล้นออกมาจากกล่อง เกมก็จะจบลง ตั้งเป้าทำคะแนนให้สูงด้วยการหลอมวัตถุต่าง ๆ โดยไม่ทำให้ล้นกล่อง!"
_announcement: _announcement:
forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น" forExistingUsers: "ผู้ใช้งานที่มีอยู่ตอนนี้เท่านั้น"
forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน" forExistingUsersDescription: "หากเปิดใช้งาน การประกาศนี้จะแสดงเฉพาะกับผู้ใช้ที่สร้างบัญชีก่อน/ที่มีอยู่ในขณะที่สร้างประกาศนี้เท่านั้น หากปิดใช้งาน การประกาศนี้จะแสดงกับผู้ใช้ที่สร้างบัญชีหลังจากสร้างประกาศนี้ด้วย"
needConfirmationToRead: "จำเป็นต้องยืนยันว่าอ่านแล้ว" needConfirmationToRead: "จำเป็นต้องยืนยันว่าอ่านแล้ว"
needConfirmationToReadDescription: "กล่องโต้ตอบการยืนยันจะปรากฏขึ้นเมื่อจะทำเครื่องหมายว่าอ่านแล้ว นอกจากนี้ยังทำให้ประกาศนี้ยังไม่ถูกอ่านเมื่อใช้ฟังก์ชั่น “ทำเครื่องหมายฯ ทั้งหมดว่าอ่านแล้ว”" needConfirmationToReadDescription: "กล่องโต้ตอบการยืนยันจะปรากฏขึ้นเมื่อจะทำเครื่องหมายว่าอ่านแล้ว นอกจากนี้ยังทำให้ประกาศนี้ยังไม่ถูกอ่านเมื่อใช้ฟังก์ชั่น “ทำเครื่องหมายฯ ทั้งหมดว่าอ่านแล้ว”"
end: "เก็บประกาศ" end: "เก็บประกาศ"
tooManyActiveAnnouncementDescription: "การมีประกาศที่ใช้งานมากเกินไปนั้นอาจจะทำให้ประสบการณ์ของผู้ใช้งานนั้นดูแย่ลง โปรดกรุณาพิจารณาการเก็บประกาศที่ล้าสมัยด้วยนะค่ะ" tooManyActiveAnnouncementDescription: "เนื่องจากมีการประกาศที่ยังใช้งานอยู่จำนวนมาก อาจทำให้ UX ลดลง แนะนำให้พิจารณาการเก็บประกาศที่สิ้นสุดไปแล้ว"
readConfirmTitle: "ทำเครื่องหมายว่าอ่านแล้วเลยไหม?" readConfirmTitle: "ทำเครื่องหมายว่าอ่านแล้วเลยไหม?"
readConfirmText: "จะทำเครื่องหมายใส่ “{title}” ว่าอ่านแล้ว" readConfirmText: "จะทำเครื่องหมายใส่ “{title}” ว่าอ่านแล้ว"
shouldNotBeUsedToPresentPermanentInfo: "เราขอแนะนำให้ใช้ประกาศเพื่อโพสต์ข้อมูลแบบ flow มากกว่าข้อมูลแบบ stock เนื่องจากมีแนวโน้มที่จะส่งผลเสียต่อ UX โดยเฉพาะสำหรับผู้ใช้ใหม่" shouldNotBeUsedToPresentPermanentInfo: "เนื่องจากมีความเป็นไปได้สูงที่จะส่งผลเสียต่อง UX ของผู้ใช้ใหม่ จึงขอแนะนำให้ใช้ประกาศสำหรับข้อมูลที่ต้องการการตอบสนองในทันที ไม่ใช่ข้อมูลที่ต้องการแสดงตลอดเวลา"
dialogAnnouncementUxWarn: "เราขอแนะนำให้ใช้ด้วยความระมัดระวัง เนื่องจากการแจ้งเตือนแบบกล่องโต้ตอบตั้งแต่ 2 รายการขึ้นไปพร้อมกันอาจส่งผลเสียต่อ UX ได้อย่างมาก" dialogAnnouncementUxWarn: "เราขอแนะนำให้ใช้ด้วยความระมัดระวัง เนื่องจากการแจ้งเตือนแบบกล่องโต้ตอบตั้งแต่ 2 รายการขึ้นไปพร้อมกันอาจส่งผลเสียต่อ UX ได้อย่างมาก"
silence: "ไม่มีการแจ้งเตือน" silence: "ไม่มีการแจ้งเตือน"
silenceDescription: "หากเปิดใช้งาน จะไม่มีการแจ้งเตือนประกาศนี้ และผู้ใช้จะไม่จำเป็นต้องทำเครื่องหมายว่าอ่านแล้ว" silenceDescription: "หากเปิดใช้งาน จะไม่มีการแจ้งเตือนประกาศนี้ และผู้ใช้จะไม่จำเป็นต้องทำเครื่องหมายว่าอ่านแล้ว"
_initialAccountSetting: _initialAccountSetting:
accountCreated: "คุณได้สร้างบัญชีของคุณสำเร็จเรียบร้อยแล้ว!" accountCreated: "สร้างบัญชีเสร็จสมบูรณ์!"
letsStartAccountSetup: "สำหรับผู้เริ่มต้นมาตั้งค่าโปรไฟล์ของคุณกันเถอะ" letsStartAccountSetup: "สำหรับผู้เริ่มต้นมาตั้งค่าโปรไฟล์ของคุณกันเถอะ"
letsFillYourProfile: "ก่อนอื่นมาตั้งค่าโปรไฟล์ของคุณ" letsFillYourProfile: "ก่อนอื่นมาตั้งค่าโปรไฟล์ของคุณ"
profileSetting: "ตั้งค่าโปรไฟล์" profileSetting: "ตั้งค่าโปรไฟล์"
@ -1387,26 +1387,26 @@ _serverSettings:
inquiryUrl: "URL สำหรับการติดต่อสอบถาม" inquiryUrl: "URL สำหรับการติดต่อสอบถาม"
inquiryUrlDescription: "ระบุ URL ของหน้าเว็บที่มีแบบฟอร์มสำหรับติดต่อผู้ดูแลเซิร์ฟเวอร์ หรือข้อมูลการติดต่อของผู้ดูแลเซิร์ฟเวอร์" inquiryUrlDescription: "ระบุ URL ของหน้าเว็บที่มีแบบฟอร์มสำหรับติดต่อผู้ดูแลเซิร์ฟเวอร์ หรือข้อมูลการติดต่อของผู้ดูแลเซิร์ฟเวอร์"
_accountMigration: _accountMigration:
moveFrom: "ย้ายข้อมูลบัญชีอื่นไปยังอีกบัญชีนี้หนึ่ง" moveFrom: "ย้ายจากบัญชีอื่นมาที่บัญชีนี้"
moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น" moveFromSub: "สร้างนามแฝงไปยังบัญชีอื่น"
moveFromLabel: "บัญชีที่จะย้ายจาก #{n}" moveFromLabel: "บัญชีที่จะย้ายจาก #{n}"
moveFromDescription: "หากต้องการโอนข้อมูลจากบัญชีอื่นมายังบัญชีนี้ จำเป็นต้องสร้างบัญชีนามแฝง (alias) ไว้ที่นี่ด้วย\nกรุณากรอกบัญชีเดิมในรูปแบบ: @username@server.example.com\nหากต้องการลบ alias, ให้เว้นว่างไว้แล้วบันทึก (ไม่แนะนำ)" moveFromDescription: "หากต้องการโอนข้อมูลจากบัญชีอื่นมายังบัญชีนี้ จำเป็นต้องสร้างบัญชีนามแฝง (alias) ไว้ที่นี่ด้วย\nกรุณากรอกบัญชีเดิมในรูปแบบ: @username@server.example.com\nหากต้องการลบ alias, ให้เว้นว่างไว้แล้วบันทึก (ไม่แนะนำ)"
moveTo: "ย้ายข้อมูลบัญชีนี้ไปยังบัญชีอีกหนึ่ง" moveTo: "ย้ายบัญชีนี้ไปยังบัญชีใหม่"
moveToLabel: "บัญชีที่จะย้ายไปที่:" moveToLabel: "บัญชีที่จะย้ายไปที่:"
moveCannotBeUndone: "ไม่สามารถยกเลิกการโอนย้ายบัญชีได้" moveCannotBeUndone: "ไม่สามารถยกเลิกการโอนย้ายบัญชีได้"
moveAccountDescription: "การดำเนินการนี้จะย้ายบัญชีของคุณไปยังบัญชีอื่น\n・ผู้ที่กำลังติดตามคุณจากบัญชีนี้จะถูกย้ายไปยังบัญชีใหม่โดยอัตโนมัติ\n・บัญชีนี้จะเลิกติดตามผู้ใช้ทั้งหมดที่กำลังติดตามอยู่\n・คุณจะไม่สามารถสร้างโน้ต ฯลฯ ในบัญชีนี้ได้\n\nแม้ว่าการย้ายผู้ที่ติดตามคุณจะเป็นไปโดยอัตโนมัติ แต่คุณต้องเตรียมขั้นตอนบางอย่างด้วยตนเอง เพื่อย้ายรายชื่อผู้ใช้ที่คุณกำลังติดตาม โดยดำเนินการส่งออกรายชื่อแล้วค่อยนำเข้ามาภายหลังในเมนูการตั้งค่าของบัญชีใหม่ ใช้ขั้นตอนเดียวกันนี้ใช้รายชื่อผู้ใช้ที่ถูกปิดเสียงและถูกบล็อก\n\n(คำอธิบายนี้ใช้กับ Misskey v13.12.0 ขึ้นไป, ซอฟต์แวร์ ActivityPub อื่นๆ เช่น Mastodon อาจทำงานแตกต่างออกไป)" moveAccountDescription: "การดำเนินการนี้จะย้ายบัญชีของคุณไปยังบัญชีอื่น\n・ผู้ที่กำลังติดตามคุณจากบัญชีนี้จะถูกย้ายไปยังบัญชีใหม่โดยอัตโนมัติ\n・บัญชีนี้จะเลิกติดตามผู้ใช้ทั้งหมดที่กำลังติดตามอยู่\n・คุณจะไม่สามารถสร้างโน้ต ฯลฯ ในบัญชีนี้ได้\n\nแม้ว่าการย้ายผู้ที่ติดตามคุณจะเป็นไปโดยอัตโนมัติ แต่คุณต้องเตรียมขั้นตอนบางอย่างด้วยตนเอง เพื่อย้ายรายชื่อผู้ใช้ที่คุณกำลังติดตาม โดยดำเนินการส่งออกรายชื่อแล้วค่อยนำเข้ามาภายหลังในเมนูการตั้งค่าของบัญชีใหม่ ใช้ขั้นตอนเดียวกันนี้ใช้รายชื่อผู้ใช้ที่ถูกปิดเสียงและถูกบล็อก\n\n(คำอธิบายนี้ใช้กับ Misskey v13.12.0 ขึ้นไป, ซอฟต์แวร์ ActivityPub อื่นๆ เช่น Mastodon อาจทำงานแตกต่างออกไป)"
moveAccountHowTo: "หากต้องการย้ายข้อมูลก่อนอื่นให้สร้างชื่อแทนสำหรับบัญชีนี้ ในบัญชีที่จะต้องการย้ายไป\nหลังจากที่คุณสร้างนามแฝงนั้นแล้ว ให้ป้อนบัญชีที่ต้องการจะย้ายไปในรูปแบบดังต่อไปนี้: @username@server.example.com" moveAccountHowTo: "การย้ายบัญชีจะเริ่มต้นโดยการสร้างบัญชีนามแฝง (alias) ของบัญชีนี้ ณ บัญชีที่เป็นปลายทาง หลังจากสร้างนามแฝงแล้ว ให้ป้อนบัญชีปลายทางในรูปแบบดังนี้: @username@server.example.com"
startMigration: "โอนย้าย" startMigration: "โอนย้าย"
migrationConfirm: "ยืนยันการย้ายข้อมูลบัญชีนี้ไปที่ {account} เมื่อเริ่มแล้วจะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี" migrationConfirm: "ยืนยันการย้ายข้อมูลบัญชีนี้ไปที่ {account} เมื่อเริ่มแล้วจะไม่สามารถหยุดหรือนำกลับคืนมาได้ และคุณจะไม่สามารถใช้บัญชีนี้ในสถานะดั้งเดิมได้อีกต่อไป\n\nนอกจากนี้ คุณจำเป็นต้องสร้างบัญชีสำรองสำหรับการย้ายบัญชี"
movedAndCannotBeUndone: "\nบัญชีนี้ถูกโอนย้ายไปแล้ว\nไม่สามารถย้อนกลับโอนย้ายข้อมูลได้" movedAndCannotBeUndone: "\nบัญชีนี้ถูกโอนย้ายไปแล้ว\nไม่สามารถยกเลิกการโอนย้ายได้"
postMigrationNote: "บัญชีนี้จะถูกเลิกติดตามบัญชีทั้งหมดที่กำลังติดตามภายใน 24 ชั่วโมงหลังจากการย้ายข้อมูลนั้นเสร็จสิ้น ทั้งจำนวนผู้ติดตามและผู้ติดตามนั้นจะกลายเป็นศูนย์ เพื่อหลีกเลี่ยงป้องกันไม่ให้ผู้ติดตามของคุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามของบัญชีนี้ได้ แต่อย่างไรก็ตามแล้วพวกเขาจะยังคงติดตามบัญชีนี้ต่อไป" postMigrationNote: "บัญชีนี้จะดำเนินการยกเลิกการติดตามทั้งหมดหลังจากการย้ายข้อมูลไปแล้ว 24 ชั่วโมง จำนวนกำลังติดตามและจำนวนผู้ติดตามของบัญชีนี้จะเป็น 0 และเพื่อหลีกเลี่ยงไม่ให้ผู้ติดตามคุณนั้นไม่สามารถเห็นโพสต์เฉพาะผู้ติดตามฯได้ การยกเลิกการติดตามจะไม่กระทบกับผู้ติดตามคุณ ดังนั้นผู้ติดตามคุณยังคงสามารถดูโพสต์ของบัญชีนี้ได้"
movedTo: "บัญชีที่จะย้ายไปที่:" movedTo: "บัญชีที่จะย้ายไป:"
_achievements: _achievements:
earnedAt: "ได้รับเมื่อ" earnedAt: "ได้รับเมื่อ"
_types: _types:
_notes1: _notes1:
title: "just setting up my msky" title: "just setting up my msky"
description: "โพสต์โน้ตแรกของคุณ" description: "โพสต์โน้ตเป็นครั้งแรก"
flavor: "ขอให้มีช่วงเวลาที่ดีกับ Misskey นะคะ!" flavor: "ขอให้มีช่วงเวลาที่ดีกับ Misskey นะคะ!"
_notes10: _notes10:
title: "โน้ตไม่กี่ชิ้น" title: "โน้ตไม่กี่ชิ้น"
@ -1506,19 +1506,19 @@ _achievements:
flavor: "ขอบคุณที่ใช้ Misskey นะ !" flavor: "ขอบคุณที่ใช้ Misskey นะ !"
_noteClipped1: _noteClipped1:
title: "อดไม่ได้ที่จะต้องคลิปมันเอาไว้" title: "อดไม่ได้ที่จะต้องคลิปมันเอาไว้"
description: "คลิปโน้ตตัวแรกของคุณ" description: "คลิปโน้ตเป็นครั้งแรก"
_noteFavorited1: _noteFavorited1:
title: "สตาร์เกเซอร์" title: "สตาร์เกเซอร์"
description: "ชื่นชอบโน้ตแรกของคุณ" description: "ใส่โน้ตเป็นรายการโปรดเป็นครั้งแรก"
_myNoteFavorited1: _myNoteFavorited1:
title: "แสวงหาดวงดาว" title: "แสวงหาดวงดาว"
description: "มีคนอื่นๆที่ชื่นชอบหนึ่งในโน้ตของคุณ" description: "โน้ตตัวเองถูกคนอื่นเพิ่มลงรายการโปรดของเขา"
_profileFilled: _profileFilled:
title: "เตรียมตัวอย่างดี" title: "เตรียมตัวอย่างดี"
description: "ตั้งค่าโปรไฟล์ของคุณ" description: "ตั้งค่าโปรไฟล์"
_markedAsCat: _markedAsCat:
title: "ฉันเป็นแมว" title: "ฉันเป็นแมว"
description: "ทำเครื่องหมายบัญชีของคุณว่าเป็นแมว" description: "ตั้งค่าบัญชีเป็นแมวเมี้ยวเมี้ยว"
flavor: "แมวน้อยไร้ชื่อ" flavor: "แมวน้อยไร้ชื่อ"
_following1: _following1:
title: "ก้าวแรกสู่...กดติดตาม" title: "ก้าวแรกสู่...กดติดตาม"
@ -1561,7 +1561,7 @@ _achievements:
description: "ได้รับความสำเร็จ 30 ครั้ง" description: "ได้รับความสำเร็จ 30 ครั้ง"
_viewAchievements3min: _viewAchievements3min:
title: "ชอบบรรลุความสําเร็จ" title: "ชอบบรรลุความสําเร็จ"
description: "มองดูรายการความสำเร็จของคุณเป็นเวลาอย่างน้อย 3 นาที" description: "มองดูรายการความสำเร็จเป็นเวลานานกว่า 3 นาที"
_iLoveMisskey: _iLoveMisskey:
title: "ฉันรัก Misskey" title: "ฉันรัก Misskey"
description: "โพสต์ “I ❤ #Misskey”" description: "โพสต์ “I ❤ #Misskey”"
@ -1588,13 +1588,13 @@ _achievements:
flavor: "โป๊ะ โป๊ะ โป๊ะ ปิ้งงงงง" flavor: "โป๊ะ โป๊ะ โป๊ะ ปิ้งงงงง"
_selfQuote: _selfQuote:
title: "อ้างอิงตนเอง" title: "อ้างอิงตนเอง"
description: "อ้างโน้ตของคุณเอง" description: "อ้างอิงโน้ตตัวเอง"
_htl20npm: _htl20npm:
title: "ไทม์ไลน์ไหล" title: "ไทม์ไลน์ไหล"
description: "มีการทำความเร็วของไทม์ไลน์หลักเกิน 20 npm (โน้ตต่อนาที)" description: "มีการทำความเร็วของไทม์ไลน์หลักเกิน 20 npm (โน้ตต่อนาที)"
_viewInstanceChart: _viewInstanceChart:
title: "วิเคราะห์" title: "วิเคราะห์"
description: "ดูแผนภูมิเซิร์ฟเวอร์ของคุณ" description: "ดูแผนภูมิของเซิร์ฟเวอร์"
_outputHelloWorldOnScratchpad: _outputHelloWorldOnScratchpad:
title: "หวัดดีชาวโลก!" title: "หวัดดีชาวโลก!"
description: "เอาพุต \"hello world\" ใน Scratchpad" description: "เอาพุต \"hello world\" ใน Scratchpad"
@ -1615,16 +1615,16 @@ _achievements:
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุก ๆ 10 วินาที" description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุก ๆ 10 วินาที"
_setNameToSyuilo: _setNameToSyuilo:
title: "คอมเพล็กซ์ของพระเจ้า" title: "คอมเพล็กซ์ของพระเจ้า"
description: "ตั้งชื่อของคุณเป็น “syuilo”" description: "ตั้งชื่อเป็น “syuilo”"
_passedSinceAccountCreated1: _passedSinceAccountCreated1:
title: "ครบรอบหนึ่งปี" title: "ครบรอบหนึ่งปี"
description: "ผ่านไปหนึ่งปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ" description: "ผ่านไป 1 ปีนับตั้งแต่สร้างบัญชี"
_passedSinceAccountCreated2: _passedSinceAccountCreated2:
title: "ครบรอบสองปี" title: "ครบรอบสองปี"
description: "ผ่านไปสองปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ" description: "ผ่านไป 2 ปีนับตั้งแต่สร้างบัญชี"
_passedSinceAccountCreated3: _passedSinceAccountCreated3:
title: "ครบรอบสามปี" title: "ครบรอบสามปี"
description: "ผ่านไปสามปีแล้วนะตั้งแต่บัญชีของคุณถูกสร้างขึ้นมาน่ะ" description: "ผ่านไป 3 ปีนับตั้งแต่สร้างบัญชี"
_loggedInOnBirthday: _loggedInOnBirthday:
title: "สุขสันต์วันเกิด" title: "สุขสันต์วันเกิด"
description: "เข้าสู่ระบบในวันเกิดของคุณ" description: "เข้าสู่ระบบในวันเกิดของคุณ"
@ -1672,8 +1672,8 @@ _role:
descriptionOfIsPublic: "บทบาทจะปรากฏบนโปรไฟล์ของผู้ใช้และเปิดเผยต่อสาธารณะ (ทุกคนสามารถเห็นได้ว่าผู้ใช้รายนี้มีบทบาทนี้)" descriptionOfIsPublic: "บทบาทจะปรากฏบนโปรไฟล์ของผู้ใช้และเปิดเผยต่อสาธารณะ (ทุกคนสามารถเห็นได้ว่าผู้ใช้รายนี้มีบทบาทนี้)"
options: "ตัวเลือกบทบาท" options: "ตัวเลือกบทบาท"
policies: "นโยบาย" policies: "นโยบาย"
baseRole: "เทมเพลตบทบาท" baseRole: "แม่แบบบทบาท"
useBaseValue: "ใช้ตามเทมเพลตบทบาท" useBaseValue: "ใช้ตามแม่แบบบทบาท"
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด" chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
iconUrl: "URL ไอคอน" iconUrl: "URL ไอคอน"
asBadge: "แสดงเป็นตรา" asBadge: "แสดงเป็นตรา"
@ -1797,7 +1797,7 @@ _plugin:
viewSource: "ดูต้นฉบับ" viewSource: "ดูต้นฉบับ"
viewLog: "แสดงปูม" viewLog: "แสดงปูม"
_preferencesBackups: _preferencesBackups:
list: "การตั้งค่าสำรองที่สร้างไว้" list: "การตั้งค่าที่สำรองไว้"
saveNew: "บันทึกการตั้งค่าสำรองใหม่" saveNew: "บันทึกการตั้งค่าสำรองใหม่"
loadFile: "โหลดจากไฟล์" loadFile: "โหลดจากไฟล์"
apply: "นำไปใช้กับอุปกรณ์นี้" apply: "นำไปใช้กับอุปกรณ์นี้"
@ -1864,7 +1864,7 @@ _menuDisplay:
hide: "ซ่อน" hide: "ซ่อน"
_wordMute: _wordMute:
muteWords: "ปิดเสียงคำ" muteWords: "ปิดเสียงคำ"
muteWordsDescription: "คั่นด้วยช่องว่างสำหรับเงื่อนไข AND หรือด้วยการขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR นะ" muteWordsDescription: "คั่นด้วยเว้นวรรคสำหรับเงื่อนไข AND, หรือขึ้นบรรทัดใหม่สำหรับเงื่อนไข OR"
muteWordsDescription2: "ล้อมรอบคีย์เวิร์ดด้วยเครื่องหมายทับเพื่อใช้นิพจน์ทั่วไป" muteWordsDescription2: "ล้อมรอบคีย์เวิร์ดด้วยเครื่องหมายทับเพื่อใช้นิพจน์ทั่วไป"
_instanceMute: _instanceMute:
instanceMuteDescription: "ปิดเสียง “โน้ต/รีโน้ต” ทั้งหมดจากเซิร์ฟเวอร์ที่ระบุไว้ รวมถึงโน้ตของผู้ใช้ที่ตอบกลับผู้ใช้จากเซิร์ฟเวอร์ที่ถูกปิดเสียง" instanceMuteDescription: "ปิดเสียง “โน้ต/รีโน้ต” ทั้งหมดจากเซิร์ฟเวอร์ที่ระบุไว้ รวมถึงโน้ตของผู้ใช้ที่ตอบกลับผู้ใช้จากเซิร์ฟเวอร์ที่ถูกปิดเสียง"
@ -2008,38 +2008,38 @@ _2fa:
backupCodesExhaustedWarning: "รหัสแบ๊กอัปทั้งหมดถูกใช้งานแล้ว หากยังไม่สามารถใช้แอปพลิเคชันการยืนยันตัวตนได้ก็จะไม่สามารถเข้าถึงบัญชีนี้ได้อีกต่อไป กรุณาลงทะเบียนแอปพลิเคชันการยืนยันตัวตนใหม่" backupCodesExhaustedWarning: "รหัสแบ๊กอัปทั้งหมดถูกใช้งานแล้ว หากยังไม่สามารถใช้แอปพลิเคชันการยืนยันตัวตนได้ก็จะไม่สามารถเข้าถึงบัญชีนี้ได้อีกต่อไป กรุณาลงทะเบียนแอปพลิเคชันการยืนยันตัวตนใหม่"
moreDetailedGuideHere: "คลิกที่นี่เพื่อดูคำแนะนำโดยละเอียด" moreDetailedGuideHere: "คลิกที่นี่เพื่อดูคำแนะนำโดยละเอียด"
_permissions: _permissions:
"read:account": "ดูข้อมูลบัญชีของคุณ" "read:account": "ดูข้อมูลบัญชี"
"write:account": "แก้ไขข้อมูลบัญชีของคุณ" "write:account": "แก้ไขข้อมูลบัญชี"
"read:blocks": "ดูรายชื่อผู้ใช้ที่ถูกบล็อกของคุณ" "read:blocks": "ดูรายชื่อผู้ใช้ที่ถูกบล็อก"
"write:blocks": "แก้ไขรายชื่อผู้ใช้ที่ถูกบล็อกของคุณ" "write:blocks": "แก้ไขรายชื่อผู้ใช้ที่ถูกบล็อก"
"read:drive": "เข้าถึงไฟล์และโฟลเดอร์ในไดรฟ์ของคุณ" "read:drive": "เข้าถึงไดรฟ์"
"write:drive": "แก้ไขหรือลบไฟล์และโฟลเดอร์ในไดรฟ์ของคุณ" "write:drive": "จัดการไดรฟ์"
"read:favorites": "ดูรายการโปรด" "read:favorites": "ดูรายการโปรด"
"write:favorites": "แก้ไขรายการโปรด" "write:favorites": "แก้ไขรายการโปรด"
"read:following": "ดูข้อมูลว่าใครที่คุณติดตาม" "read:following": "ดูข้อมูลว่าใครที่คุณติดตาม"
"write:following": "ติดตามหรือเลิกติดตามบัญชีอื่น" "write:following": "ติดตามหรือเลิกติดตามบัญชีอื่น"
"read:messaging": "ดูแชทของคุณ" "read:messaging": "ดูแชท"
"write:messaging": "เขียนหรือลบข้อความแชท" "write:messaging": "เขียนหรือลบข้อความแชท"
"read:mutes": "ดูรายชื่อผู้ใช้ที่ปิดเสียงของคุณ" "read:mutes": "ดูรายชื่อผู้ใช้ที่ถูกปิดเสียง"
"write:mutes": "แก้ไขรายชื่อผู้ใช้ที่ถูกปิดเสียง" "write:mutes": "แก้ไขรายชื่อผู้ใช้ที่ถูกปิดเสียง"
"write:notes": "เขียนหรือลบโน้ต" "write:notes": "เขียนหรือลบโน้ต"
"read:notifications": "ดูการแจ้งเตือนของคุณ" "read:notifications": "ดูการแจ้งเตือน"
"write:notifications": "จัดการแจ้งเตือนของคุณ" "write:notifications": "จัดการแจ้งเตือน"
"read:reactions": "ดูรีแอคชั่นของคุณ" "read:reactions": "ดูรีแอคชั่น"
"write:reactions": "แก้ไขรีแอคชั่นของคุณ" "write:reactions": "แก้ไขรีแอคชั่น"
"write:votes": "โหวตบนสำรวจความคิดเห็น" "write:votes": "โหวตบนสำรวจความคิดเห็น"
"read:pages": "ดูหน้าเพจ" "read:pages": "ดูหน้าเพจ"
"write:pages": "แก้ไขหรือลบเพจของคุณ" "write:pages": "แก้ไขหรือลบเพจ"
"read:page-likes": "ดูรายการเพจที่ถูกใจไว้" "read:page-likes": "ดูรายการเพจที่ถูกใจไว้"
"write:page-likes": "แก้ไขรายการเพจที่ถูกใจ" "write:page-likes": "แก้ไขรายการเพจที่ถูกใจ"
"read:user-groups": "ดูกลุ่มผู้ใช้ของคุณ" "read:user-groups": "ดูกลุ่มผู้ใช้"
"write:user-groups": "แก้ไขหรือลบกลุ่มผู้ใช้ของคุณ" "write:user-groups": "แก้ไขหรือลบกลุ่มผู้ใช้"
"read:channels": "ดูแชนแนลของคุณ" "read:channels": "ดูช่อง"
"write:channels": "แก้ไขแชนแนลของคุณ" "write:channels": "แก้ไขช่อง"
"read:gallery": "ดูแกลเลอรี่" "read:gallery": "ดูแกลเลอรี่"
"write:gallery": "แก้ไขแกลเลอรี่ของคุณ" "write:gallery": "แก้ไขแกลเลอรี"
"read:gallery-likes": "ดูรายการโพสต์แกลเลอรีที่ถูกใจไว้" "read:gallery-likes": "ดูแกลเลอรีที่ถูกใจไว้"
"write:gallery-likes": "แก้ไขรายการโพสต์แกลเลอรีที่ถูกใจไว้" "write:gallery-likes": "จัดการแกลเลอรีที่ถูกใจไว้"
"read:flash": "ดู Play" "read:flash": "ดู Play"
"write:flash": "แก้ไข Play" "write:flash": "แก้ไข Play"
"read:flash-likes": "ดูรายการ play ที่ถูกใจไว้" "read:flash-likes": "ดูรายการ play ที่ถูกใจไว้"
@ -2048,20 +2048,20 @@ _permissions:
"write:admin:delete-account": "ลบบัญชีผู้ใช้" "write:admin:delete-account": "ลบบัญชีผู้ใช้"
"write:admin:delete-all-files-of-a-user": "ลบไฟล์ทั้งหมดของผู้ใช้" "write:admin:delete-all-files-of-a-user": "ลบไฟล์ทั้งหมดของผู้ใช้"
"read:admin:index-stats": "ดูข้อมูลเกี่ยวกับดัชนีฐานข้อมูล" "read:admin:index-stats": "ดูข้อมูลเกี่ยวกับดัชนีฐานข้อมูล"
"read:admin:table-stats": "ดูข้อมูลเกี่ยวกับตารางฐานข้อมูล" "read:admin:table-stats": "ดูข้อมูลเกี่ยวกับตารางในฐานข้อมูล"
"read:admin:user-ips": "ดูที่อยู่ IP ของผู้ใช้" "read:admin:user-ips": "ดูที่อยู่ IP ของผู้ใช้"
"read:admin:meta": "ดูข้อมูลเมตาของอินสแตนซ์" "read:admin:meta": "ดูข้อมูลอภิพันธุ์ของอินสแตนซ์"
"write:admin:reset-password": "รีเซ็ตรหัสผ่านของผู้ใช้" "write:admin:reset-password": "รีเซ็ตรหัสผ่านของผู้ใช้"
"write:admin:resolve-abuse-user-report": "แก้ไขรายงานจากผู้ใช้" "write:admin:resolve-abuse-user-report": "แก้ไขรายงานจากผู้ใช้"
"write:admin:send-email": "ส่งอีเมล" "write:admin:send-email": "ส่งอีเมล"
"read:admin:server-info": "ดูข้อมูลเซิร์ฟเวอร์" "read:admin:server-info": "ดูข้อมูลเซิร์ฟเวอร์"
"read:admin:show-moderation-log": "ดูปูมการแก้ไข" "read:admin:show-moderation-log": "ดูปูมการควบคุมดูแล"
"read:admin:show-user": "ดูข้อมูลส่วนตัวของผู้ใช้" "read:admin:show-user": "ดูข้อมูลส่วนตัวของผู้ใช้"
"write:admin:suspend-user": "ระงับผู้ใช้" "write:admin:suspend-user": "ระงับผู้ใช้"
"write:admin:unset-user-avatar": "ลบอวตารผู้ใช้" "write:admin:unset-user-avatar": "ลบอวตารผู้ใช้"
"write:admin:unset-user-banner": "ลบแบนเนอร์ผู้ใช้" "write:admin:unset-user-banner": "ลบแบนเนอร์ผู้ใช้"
"write:admin:unsuspend-user": "ยกเลิกการระงับผู้ใช้" "write:admin:unsuspend-user": "ยกเลิกการระงับผู้ใช้"
"write:admin:meta": "จัดการข้อมูลเมตาของอินสแตนซ์" "write:admin:meta": "จัดการข้อมูลอภิพันธุ์ของอินสแตนซ์"
"write:admin:user-note": "จัดการโน้ตการกลั่นกรอง" "write:admin:user-note": "จัดการโน้ตการกลั่นกรอง"
"write:admin:roles": "จัดการบทบาท" "write:admin:roles": "จัดการบทบาท"
"read:admin:roles": "ดูบทบาท" "read:admin:roles": "ดูบทบาท"
@ -2088,8 +2088,8 @@ _permissions:
"read:admin:ad": "ดูโฆษณา" "read:admin:ad": "ดูโฆษณา"
"write:invite-codes": "สร้างรหัสเชิญ" "write:invite-codes": "สร้างรหัสเชิญ"
"read:invite-codes": "รับรหัสเชิญ" "read:invite-codes": "รับรหัสเชิญ"
"write:clip-favorite": "ควบคุมการถูกใจของคลิป" "write:clip-favorite": "จัดการคลิปที่ถูกใจ"
"read:clip-favorite": "ดูการถูกใจของคลิป" "read:clip-favorite": "ดูคลิปที่ถูกใจ"
"read:federation": "รับข้อมูลเกี่ยวกับสหพันธ์" "read:federation": "รับข้อมูลเกี่ยวกับสหพันธ์"
"write:report-abuse": "รายงานการละเมิด" "write:report-abuse": "รายงานการละเมิด"
_auth: _auth:
@ -2165,7 +2165,7 @@ _poll:
deadlineTime: "เวลา" deadlineTime: "เวลา"
duration: "ระยะเวลา" duration: "ระยะเวลา"
votesCount: "{n} คะแนนเสียง" votesCount: "{n} คะแนนเสียง"
totalVotes: "{n} คะแนนเสียงทั้งหมด" totalVotes: "ทั้งหมด {n} คะแนนเสียง"
vote: "โหวต" vote: "โหวต"
showResult: "ดูผลลัพธ์" showResult: "ดูผลลัพธ์"
voted: "โหวตแล้ว" voted: "โหวตแล้ว"
@ -2266,7 +2266,7 @@ _play:
featured: "เป็นที่นิยม" featured: "เป็นที่นิยม"
title: "หัวข้อ" title: "หัวข้อ"
script: "สคริปต์" script: "สคริปต์"
summary: "รายละเอียด" summary: "คำอธิบาย"
visibilityDescription: "หากตั้งค่าเป็นส่วนตัว มันจะไม่ปรากฏในโปรไฟล์อีกต่อไป แต่ผู้ที่ทราบ URL ของมันจะยังสามารถเข้าถึงได้" visibilityDescription: "หากตั้งค่าเป็นส่วนตัว มันจะไม่ปรากฏในโปรไฟล์อีกต่อไป แต่ผู้ที่ทราบ URL ของมันจะยังสามารถเข้าถึงได้"
_pages: _pages:
newPage: "สร้างหน้าเพจใหม่" newPage: "สร้างหน้าเพจใหม่"
@ -2425,6 +2425,7 @@ _webhookSettings:
_systemEvents: _systemEvents:
abuseReport: "เมื่อมีการรายงานจากผู้ใช้" abuseReport: "เมื่อมีการรายงานจากผู้ใช้"
abuseReportResolved: "เมื่อมีการจัดการกับการรายงานจากผู้ใช้" abuseReportResolved: "เมื่อมีการจัดการกับการรายงานจากผู้ใช้"
userCreated: "เมื่อผู้ใช้ถูกสร้างขึ้น"
deleteConfirm: "ต้องการลบ Webhook ใช่ไหม?" deleteConfirm: "ต้องการลบ Webhook ใช่ไหม?"
_abuseReport: _abuseReport:
_notificationRecipient: _notificationRecipient:

View File

@ -60,6 +60,7 @@ copyFileId: "复制文件ID"
copyFolderId: "复制文件夹ID" copyFolderId: "复制文件夹ID"
copyProfileUrl: "复制个人资料URL" copyProfileUrl: "复制个人资料URL"
searchUser: "搜索用户" searchUser: "搜索用户"
searchThisUsersNotes: "搜索用户帖子"
reply: "回复" reply: "回复"
loadMore: "查看更多" loadMore: "查看更多"
showMore: "查看更多" showMore: "查看更多"
@ -154,6 +155,7 @@ editList: "编辑列表"
selectChannel: "选择频道" selectChannel: "选择频道"
selectAntenna: "选择天线" selectAntenna: "选择天线"
editAntenna: "编辑天线" editAntenna: "编辑天线"
createAntenna: "创建天线"
selectWidget: "选择小工具" selectWidget: "选择小工具"
editWidgets: "编辑部件" editWidgets: "编辑部件"
editWidgetsExit: "完成编辑" editWidgetsExit: "完成编辑"
@ -194,6 +196,7 @@ followConfirm: "你确定要关注 {name} 吗?"
proxyAccount: "代理账户" proxyAccount: "代理账户"
proxyAccountDescription: "代理账户是在某些情况下替代用户进行远程关注用的账户。 例如说,当用户将一位远程用户放入一个列表中时,如果本地服务器上没有任何人关注这位远程用户,则这位远程用户的账户活动将不会被送到本地服务器上。作为替代,此时将使用代理账户进行关注。" proxyAccountDescription: "代理账户是在某些情况下替代用户进行远程关注用的账户。 例如说,当用户将一位远程用户放入一个列表中时,如果本地服务器上没有任何人关注这位远程用户,则这位远程用户的账户活动将不会被送到本地服务器上。作为替代,此时将使用代理账户进行关注。"
host: "主机名" host: "主机名"
selectSelf: "选择自己"
selectUser: "选择用户" selectUser: "选择用户"
recipient: "收件人" recipient: "收件人"
annotation: "注解" annotation: "注解"
@ -1106,6 +1109,8 @@ preservedUsernames: "保留的用户名"
preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。" preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。"
createNoteFromTheFile: "从文件创建帖子" createNoteFromTheFile: "从文件创建帖子"
archive: "归档" archive: "归档"
archived: "已归档"
unarchive: "取消归档"
channelArchiveConfirmTitle: "要将 {name} 归档吗?" channelArchiveConfirmTitle: "要将 {name} 归档吗?"
channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。" channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。"
thisChannelArchived: "该频道已被归档。" thisChannelArchived: "该频道已被归档。"
@ -1116,6 +1121,7 @@ preventAiLearning: "拒绝接受生成式 AI 的学习"
preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。" preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。"
options: "选项" options: "选项"
specifyUser: "用户指定" specifyUser: "用户指定"
specifyHost: "指定主机名"
failedToPreviewUrl: "无法预览" failedToPreviewUrl: "无法预览"
update: "更新" update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "可以使用表情作为回应的角色" rolesThatCanBeUsedThisEmojiAsReaction: "可以使用表情作为回应的角色"
@ -1250,6 +1256,8 @@ inquiry: "联系我们"
tryAgain: "请再试一次" tryAgain: "请再试一次"
confirmWhenRevealingSensitiveMedia: "显示敏感内容前需要确认" confirmWhenRevealingSensitiveMedia: "显示敏感内容前需要确认"
sensitiveMediaRevealConfirm: "这是敏感内容。是否显示?" sensitiveMediaRevealConfirm: "这是敏感内容。是否显示?"
createdLists: "已创建的列表"
createdAntennas: "已创建的天线"
_delivery: _delivery:
status: "投递状态" status: "投递状态"
stop: "停止投递" stop: "停止投递"
@ -2424,6 +2432,7 @@ _webhookSettings:
_systemEvents: _systemEvents:
abuseReport: "当收到举报时" abuseReport: "当收到举报时"
abuseReportResolved: "当举报被处理时" abuseReportResolved: "当举报被处理时"
userCreated: "当用户被创建时"
deleteConfirm: "要删除 webhook 吗?" deleteConfirm: "要删除 webhook 吗?"
_abuseReport: _abuseReport:
_notificationRecipient: _notificationRecipient:
@ -2614,3 +2623,8 @@ _mediaControls:
pip: "画中画" pip: "画中画"
playbackRate: "播放速度" playbackRate: "播放速度"
loop: "循环播放" loop: "循环播放"
_contextMenu:
title: "上下文菜单"
app: "应用"
appWithShift: "Shift 键应用"
native: "浏览器的用户界面"

View File

@ -60,6 +60,7 @@ copyFileId: "複製檔案 ID"
copyFolderId: "複製資料夾ID" copyFolderId: "複製資料夾ID"
copyProfileUrl: "複製個人資料網址" copyProfileUrl: "複製個人資料網址"
searchUser: "搜尋使用者" searchUser: "搜尋使用者"
searchThisUsersNotes: "搜尋這個使用者的貼文"
reply: "回覆" reply: "回覆"
loadMore: "載入更多" loadMore: "載入更多"
showMore: "載入更多" showMore: "載入更多"
@ -154,6 +155,7 @@ editList: "編輯清單"
selectChannel: "選擇頻道" selectChannel: "選擇頻道"
selectAntenna: "選擇天線" selectAntenna: "選擇天線"
editAntenna: "編輯天線" editAntenna: "編輯天線"
createAntenna: "建立天線"
selectWidget: "選擇小工具" selectWidget: "選擇小工具"
editWidgets: "編輯小工具" editWidgets: "編輯小工具"
editWidgetsExit: "完成" editWidgetsExit: "完成"
@ -194,6 +196,7 @@ followConfirm: "你真的要追隨{name}嗎?"
proxyAccount: "代理帳戶" proxyAccount: "代理帳戶"
proxyAccountDescription: "代理帳戶是在特定條件下充當遠端追隨者的帳戶。例如,當使用者新增遠端使用者至其列表時,若沒有本地使用者追隨該遠端使用者,則其活動將不會傳送至伺服器,此時便會由代理帳戶代為追隨以解決問題。" proxyAccountDescription: "代理帳戶是在特定條件下充當遠端追隨者的帳戶。例如,當使用者新增遠端使用者至其列表時,若沒有本地使用者追隨該遠端使用者,則其活動將不會傳送至伺服器,此時便會由代理帳戶代為追隨以解決問題。"
host: "主機" host: "主機"
selectSelf: "選擇自己"
selectUser: "選取使用者" selectUser: "選取使用者"
recipient: "收件人" recipient: "收件人"
annotation: "註解" annotation: "註解"
@ -1106,6 +1109,8 @@ preservedUsernames: "保留的使用者名稱"
preservedUsernamesDescription: "換行列舉要保留的使用者名稱。此處出現的名稱將在註冊時禁用,但由管理者建立帳戶則不受此限。此外,既有的帳戶也不受影響。" preservedUsernamesDescription: "換行列舉要保留的使用者名稱。此處出現的名稱將在註冊時禁用,但由管理者建立帳戶則不受此限。此外,既有的帳戶也不受影響。"
createNoteFromTheFile: "由此檔案建立貼文" createNoteFromTheFile: "由此檔案建立貼文"
archive: "封存" archive: "封存"
archived: "已封存"
unarchive: "取消封存"
channelArchiveConfirmTitle: "要封存{name}嗎?" channelArchiveConfirmTitle: "要封存{name}嗎?"
channelArchiveConfirmDescription: "封存後,將不會在頻道列表與搜尋結果中顯示,也無法發佈新貼文。" channelArchiveConfirmDescription: "封存後,將不會在頻道列表與搜尋結果中顯示,也無法發佈新貼文。"
thisChannelArchived: "這個頻道已被封存。" thisChannelArchived: "這個頻道已被封存。"
@ -1116,6 +1121,7 @@ preventAiLearning: "拒絕接受生成式AI的訓練"
preventAiLearningDescription: "要求站外生成式 AI 不使用您發佈的內容訓練模型。此功能會使伺服器於 HTML 回應新增「noai」標籤而因為要視乎 AI 會否遵守該標籤,所以此功能無法完全阻止所有 AI 使用您的內容。" preventAiLearningDescription: "要求站外生成式 AI 不使用您發佈的內容訓練模型。此功能會使伺服器於 HTML 回應新增「noai」標籤而因為要視乎 AI 會否遵守該標籤,所以此功能無法完全阻止所有 AI 使用您的內容。"
options: "選項" options: "選項"
specifyUser: "指定使用者" specifyUser: "指定使用者"
specifyHost: "指定主機"
failedToPreviewUrl: "無法預覽" failedToPreviewUrl: "無法預覽"
update: "更新" update: "更新"
rolesThatCanBeUsedThisEmojiAsReaction: "可以使用此表情符號為反應的角色" rolesThatCanBeUsedThisEmojiAsReaction: "可以使用此表情符號為反應的角色"
@ -1250,6 +1256,8 @@ inquiry: "聯絡我們"
tryAgain: "請再試一次。" tryAgain: "請再試一次。"
confirmWhenRevealingSensitiveMedia: "要顯示敏感媒體時需確認" confirmWhenRevealingSensitiveMedia: "要顯示敏感媒體時需確認"
sensitiveMediaRevealConfirm: "這是敏感媒體。確定要顯示嗎?" sensitiveMediaRevealConfirm: "這是敏感媒體。確定要顯示嗎?"
createdLists: "已建立的清單"
createdAntennas: "已建立的天線"
_delivery: _delivery:
status: "傳送狀態" status: "傳送狀態"
stop: "停止發送" stop: "停止發送"
@ -2615,3 +2623,7 @@ _mediaControls:
pip: "畫中畫" pip: "畫中畫"
playbackRate: "播放速度" playbackRate: "播放速度"
loop: "循環播放" loop: "循環播放"
_contextMenu:
title: "內容功能表"
app: "應用程式"
native: "瀏覽器的使用者介面"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.7.0-rc.4", "version": "2024.7.0-rc.5",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -154,7 +154,7 @@ export function federationInstance(): entities.FederationInstance {
}; };
} }
export function userDetailed(id = 'someuserid', username = 'miskist', host = 'misskey-hub.net', name = 'Misskey User'): entities.UserDetailed { export function userDetailed(id = 'someuserid', username = 'miskist', host:entities.UserDetailed['host'] = 'misskey-hub.net', name:entities.UserDetailed['name'] = 'Misskey User'): entities.UserDetailed {
return { return {
id, id,
username, username,

View File

@ -405,6 +405,7 @@ function toStories(component: string): Promise<string> {
glob('src/components/MkUserSetupDialog.*.vue'), glob('src/components/MkUserSetupDialog.*.vue'),
glob('src/components/MkInstanceCardMini.vue'), glob('src/components/MkInstanceCardMini.vue'),
glob('src/components/MkInviteCode.vue'), glob('src/components/MkInviteCode.vue'),
glob('src/pages/search.vue'),
glob('src/pages/user/home.vue'), glob('src/pages/user/home.vue'),
]); ]);
const components = globs.flat(); const components = globs.flat();

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

@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.actions"> <div :class="$style.actions">
<div class="_buttons"> <div class="_buttons">
<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> <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> <MkButton v-if="initialAntenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div> </div>
</div> </div>
</div> </div>
@ -61,28 +61,53 @@ import MkSwitch from '@/components/MkSwitch.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.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<{ const props = defineProps<{
antenna: Misskey.entities.Antenna antenna?: DeepPartial<PartialAllowedAntenna>;
}>(); }>();
const initialAntenna = deepMerge<PartialAllowedAntenna>(props.antenna ?? {}, {
name: '',
src: 'all',
userListId: null,
users: [],
keywords: [],
excludeKeywords: [],
excludeBots: false,
withReplies: false,
caseSensitive: false,
localOnly: false,
withFile: false,
isActive: true,
hasUnreadNote: false,
notify: false,
});
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'created'): void, (ev: 'created', newAntenna: Misskey.entities.Antenna): void,
(ev: 'updated'): void, (ev: 'updated', editedAntenna: Misskey.entities.Antenna): void,
(ev: 'deleted'): void, (ev: 'deleted'): void,
}>(); }>();
const name = ref<string>(props.antenna.name); const name = ref<string>(initialAntenna.name);
const src = ref<Misskey.entities.AntennasCreateRequest['src']>(props.antenna.src); const src = ref<Misskey.entities.AntennasCreateRequest['src']>(initialAntenna.src);
const userListId = ref<string | null>(props.antenna.userListId); const userListId = ref<string | null>(initialAntenna.userListId);
const users = ref<string>(props.antenna.users.join('\n')); const users = ref<string>(initialAntenna.users.join('\n'));
const keywords = ref<string>(props.antenna.keywords.map(x => x.join(' ')).join('\n')); const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n'));
const excludeKeywords = ref<string>(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n')); const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n'));
const caseSensitive = ref<boolean>(props.antenna.caseSensitive); const caseSensitive = ref<boolean>(initialAntenna.caseSensitive);
const localOnly = ref<boolean>(props.antenna.localOnly); const localOnly = ref<boolean>(initialAntenna.localOnly);
const excludeBots = ref<boolean>(props.antenna.excludeBots); const excludeBots = ref<boolean>(initialAntenna.excludeBots);
const withReplies = ref<boolean>(props.antenna.withReplies); const withReplies = ref<boolean>(initialAntenna.withReplies);
const withFile = ref<boolean>(props.antenna.withFile); const withFile = ref<boolean>(initialAntenna.withFile);
const userLists = ref<Misskey.entities.UserList[] | null>(null); const userLists = ref<Misskey.entities.UserList[] | null>(null);
watch(() => src.value, async () => { watch(() => src.value, async () => {
@ -106,24 +131,26 @@ async function saveAntenna() {
excludeKeywords: excludeKeywords.value.trim().split('\n').map(x => x.trim().split(' ')), excludeKeywords: excludeKeywords.value.trim().split('\n').map(x => x.trim().split(' ')),
}; };
if (props.antenna.id == null) { if (initialAntenna.id == null) {
await os.apiWithDialog('antennas/create', antennaData); const res = await os.apiWithDialog('antennas/create', antennaData);
emit('created'); emit('created', res);
} else { } else {
await os.apiWithDialog('antennas/update', { ...antennaData, antennaId: props.antenna.id }); const res = await os.apiWithDialog('antennas/update', { ...antennaData, antennaId: initialAntenna.id });
emit('updated'); emit('updated', res);
} }
} }
async function deleteAntenna() { async function deleteAntenna() {
if (initialAntenna.id == null) return;
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: props.antenna.name }), text: i18n.tsx.removeAreYouSure({ x: initialAntenna.name }),
}); });
if (canceled) return; if (canceled) return;
await misskeyApi('antennas/delete', { await misskeyApi('antennas/delete', {
antennaId: props.antenna.id, antennaId: initialAntenna.id,
}); });
os.success(); 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 'misskey-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: { args: {
clip: clip(), clip: clip(),
noUserInfo: false,
}, },
parameters: { parameters: {
layout: 'fullscreen', 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.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 v-if="clip.notesCount != null">{{ i18n.ts.notesCount }}: {{ number(clip.notesCount) }} / {{ $i?.policies.noteEachClipsLimit }} ({{ i18n.tsx.remainingN({ n: remaining }) }})</div>
</div> </div>
<template v-if="!props.noUserInfo">
<div :class="$style.divider"></div> <div :class="$style.divider"></div>
<div> <div>
<MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/> <MkAvatar :user="clip.user" :class="$style.userAvatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
</div> </div>
</template>
</div> </div>
</MkA> </MkA>
</template> </template>
@ -27,9 +29,12 @@ import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import number from '@/filters/number.js'; import number from '@/filters/number.js';
const props = defineProps<{ const props = withDefaults(defineProps<{
clip: Misskey.entities.Clip; clip: Misskey.entities.Clip;
}>(); noUserInfo?: boolean;
}>(), {
noUserInfo: false,
});
const remaining = computed(() => { const remaining = computed(() => {
return ($i?.policies && props.clip.notesCount != null) ? ($i.policies.noteEachClipsLimit - props.clip.notesCount) : i18n.ts.unknown; return ($i?.policies && props.clip.notesCount != null) ? ($i.policies.noteEachClipsLimit - props.clip.notesCount) : i18n.ts.unknown;

View File

@ -36,7 +36,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput> </MkInput>
<MkSelect v-if="select" v-model="selectedValue" autofocus> <MkSelect v-if="select" v-model="selectedValue" autofocus>
<template v-if="select.items"> <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> </template>
</MkSelect> </MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons"> <div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
@ -67,11 +72,16 @@ type Input = {
maxLength?: number; maxLength?: number;
}; };
type Select = { type SelectItem = {
items: {
value: any; value: any;
text: string; text: string;
}[]; };
type Select = {
items: (SelectItem | {
sectionTitle: string;
items: SelectItem[];
})[];
default: string | null; default: string | null;
}; };

View File

@ -27,7 +27,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload"> <p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload">
{{ i18n.ts.uploadFolder }} {{ i18n.ts.uploadFolder }}
</p> </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> </div>
</template> </template>
@ -53,6 +55,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'chosen', v: Misskey.entities.DriveFolder): void; (ev: 'chosen', v: Misskey.entities.DriveFolder): void;
(ev: 'unchose', v: Misskey.entities.DriveFolder): void;
(ev: 'move', v: Misskey.entities.DriveFolder): void; (ev: 'move', v: Misskey.entities.DriveFolder): void;
(ev: 'upload', file: File, folder: Misskey.entities.DriveFolder); (ev: 'upload', file: File, folder: Misskey.entities.DriveFolder);
(ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void; (ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
@ -68,8 +71,12 @@ const isDragging = ref(false);
const title = computed(() => props.folder.name); const title = computed(() => props.folder.name);
function checkboxClicked() { function checkboxClicked() {
if (props.isSelected) {
emit('unchose', props.folder);
} else {
emit('chosen', props.folder); emit('chosen', props.folder);
} }
}
function onClick() { function onClick() {
emit('move', props.folder); emit('move', props.folder);
@ -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() { function deleteFolder() {
misskeyApi('drive/folders/delete', { misskeyApi('drive/folders/delete', {
folderId: props.folder.id, folderId: props.folder.id,
@ -267,6 +285,10 @@ function onContextmenu(ev: MouseEvent) {
text: i18n.ts.rename, text: i18n.ts.rename,
icon: 'ti ti-forms', icon: 'ti ti-forms',
action: rename, action: rename,
}, {
text: i18n.ts.move,
icon: 'ti ti ti-folder-symlink',
action: move,
}, { type: 'divider' }, { }, { type: 'divider' }, {
text: i18n.ts.delete, text: i18n.ts.delete,
icon: 'ti ti-trash', icon: 'ti ti-trash',
@ -310,17 +332,43 @@ function onContextmenu(ev: MouseEvent) {
} }
} }
.checkbox { .checkboxWrapper {
position: absolute; position: absolute;
bottom: 8px; border-radius: 50%;
right: 8px; bottom: 2px;
width: 16px; right: 2px;
height: 16px; padding: 8px;
box-sizing: border-box;
> .checkbox {
position: relative;
width: 18px;
height: 18px;
background: #fff; background: #fff;
border: solid 1px #000; border: solid 2px var(--divider);
border-radius: 4px;
box-sizing: border-box;
&.checked { &.checked {
border-color: var(--accent);
background: 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

@ -52,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:selectMode="select === 'folder'" :selectMode="select === 'folder'"
:isSelected="selectedFolders.some(x => x.id === f.id)" :isSelected="selectedFolders.some(x => x.id === f.id)"
@chosen="chooseFolder" @chosen="chooseFolder"
@unchose="unchoseFolder"
@move="move" @move="move"
@upload="upload" @upload="upload"
@removeFile="removeFile" @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']) { function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) {
if (!target) { if (!target) {
goRoot(); goRoot();

View File

@ -259,7 +259,7 @@ const canPost = computed((): boolean => {
1 <= files.value.length || 1 <= files.value.length ||
poll.value != null || poll.value != null ||
props.renote != null || props.renote != null ||
(props.reply != null && quoteId.value != null) quoteId.value != null
) && ) &&
(textLength.value <= maxTextLength.value) && (textLength.value <= maxTextLength.value) &&
(!poll.value || poll.value.choices.length >= 2); (!poll.value || poll.value.choices.length >= 2);
@ -906,10 +906,23 @@ async function insertEmoji(ev: MouseEvent) {
textAreaReadOnly.value = true; textAreaReadOnly.value = true;
const target = ev.currentTarget ?? ev.target; const target = ev.currentTarget ?? ev.target;
if (target == null) return; if (target == null) return;
// emojiPickertextarea
// focustrapinsertTextAtCursor
// 稿
// See: https://github.com/misskey-dev/misskey/pull/14282
// https://github.com/misskey-dev/misskey/issues/14274
let pos = textareaEl.value?.selectionStart ?? 0;
let posEnd = textareaEl.value?.selectionEnd ?? text.value.length;
emojiPicker.show( emojiPicker.show(
target as HTMLElement, target as HTMLElement,
emoji => { emoji => {
insertTextAtCursor(textareaEl.value, emoji); const textBefore = text.value.substring(0, pos);
const textAfter = text.value.substring(posEnd);
text.value = textBefore + emoji + textAfter;
pos += emoji.length;
posEnd += emoji.length;
}, },
() => { () => {
textAreaReadOnly.value = false; textAreaReadOnly.value = false;

View File

@ -29,6 +29,9 @@ export default defineComponent({
// Fragment // Fragment
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[]; if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
// vnodev-if=false(trueoptiontype)
options = options.filter(vnode => !(typeof vnode.type === 'symbol' && vnode.type.description === 'v-cmt' && vnode.children === 'v-if'));
return () => h('div', { return () => h('div', {
class: 'novjtcto', class: 'novjtcto',
}, [ }, [
@ -40,6 +43,7 @@ export default defineComponent({
}, options.map(option => h(MkRadio, { }, options.map(option => h(MkRadio, {
key: option.key as string, key: option.key as string,
value: option.props?.value, value: option.props?.value,
disabled: option.props?.disabled,
modelValue: value.value, modelValue: value.value,
'onUpdate:modelValue': _v => value.value = _v, 'onUpdate:modelValue': _v => value.value = _v,
}, () => option.children)), }, () => option.children)),

View File

@ -230,6 +230,7 @@ onMounted(async () => {
left: 0; left: 0;
padding: 12px; padding: 12px;
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
background: var(--acrylicBg);
-webkit-backdrop-filter: var(--blur, blur(15px)); -webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px));
} }

View File

@ -11,6 +11,7 @@ import * as Misskey from 'misskey-js';
import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/scripts/form.js'; import type { Form, GetFormResultType } from '@/scripts/form.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkPostFormDialog from '@/components/MkPostFormDialog.vue'; import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
import MkWaitingDialog from '@/components/MkWaitingDialog.vue'; import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
@ -447,15 +448,20 @@ export function authenticateDialog(): Promise<{
}); });
} }
type SelectItem<C> = {
value: C;
text: string;
};
// default が指定されていたら result は null になり得ないことを保証する overload function // default が指定されていたら result は null になり得ないことを保証する overload function
export function select<C = any>(props: { export function select<C = any>(props: {
title?: string; title?: string;
text?: string; text?: string;
default: string; default: string;
items: { items: (SelectItem<C> | {
value: C; sectionTitle: string;
text: string; items: SelectItem<C>[];
}[]; } | undefined)[];
}): Promise<{ }): Promise<{
canceled: true; result: undefined; canceled: true; result: undefined;
} | { } | {
@ -465,10 +471,10 @@ export function select<C = any>(props: {
title?: string; title?: string;
text?: string; text?: string;
default?: string | null; default?: string | null;
items: { items: (SelectItem<C> | {
value: C; sectionTitle: string;
text: string; items: SelectItem<C>[];
}[]; } | undefined)[];
}): Promise<{ }): Promise<{
canceled: true; result: undefined; canceled: true; result: undefined;
} | { } | {
@ -478,10 +484,10 @@ export function select<C = any>(props: {
title?: string; title?: string;
text?: string; text?: string;
default?: string | null; default?: string | null;
items: { items: (SelectItem<C> | {
value: C; sectionTitle: string;
text: string; items: SelectItem<C>[];
}[]; } | undefined)[];
}): Promise<{ }): Promise<{
canceled: true; result: undefined; canceled: true; result: undefined;
} | { } | {
@ -492,7 +498,7 @@ export function select<C = any>(props: {
title: props.title, title: props.title,
text: props.text, text: props.text,
select: { select: {
items: props.items, items: props.items.filter(x => x !== undefined),
default: props.default ?? null, default: props.default ?? null,
}, },
}, { }, {
@ -649,6 +655,13 @@ export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | n
} }
export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> { export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
if (
defaultStore.state.contextMenu === 'native' ||
(defaultStore.state.contextMenu === 'appWithShift' && !ev.shiftKey)
) {
return Promise.resolve();
}
let returnFocusTo = getHTMLElementOrNull(ev.currentTarget ?? ev.target) ?? getHTMLElementOrNull(document.activeElement); let returnFocusTo = getHTMLElementOrNull(ev.currentTarget ?? ev.target) ?? getHTMLElementOrNull(document.activeElement);
ev.preventDefault(); ev.preventDefault();
return new Promise(resolve => nextTick(() => { return new Promise(resolve => nextTick(() => {

View File

@ -71,9 +71,9 @@ const pagination = {
sort: sort.value, sort: sort.value,
host: host.value !== '' ? host.value : null, host: host.value !== '' ? host.value : null,
...( ...(
state.value === 'federating' ? { federating: true } : state.value === 'federating' ? { federating: true, suspended: false, blocked: false } :
state.value === 'subscribing' ? { subscribing: true } : state.value === 'subscribing' ? { subscribing: true, suspended: false, blocked: false } :
state.value === 'publishing' ? { publishing: true } : state.value === 'publishing' ? { publishing: true, suspended: false, blocked: false } :
state.value === 'suspended' ? { suspended: true } : state.value === 'suspended' ? { suspended: true } :
state.value === 'blocked' ? { blocked: true } : state.value === 'blocked' ? { blocked: true } :
state.value === 'silenced' ? { silenced: true } : state.value === 'silenced' ? { silenced: true } :

View File

@ -24,8 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="buttons right"> <div class="buttons right">
<template v-if="actions"> <template v-if="actions">
<template v-for="action in actions"> <template v-for="action in actions">
<MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton> <MkButton v-if="action.asFullButton" class="fullButton" primary :disabled="action.disabled" @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
<button v-else v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button> <button v-else v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" :disabled="action.disabled" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
</template> </template>
</template> </template>
</div> </div>
@ -56,6 +56,7 @@ const props = defineProps<{
text: string; text: string;
icon: string; icon: string;
asFullButton?: boolean; asFullButton?: boolean;
disabled?: boolean;
handler: (ev: MouseEvent) => void; handler: (ev: MouseEvent) => void;
}[]; }[];
thin?: boolean; thin?: boolean;

View File

@ -11,6 +11,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo> <MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo>
<MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo> <MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo>
<MkSelect v-model="announcementsStatus">
<template #label>{{ i18n.ts.filter }}</template>
<option value="active">{{ i18n.ts.active }}</option>
<option value="archived">{{ i18n.ts.archived }}</option>
</MkSelect>
<MkLoading v-if="loading"/>
<template v-else>
<MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null"> <MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null">
<template #label>{{ announcement.title }}</template> <template #label>{{ announcement.title }}</template>
<template #icon> <template #icon>
@ -57,24 +66,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p> <p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p>
<div class="buttons _buttons"> <div class="buttons _buttons">
<MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> <MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="announcement.id != null" class="button" inline @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton> <MkButton v-if="announcement.id != null && announcement.isActive" class="button" inline @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton>
<MkButton v-if="announcement.id != null && !announcement.isActive" class="button" inline @click="unarchive(announcement)"><i class="ti ti-restore"></i> {{ i18n.ts.unarchive }}</MkButton>
<MkButton v-if="announcement.id != null" class="button" inline danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> <MkButton v-if="announcement.id != null" class="button" inline danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div> </div>
</div> </div>
</MkFolder> </MkFolder>
<MkLoading v-if="loadingMore"/>
<MkButton class="button" @click="more()"> <MkButton class="button" @click="more()">
<i class="ti ti-reload"></i>{{ i18n.ts.more }} <i class="ti ti-reload"></i>{{ i18n.ts.more }}
</MkButton> </MkButton>
</template>
</div> </div>
</MkSpacer> </MkSpacer>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue'; import { ref, computed, watch } from 'vue';
import XHeader from './_header_.vue'; import XHeader from './_header_.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkRadios from '@/components/MkRadios.vue'; import MkRadios from '@/components/MkRadios.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
@ -85,11 +98,22 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkTextarea from '@/components/MkTextarea.vue'; import MkTextarea from '@/components/MkTextarea.vue';
const announcementsStatus = ref<'active' | 'archived'>('active');
const loading = ref(true);
const loadingMore = ref(false);
const announcements = ref<any[]>([]); const announcements = ref<any[]>([]);
misskeyApi('admin/announcements/list').then(announcementResponse => { watch(announcementsStatus, (to) => {
loading.value = true;
misskeyApi('admin/announcements/list', {
status: to,
}).then(announcementResponse => {
announcements.value = announcementResponse; announcements.value = announcementResponse;
loading.value = false;
}); });
}, { immediate: true });
function add() { function add() {
announcements.value.unshift({ announcements.value.unshift({
@ -125,6 +149,14 @@ async function archive(announcement) {
refresh(); refresh();
} }
async function unarchive(announcement) {
await os.apiWithDialog('admin/announcements/update', {
...announcement,
isActive: true,
});
refresh();
}
async function save(announcement) { async function save(announcement) {
if (announcement.id == null) { if (announcement.id == null) {
await os.apiWithDialog('admin/announcements/create', announcement); await os.apiWithDialog('admin/announcements/create', announcement);
@ -135,24 +167,32 @@ async function save(announcement) {
} }
function more() { function more() {
misskeyApi('admin/announcements/list', { untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id }).then(announcementResponse => { loadingMore.value = true;
misskeyApi('admin/announcements/list', {
status: announcementsStatus.value,
untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id
}).then(announcementResponse => {
announcements.value = announcements.value.concat(announcementResponse); announcements.value = announcements.value.concat(announcementResponse);
loadingMore.value = false;
}); });
} }
function refresh() { function refresh() {
misskeyApi('admin/announcements/list').then(announcementResponse => { loading.value = true;
misskeyApi('admin/announcements/list', {
status: announcementsStatus.value,
}).then(announcementResponse => {
announcements.value = announcementResponse; announcements.value = announcementResponse;
loading.value = false;
}); });
} }
refresh();
const headerActions = computed(() => [{ const headerActions = computed(() => [{
asFullButton: true, asFullButton: true,
icon: 'ti ti-plus', icon: 'ti ti-plus',
text: i18n.ts.add, text: i18n.ts.add,
handler: add, handler: add,
disabled: announcementsStatus.value === 'archived',
}]); }]);
const headerTabs = computed(() => []); const headerTabs = computed(() => []);

View File

@ -80,9 +80,9 @@ const pagination = {
sort: sort.value, sort: sort.value,
host: host.value !== '' ? host.value : null, host: host.value !== '' ? host.value : null,
...( ...(
state.value === 'federating' ? { federating: true } : state.value === 'federating' ? { federating: true, suspended: false, blocked: false } :
state.value === 'subscribing' ? { subscribing: true } : state.value === 'subscribing' ? { subscribing: true, suspended: false, blocked: false } :
state.value === 'publishing' ? { publishing: true } : state.value === 'publishing' ? { publishing: true, suspended: false, blocked: false } :
state.value === 'suspended' ? { suspended: true } : state.value === 'suspended' ? { suspended: true } :
state.value === 'blocked' ? { blocked: true } : state.value === 'blocked' ? { blocked: true } :
state.value === 'silenced' ? { silenced: true } : state.value === 'silenced' ? { silenced: true } :

View File

@ -37,11 +37,17 @@ SPDX-License-Identifier: AGPL-3.0-only
</button> </button>
</div> </div>
</div> </div>
<div> <div class="_gaps_s">
<button class="_button" :class="$style.fileAltEditBtn" @click="describe()"> <button class="_button" :class="$style.kvEditBtn" @click="move()">
<MkKeyValue>
<template #key>{{ i18n.ts.folder }}</template>
<template #value>{{ folderHierarchy.join(' > ') }}<i class="ti ti-pencil" :class="$style.kvEditIcon"></i></template>
</MkKeyValue>
</button>
<button class="_button" :class="$style.kvEditBtn" @click="describe()">
<MkKeyValue> <MkKeyValue>
<template #key>{{ i18n.ts.description }}</template> <template #key>{{ i18n.ts.description }}</template>
<template #value>{{ file.comment ? file.comment : `(${i18n.ts.none})` }}<i class="ti ti-pencil" :class="$style.fileAltEditIcon"></i></template> <template #value>{{ file.comment ? file.comment : `(${i18n.ts.none})` }}<i class="ti ti-pencil" :class="$style.kvEditIcon"></i></template>
</MkKeyValue> </MkKeyValue>
</button> </button>
<MkKeyValue :class="$style.fileMetaDataChildren"> <MkKeyValue :class="$style.fileMetaDataChildren">
@ -90,6 +96,18 @@ const props = defineProps<{
const fetching = ref(true); const fetching = ref(true);
const file = ref<Misskey.entities.DriveFile>(); const file = ref<Misskey.entities.DriveFile>();
const folderHierarchy = computed(() => {
if (!file.value) return [i18n.ts.drive];
const folderNames = [i18n.ts.drive];
function get(folder: Misskey.entities.DriveFolder) {
if (folder.parent) get(folder.parent);
folderNames.push(folder.name);
}
if (file.value.folder) get(file.value.folder);
return folderNames;
});
const isImage = computed(() => file.value?.type.startsWith('image/')); const isImage = computed(() => file.value?.type.startsWith('image/'));
async function fetch() { async function fetch() {
@ -122,6 +140,19 @@ function crop() {
}); });
} }
function move() {
if (!file.value) return;
os.selectDriveFolder(false).then(folder => {
misskeyApi('drive/files/update', {
fileId: file.value.id,
folderId: folder[0] ? folder[0].id : null,
}).then(async () => {
await fetch();
});
});
}
function toggleSensitive() { function toggleSensitive() {
if (!file.value) return; if (!file.value) return;
@ -282,14 +313,14 @@ onMounted(async () => {
padding: .5rem 1rem; padding: .5rem 1rem;
} }
.fileAltEditBtn { .kvEditBtn {
text-align: start; text-align: start;
display: block; display: block;
width: 100%; width: 100%;
padding: .5rem 1rem; padding: .5rem 1rem;
border-radius: var(--radius); border-radius: var(--radius);
.fileAltEditIcon { .kvEditIcon {
display: inline-block; display: inline-block;
color: transparent; color: transparent;
visibility: hidden; visibility: hidden;
@ -300,7 +331,7 @@ onMounted(async () => {
color: var(--accent); color: var(--accent);
background-color: var(--accentedBg); background-color: var(--accentedBg);
.fileAltEditIcon { .kvEditIcon {
color: var(--accent); color: var(--accent);
visibility: visible; visibility: visible;
} }

View File

@ -243,6 +243,7 @@ async function del() {
left: 0; left: 0;
padding: 12px; padding: 12px;
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
background: var(--acrylicBg);
-webkit-backdrop-filter: var(--blur, blur(15px)); -webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px)); backdrop-filter: var(--blur, blur(15px));
} }

View File

@ -4,43 +4,33 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div> <MkStickyContainer>
<XAntenna :antenna="draft" @created="onAntennaCreated"/> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
</div>
<MkAntennaEditor @created="onAntennaCreated"/>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { computed } from 'vue';
import XAntenna from './editor.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { antennasCache } from '@/cache.js'; import { antennasCache } from '@/cache.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
import MkAntennaEditor from '@/components/MkAntennaEditor.vue';
const router = useRouter(); const router = useRouter();
const draft = ref({
name: '',
src: 'all',
userListId: null,
users: [],
keywords: [],
excludeKeywords: [],
excludeBots: false,
withReplies: false,
caseSensitive: false,
localOnly: false,
withFile: false,
notify: false,
});
function onAntennaCreated() { function onAntennaCreated() {
antennasCache.delete(); antennasCache.delete();
router.push('/my/antennas'); router.push('/my/antennas');
} }
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata(() => ({ definePageMetadata(() => ({
title: i18n.ts.manageAntennas, title: i18n.ts.createAntenna,
icon: 'ti ti-antenna', icon: 'ti ti-antenna',
})); }));
</script> </script>

View File

@ -4,15 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div class=""> <MkStickyContainer>
<XAntenna v-if="antenna" :antenna="antenna" @updated="onAntennaUpdated"/> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
</div>
<MkAntennaEditor v-if="antenna" :antenna="antenna" @updated="onAntennaUpdated"/>
</MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XAntenna from './editor.vue'; import MkAntennaEditor from '@/components/MkAntennaEditor.vue';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
@ -36,8 +38,11 @@ misskeyApi('antennas/show', { antennaId: props.antennaId }).then((antennaRespons
antenna.value = antennaResponse; antenna.value = antennaResponse;
}); });
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePageMetadata(() => ({ definePageMetadata(() => ({
title: i18n.ts.manageAntennas, title: i18n.ts.editAntenna,
icon: 'ti ti-antenna', icon: 'ti ti-antenna',
})); }));
</script> </script>

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> <MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkPagination v-slot="{ items }" ref="pagingComponent" :pagination="pagination" class="_gaps"> <MkPagination v-slot="{ items }" ref="pagingComponent" :pagination="pagination" class="_gaps">
<MkClipPreview v-for="item in items" :key="item.id" :clip="item"/> <MkClipPreview v-for="item in items" :key="item.id" :clip="item" :noUserInfo="true"/>
</MkPagination> </MkPagination>
</div> </div>
<div v-else-if="tab === 'favorites'" key="favorites" class="_gaps"> <div v-else-if="tab === 'favorites'" key="favorites" class="_gaps">

View File

@ -9,26 +9,35 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search"> <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
<template #prefix><i class="ti ti-search"></i></template> <template #prefix><i class="ti ti-search"></i></template>
</MkInput> </MkInput>
<MkFolder> <MkFoldableSection :expanded="true">
<template #label>{{ i18n.ts.options }}</template> <template #header>{{ i18n.ts.options }}</template>
<div class="_gaps_m"> <div class="_gaps_m">
<MkSwitch v-model="isLocalOnly">{{ i18n.ts.localOnly }}</MkSwitch> <MkRadios v-model="hostSelect">
<template #label>{{ i18n.ts.host }}</template>
<option value="all" default>{{ i18n.ts.all }}</option>
<option value="local">{{ i18n.ts.local }}</option>
<option v-if="noteSearchableScope === 'global'" value="specified">{{ i18n.ts.specifyHost }}</option>
</MkRadios>
<MkInput v-if="noteSearchableScope === 'global'" v-model="hostInput" :disabled="hostSelect !== 'specified'" :large="true" type="search">
<template #prefix><i class="ti ti-server"></i></template>
</MkInput>
<MkFolder :defaultOpen="true"> <MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.specifyUser }}</template> <template #label>{{ i18n.ts.specifyUser }}</template>
<template v-if="user" #suffix>@{{ user.username }}</template> <template v-if="user" #suffix>@{{ user.username }}{{ user.host ? `@${user.host}` : "" }}</template>
<div style="text-align: center;" class="_gaps"> <div class="_gaps">
<div v-if="user">@{{ user.username }}</div> <div :class="$style.userItem">
<div> <MkUserCardMini v-if="user" :class="$style.userCard" :user="user" :withChart="false"/>
<MkButton v-if="user == null" primary rounded inline @click="selectUser">{{ i18n.ts.selectUser }}</MkButton> <MkButton v-if="user == null && $i != null" transparent :class="$style.addMeButton" @click="selectSelf"><div :class="$style.addUserButtonInner"><span><i class="ti ti-plus"></i><i class="ti ti-user"></i></span><span>{{ i18n.ts.selectSelf }}</span></div></MkButton>
<MkButton v-else danger rounded inline @click="user = null">{{ i18n.ts.remove }}</MkButton> <MkButton v-if="user == null" transparent :class="$style.addUserButton" @click="selectUser"><div :class="$style.addUserButtonInner"><i class="ti ti-plus"></i><span>{{ i18n.ts.selectUser }}</span></div></MkButton>
<button class="_button" :class="$style.remove" :disabled="user == null" @click="removeUser"><i class="ti ti-x"></i></button>
</div> </div>
</div> </div>
</MkFolder> </MkFolder>
</div> </div>
</MkFolder> </MkFoldableSection>
<div> <div>
<MkButton large primary gradate rounded style="margin: 0 auto;" @click="search">{{ i18n.ts.search }}</MkButton> <MkButton large primary gradate rounded style="margin: 0 auto;" @click="search">{{ i18n.ts.search }}</MkButton>
</div> </div>
@ -42,31 +51,90 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { computed, ref, toRef, watch } from 'vue';
import type { UserDetailed } from 'misskey-js/entities.js';
import type { Paging } from '@/components/MkPagination.vue';
import MkNotes from '@/components/MkNotes.vue'; import MkNotes from '@/components/MkNotes.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkRadios from '@/components/MkRadios.vue';
import { $i } from '@/account.js';
import { instance } from '@/instance.js';
const props = withDefaults(defineProps<{
query?: string;
userId?: string;
username?: string;
host?: string | null;
}>(), {
query: '',
userId: undefined,
username: undefined,
host: '',
});
const router = useRouter(); const router = useRouter();
const key = ref(0); const key = ref(0);
const searchQuery = ref(''); const searchQuery = ref(toRef(props, 'query').value);
const searchOrigin = ref('combined'); const notePagination = ref<Paging>();
const notePagination = ref(); const user = ref<UserDetailed | null>(null);
const user = ref<any>(null); const hostInput = ref(toRef(props, 'host').value);
const isLocalOnly = ref(false);
function selectUser() { const noteSearchableScope = instance.noteSearchableScope ?? 'local';
os.selectUser({ includeSelf: true }).then(_user => {
const hostSelect = ref<'all' | 'local' | 'specified'>('all');
const setHostSelectWithInput = (after:string|undefined|null, before:string|undefined|null) => {
if (before === after) return;
if (after === '') hostSelect.value = 'all';
else hostSelect.value = 'specified';
};
setHostSelectWithInput(hostInput.value, undefined);
watch(hostInput, setHostSelectWithInput);
const searchHost = computed(() => {
if (hostSelect.value === 'local') return '.';
if (hostSelect.value === 'specified') return hostInput.value;
return null;
});
if (props.userId != null) {
misskeyApi('users/show', { userId: props.userId }).then(_user => {
user.value = _user; user.value = _user;
}); });
} else if (props.username != null) {
misskeyApi('users/show', {
username: props.username,
...(props.host != null && props.host !== '') ? { host: props.host } : {},
}).then(_user => {
user.value = _user;
});
}
function selectUser() {
os.selectUser({ includeSelf: true, localOnly: instance.noteSearchableScope === 'local' }).then(_user => {
user.value = _user;
hostInput.value = _user.host ?? '';
});
}
function selectSelf() {
user.value = $i as UserDetailed | null;
hostInput.value = null;
}
function removeUser() {
user.value = null;
hostInput.value = '';
} }
async function search() { async function search() {
@ -74,6 +142,7 @@ async function search() {
if (query == null || query === '') return; if (query == null || query === '') return;
//#region AP lookup
if (query.startsWith('https://')) { if (query.startsWith('https://')) {
const promise = misskeyApi('ap/show', { const promise = misskeyApi('ap/show', {
uri: query, uri: query,
@ -91,6 +160,7 @@ async function search() {
return; return;
} }
//#endregion
notePagination.value = { notePagination.value = {
endpoint: 'notes/search', endpoint: 'notes/search',
@ -98,11 +168,49 @@ async function search() {
params: { params: {
query: searchQuery.value, query: searchQuery.value,
userId: user.value ? user.value.id : null, userId: user.value ? user.value.id : null,
...(searchHost.value ? { host: searchHost.value } : {}),
}, },
}; };
if (isLocalOnly.value) notePagination.value.params.host = '.';
key.value++; key.value++;
} }
</script> </script>
<style lang="scss" module>
.userItem {
display: flex;
justify-content: center;
}
.addMeButton {
border: 2px dashed var(--fgTransparent);
padding: 12px;
margin-right: 16px;
}
.addUserButton {
border: 2px dashed var(--fgTransparent);
padding: 12px;
flex-grow: 1;
}
.addUserButtonInner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
min-height: 38px;
}
.userCard {
flex-grow: 1;
}
.remove {
width: 32px;
height: 32px;
align-self: center;
& > i:before {
color: #ff2a2a;
}
&:disabled {
opacity: 0;
}
}
</style>

View File

@ -0,0 +1,88 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw';
import search_ from './search.vue';
import { userDetailed } from '@/../.storybook/fakes.js';
import { commonHandlers } from '@/../.storybook/mocks.js';
const localUser = userDetailed('someuserid', 'miskist', null, 'Local Misskey User');
export const Default = {
render(args) {
return {
components: {
search_,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<search_ v-bind="props" />',
};
},
args: {
ignoreNotesSearchAvailable: true,
},
parameters: {
layout: 'fullscreen',
msw: {
handlers: [
...commonHandlers,
http.post('/api/users/show', () => {
return HttpResponse.json(userDetailed());
}),
http.post('/api/users/search', () => {
return HttpResponse.json([userDetailed(), localUser]);
}),
],
},
},
} satisfies StoryObj<typeof search_>;
export const NoteSearchDisabled = {
...Default,
args: {},
} satisfies StoryObj<typeof search_>;
export const WithUsernameLocal = {
...Default,
args: {
...Default.args,
username: localUser.username,
host: localUser.host,
},
parameters: {
layout: 'fullscreen',
msw: {
handlers: [
...commonHandlers,
http.post('/api/users/show', () => {
return HttpResponse.json(localUser);
}),
http.post('/api/users/search', () => {
return HttpResponse.json([userDetailed(), localUser]);
}),
],
},
},
} satisfies StoryObj<typeof search_>;
export const WithUserType = {
...Default,
args: {
type: 'user',
},
} satisfies StoryObj<typeof search_>;

View File

@ -25,7 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref, toRef } from 'vue';
import type { Endpoints } from 'misskey-js';
import type { Paging } from '@/components/MkPagination.vue';
import MkUserList from '@/components/MkUserList.vue'; import MkUserList from '@/components/MkUserList.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue'; import MkRadios from '@/components/MkRadios.vue';
@ -36,18 +38,27 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
const props = withDefaults(defineProps<{
query?: string,
origin?: Endpoints['users/search']['req']['origin'],
}>(), {
query: '',
origin: 'combined',
});
const router = useRouter(); const router = useRouter();
const key = ref(''); const key = ref('');
const searchQuery = ref(''); const searchQuery = ref(toRef(props, 'query').value);
const searchOrigin = ref('combined'); const searchOrigin = ref(toRef(props, 'origin').value);
const userPagination = ref(); const userPagination = ref<Paging>();
async function search() { async function search() {
const query = searchQuery.value.toString().trim(); const query = searchQuery.value.toString().trim();
if (query == null || query === '') return; if (query == null || query === '') return;
//#region AP lookup
if (query.startsWith('https://')) { if (query.startsWith('https://')) {
const promise = misskeyApi('ap/show', { const promise = misskeyApi('ap/show', {
uri: query, uri: query,
@ -65,6 +76,7 @@ async function search() {
return; return;
} }
//#endregion
userPagination.value = { userPagination.value = {
endpoint: 'users/search', endpoint: 'users/search',

View File

@ -9,8 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<MkSpacer v-if="tab === 'note'" key="note" :contentMax="800"> <MkSpacer v-if="tab === 'note'" key="note" :contentMax="800">
<div v-if="notesSearchAvailable"> <div v-if="notesSearchAvailable || ignoreNotesSearchAvailable">
<XNote/> <XNote v-bind="props"/>
</div> </div>
<div v-else> <div v-else>
<MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo> <MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
@ -18,27 +18,43 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSpacer> </MkSpacer>
<MkSpacer v-else-if="tab === 'user'" key="user" :contentMax="800"> <MkSpacer v-else-if="tab === 'user'" key="user" :contentMax="800">
<XUser/> <XUser v-bind="props"/>
</MkSpacer> </MkSpacer>
</MkHorizontalSwipe> </MkHorizontalSwipe>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent, ref } from 'vue'; import { computed, defineAsyncComponent, ref, toRef } from 'vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i } from '@/account.js'; import { notesSearchAvailable } from '@/scripts/check-permissions.js';
import { instance } from '@/instance.js';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const props = withDefaults(defineProps<{
query?: string,
userId?: string,
username?: string,
host?: string | null,
type?: 'note' | 'user',
origin?: 'combined' | 'local' | 'remote',
// For storybook only
ignoreNotesSearchAvailable?: boolean,
}>(), {
query: '',
userId: undefined,
username: undefined,
host: undefined,
type: 'note',
origin: 'combined',
ignoreNotesSearchAvailable: false,
});
const XNote = defineAsyncComponent(() => import('./search.note.vue')); const XNote = defineAsyncComponent(() => import('./search.note.vue'));
const XUser = defineAsyncComponent(() => import('./search.user.vue')); const XUser = defineAsyncComponent(() => import('./search.user.vue'));
const tab = ref('note'); const tab = ref(toRef(props, 'type').value);
const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes));
const headerActions = computed(() => []); const headerActions = computed(() => []);

View File

@ -177,6 +177,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option> <option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option> <option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
</MkSelect> </MkSelect>
<MkSelect v-model="contextMenu">
<template #label>{{ i18n.ts._contextMenu.title }}</template>
<option value="app">{{ i18n.ts._contextMenu.app }}</option>
<option value="appWithShift">{{ i18n.ts._contextMenu.appWithShift }}</option>
<option value="native">{{ i18n.ts._contextMenu.native }}</option>
</MkSelect>
<MkRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing> <MkRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing>
<template #label>{{ i18n.ts.numberOfPageCache }}</template> <template #label>{{ i18n.ts.numberOfPageCache }}</template>
<template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template> <template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
@ -317,6 +323,7 @@ const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHori
const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer')); const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer'));
const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow')); const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow'));
const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia')); const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia'));
const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu'));
watch(lang, () => { watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string); miLocalStorage.setItem('lang', lang.value as string);
@ -360,6 +367,7 @@ watch([
enableSeasonalScreenEffect, enableSeasonalScreenEffect,
alwaysConfirmFollow, alwaysConfirmFollow,
confirmWhenRevealingSensitiveMedia, confirmWhenRevealingSensitiveMedia,
contextMenu,
], async () => { ], async () => {
await reloadAsk(); await reloadAsk();
}); });

View File

@ -232,6 +232,9 @@ const routes: RouteDef[] = [{
component: page(() => import('@/pages/search.vue')), component: page(() => import('@/pages/search.vue')),
query: { query: {
q: 'query', q: 'query',
userId: 'userId',
username: 'username',
host: 'host',
channel: 'channel', channel: 'channel',
type: 'type', type: 'type',
origin: 'origin', origin: 'origin',

View File

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { instance } from '@/instance.js';
import { $i } from '@/account.js';
export const notesSearchAvailable = (
// FIXME: instance.policies would be null in Vitest
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
($i == null && instance.policies != null && instance.policies.canSearchNotes) ||
($i != null && $i.policies.canSearchNotes) ||
false
) as boolean;
export const canSearchNonLocalNotes = (
instance.noteSearchableScope === 'global'
);

View File

@ -41,6 +41,15 @@ function describe(file: Misskey.entities.DriveFile) {
}); });
} }
function move(file: Misskey.entities.DriveFile) {
os.selectDriveFolder(false).then(folder => {
misskeyApi('drive/files/update', {
fileId: file.id,
folderId: folder[0] ? folder[0].id : null,
});
});
}
function toggleSensitive(file: Misskey.entities.DriveFile) { function toggleSensitive(file: Misskey.entities.DriveFile) {
misskeyApi('drive/files/update', { misskeyApi('drive/files/update', {
fileId: file.id, fileId: file.id,
@ -88,6 +97,10 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
text: i18n.ts.rename, text: i18n.ts.rename,
icon: 'ti ti-forms', icon: 'ti ti-forms',
action: () => rename(file), action: () => rename(file),
}, {
text: i18n.ts.move,
icon: 'ti ti-folder-symlink',
action: () => move(file),
}, { }, {
text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation', icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',

View File

@ -13,6 +13,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore, userActions } from '@/store.js'; import { defaultStore, userActions } from '@/store.js';
import { $i, iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/account.js';
import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-permissions.js';
import { IRouter } from '@/nirax.js'; import { IRouter } from '@/nirax.js';
import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
import { mainRouter } from '@/router/main.js'; import { mainRouter } from '@/router/main.js';
@ -160,7 +161,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
action: () => { action: () => {
copyToClipboard(`@${user.username}@${user.host ?? host}`); copyToClipboard(`@${user.username}@${user.host ?? host}`);
}, },
}, ...(iAmModerator ? [{ }, ...( notesSearchAvailable && (user.host == null || canSearchNonLocalNotes) ? [{
icon: 'ti ti-search',
text: i18n.ts.searchThisUsersNotes,
action: () => {
router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`);
},
}] : [])
, ...(iAmModerator ? [{
icon: 'ti ti-user-exclamation', icon: 'ti ti-user-exclamation',
text: i18n.ts.moderation, text: i18n.ts.moderation,
action: () => { action: () => {

View File

@ -6,7 +6,7 @@
import { deepClone } from './clone.js'; import { deepClone } from './clone.js';
import type { Cloneable } from './clone.js'; import type { Cloneable } from './clone.js';
type DeepPartial<T> = { export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends Record<string | number | symbol, unknown> ? DeepPartial<T[P]> : T[P]; [P in keyof T]?: T[P] extends Record<string | number | symbol, unknown> ? DeepPartial<T[P]> : T[P];
}; };

View File

@ -458,6 +458,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device', where: 'device',
default: false, default: false,
}, },
contextMenu: {
where: 'device',
default: 'app' as 'app' | 'appWithShift' | 'native',
},
sound_masterVolume: { sound_masterVolume: {
where: 'device', where: 'device',

View File

@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:ref="id" :ref="id"
:key="id" :key="id"
:class="$style.column" :class="$style.column"
:column="columns.find(c => c.id === id)" :column="columns.find(c => c.id === id)!"
:isStacked="ids.length > 1" :isStacked="ids.length > 1"
@headerWheel="onWheel" @headerWheel="onWheel"
/> />
@ -95,7 +95,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue'; import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import XCommon from './_common_/common.vue'; import XCommon from './_common_/common.vue';
import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js'; import { deckStore, columnTypes, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js';
import type { ColumnType } from './deck/deck-store.js';
import XSidebar from '@/ui/_common_/navbar.vue'; import XSidebar from '@/ui/_common_/navbar.vue';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -152,10 +153,12 @@ window.addEventListener('resize', () => {
const snapScroll = deviceKind === 'smartphone' || deviceKind === 'tablet'; const snapScroll = deviceKind === 'smartphone' || deviceKind === 'tablet';
const drawerMenuShowing = ref(false); const drawerMenuShowing = ref(false);
/*
const route = 'TODO'; const route = 'TODO';
watch(route, () => { watch(route, () => {
drawerMenuShowing.value = false; drawerMenuShowing.value = false;
}); });
*/
const columns = deckStore.reactiveState.columns; const columns = deckStore.reactiveState.columns;
const layout = deckStore.reactiveState.layout; const layout = deckStore.reactiveState.layout;
@ -174,32 +177,20 @@ function showSettings() {
const columnsEl = shallowRef<HTMLElement>(); const columnsEl = shallowRef<HTMLElement>();
const addColumn = async (ev) => { const addColumn = async (ev) => {
const columns = [
'main',
'widgets',
'notifications',
'tl',
'antenna',
'list',
'channel',
'mentions',
'direct',
'roleTimeline',
];
const { canceled, result: column } = await os.select({ const { canceled, result: column } = await os.select({
title: i18n.ts._deck.addColumn, title: i18n.ts._deck.addColumn,
items: columns.map(column => ({ items: columnTypes.map(column => ({
value: column, text: i18n.ts._deck._columns[column], value: column, text: i18n.ts._deck._columns[column],
})), })),
}); });
if (canceled) return; if (canceled || column == null) return;
addColumnToStore({ addColumnToStore({
type: column, type: column,
id: uuid(), id: uuid(),
name: i18n.ts._deck._columns[column], name: i18n.ts._deck._columns[column],
width: 330, width: 330,
soundSetting: { type: null, volume: 1 },
}); });
}; };
@ -211,7 +202,7 @@ const onContextmenu = (ev) => {
}; };
function onWheel(ev: WheelEvent) { function onWheel(ev: WheelEvent) {
if (ev.deltaX === 0) { if (ev.deltaX === 0 && columnsEl.value != null) {
columnsEl.value.scrollLeft += ev.deltaY; columnsEl.value.scrollLeft += ev.deltaY;
} }
} }
@ -242,7 +233,7 @@ function changeProfile(ev: MouseEvent) {
title: i18n.ts._deck.profile, title: i18n.ts._deck.profile,
minLength: 1, minLength: 1,
}); });
if (canceled) return; if (canceled || name == null) return;
deckStore.set('profile', name); deckStore.set('profile', name);
unisonReload(); unisonReload();

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
<template #header> <template #header>
<i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span> <i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template> </template>
@ -14,7 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref, shallowRef, watch } from 'vue'; import { onMounted, ref, shallowRef, watch, defineAsyncComponent } from 'vue';
import type { entities as MisskeyEntities } from 'misskey-js';
import XColumn from './column.vue'; import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store.js'; import { updateColumn, Column } from './deck-store.js';
import MkTimeline from '@/components/MkTimeline.vue'; import MkTimeline from '@/components/MkTimeline.vue';
@ -22,6 +23,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
import { antennasCache } from '@/cache.js';
import { SoundStore } from '@/store.js'; import { SoundStore } from '@/store.js';
import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
@ -46,14 +48,36 @@ watch(soundSetting, v => {
async function setAntenna() { async function setAntenna() {
const antennas = await misskeyApi('antennas/list'); const antennas = await misskeyApi('antennas/list');
const { canceled, result: antenna } = await os.select({ const { canceled, result: antenna } = await os.select<MisskeyEntities.Antenna | '_CREATE_'>({
title: i18n.ts.selectAntenna, title: i18n.ts.selectAntenna,
items: [
{ value: '_CREATE_', text: i18n.ts.createNew },
(antennas.length > 0 ? {
sectionTitle: i18n.ts.createdAntennas,
items: antennas.map(x => ({ items: antennas.map(x => ({
value: x, text: x.name, value: x, text: x.name,
})), })),
} : undefined),
],
default: props.column.antennaId, default: props.column.antennaId,
}); });
if (canceled) return; if (canceled || antenna == null) return;
if (antenna === '_CREATE_') {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAntennaEditorDialog.vue')), {}, {
created: (newAntenna: MisskeyEntities.Antenna) => {
antennasCache.delete();
updateColumn(props.column.id, {
antennaId: newAntenna.id,
});
},
closed: () => {
dispose();
},
});
return;
}
updateColumn(props.column.id, { updateColumn(props.column.id, {
antennaId: antenna.id, antennaId: antenna.id,
}); });

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
<template #header> <template #header>
<i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span> <i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template> </template>
@ -68,6 +68,7 @@ async function setChannel() {
} }
async function post() { async function post() {
if (props.column.channelId == null) return;
if (!channel.value || channel.value.id !== props.column.channelId) { if (!channel.value || channel.value.id !== props.column.channelId) {
channel.value = await misskeyApi('channels/show', { channel.value = await misskeyApi('channels/show', {
channelId: props.column.channelId, channelId: props.column.channelId,

View File

@ -17,9 +17,24 @@ type ColumnWidget = {
data: Record<string, any>; data: Record<string, any>;
}; };
export const columnTypes = [
'main',
'widgets',
'notifications',
'tl',
'antenna',
'list',
'channel',
'mentions',
'direct',
'roleTimeline',
] as const;
export type ColumnType = typeof columnTypes[number];
export type Column = { export type Column = {
id: string; id: string;
type: 'main' | 'widgets' | 'notifications' | 'tl' | 'antenna' | 'channel' | 'list' | 'mentions' | 'direct'; type: ColumnType;
name: string | null; name: string | null;
width: number; width: number;
widgets?: ColumnWidget[]; widgets?: ColumnWidget[];
@ -265,7 +280,7 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
const columns = deepClone(deckStore.state.columns); const columns = deepClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = deepClone(deckStore.state.columns[columnIndex]); const column = deepClone(deckStore.state.columns[columnIndex]);
if (column == null) return; if (column == null || column.widgets == null) return;
column.widgets = column.widgets.filter(w => w.id !== widget.id); column.widgets = column.widgets.filter(w => w.id !== widget.id);
columns[columnIndex] = column; columns[columnIndex] = column;
deckStore.set('columns', columns); deckStore.set('columns', columns);
@ -287,7 +302,7 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat
const columns = deepClone(deckStore.state.columns); const columns = deepClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = deepClone(deckStore.state.columns[columnIndex]); const column = deepClone(deckStore.state.columns[columnIndex]);
if (column == null) return; if (column == null || column.widgets == null) return;
column.widgets = column.widgets.map(w => w.id === widgetId ? { column.widgets = column.widgets.map(w => w.id === widgetId ? {
...w, ...w,
data: widgetData, data: widgetData,

View File

@ -34,7 +34,7 @@ const tlComponent = ref<InstanceType<typeof MkNotes>>();
function reloadTimeline() { function reloadTimeline() {
return new Promise<void>((res) => { return new Promise<void>((res) => {
tlComponent.value.pagingComponent?.reload().then(() => { tlComponent.value?.pagingComponent?.reload().then(() => {
res(); res();
}); });
}); });

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
<template #header> <template #header>
<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span> <i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template> </template>
@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { watch, shallowRef, ref } from 'vue'; import { watch, shallowRef, ref } from 'vue';
import type { entities as MisskeyEntities } from 'misskey-js';
import XColumn from './column.vue'; import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store.js'; import { updateColumn, Column } from './deck-store.js';
import MkTimeline from '@/components/MkTimeline.vue'; import MkTimeline from '@/components/MkTimeline.vue';
@ -23,6 +24,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
import { SoundStore } from '@/store.js'; import { SoundStore } from '@/store.js';
import { userListsCache } from '@/cache.js';
import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
@ -51,18 +53,39 @@ watch(soundSetting, v => {
async function setList() { async function setList() {
const lists = await misskeyApi('users/lists/list'); const lists = await misskeyApi('users/lists/list');
const { canceled, result: list } = await os.select({ const { canceled, result: list } = await os.select<MisskeyEntities.UserList | '_CREATE_'>({
title: i18n.ts.selectList, title: i18n.ts.selectList,
items: [
{ value: '_CREATE_', text: i18n.ts.createNew },
(lists.length > 0 ? {
sectionTitle: i18n.ts.createdLists,
items: lists.map(x => ({ items: lists.map(x => ({
value: x, text: x.name, value: x, text: x.name,
})), })),
} : undefined),
],
default: props.column.listId, default: props.column.listId,
}); });
if (canceled) return; if (canceled || list == null) return;
if (list === '_CREATE_') {
const { canceled, result: name } = await os.inputText({
title: i18n.ts.enterListName,
});
if (canceled || name == null || name === '') return;
const res = await os.apiWithDialog('users/lists/create', { name: name });
userListsCache.delete();
updateColumn(props.column.id, {
listId: res.id,
});
} else {
updateColumn(props.column.id, { updateColumn(props.column.id, {
listId: list.id, listId: list.id,
}); });
} }
}
function editList() { function editList() {
os.pageWindow('my/lists/' + props.column.listId); os.pageWindow('my/lists/' + props.column.listId);

View File

@ -26,7 +26,7 @@ const tlComponent = ref<InstanceType<typeof MkNotes>>();
function reloadTimeline() { function reloadTimeline() {
return new Promise<void>((res) => { return new Promise<void>((res) => {
tlComponent.value.pagingComponent?.reload().then(() => { tlComponent.value?.pagingComponent?.reload().then(() => {
res(); res();
}); });
}); });

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="() => notificationsComponent.reload()"> <XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="async () => { await notificationsComponent?.reload() }">
<template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template> <template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
<XNotifications ref="notificationsComponent" :excludeTypes="props.column.excludeTypes"/> <XNotifications ref="notificationsComponent" :excludeTypes="props.column.excludeTypes"/>

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
<template #header> <template #header>
<i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span> <i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template> </template>
@ -53,7 +53,7 @@ async function setRole() {
})), })),
default: props.column.roleId, default: props.column.roleId,
}); });
if (canceled) return; if (canceled || role == null) return;
updateColumn(props.column.id, { updateColumn(props.column.id, {
roleId: role.id, roleId: role.id,
}); });

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()"> <XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
<template #header> <template #header>
<i v-if="column.tl === 'home'" class="ti ti-home"></i> <i v-if="column.tl === 'home'" class="ti ti-home"></i>
<i v-else-if="column.tl === 'local'" class="ti ti-planet"></i> <i v-else-if="column.tl === 'local'" class="ti ti-planet"></i>
@ -113,6 +113,7 @@ async function setType() {
} }
return; return;
} }
if (src == null) return;
updateColumn(props.column.id, { updateColumn(props.column.id, {
tl: src, tl: src,
}); });

View File

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "2024.7.0-rc.4", "version": "2024.7.0-rc.5",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"license": "MIT", "license": "MIT",
"main": "./built/index.js", "main": "./built/index.js",

View File

@ -4939,6 +4939,11 @@ export type components = {
serverRules: string[]; serverRules: string[];
themeColor: string | null; themeColor: string | null;
policies: components['schemas']['RolePolicies']; policies: components['schemas']['RolePolicies'];
/**
* @default local
* @enum {string}
*/
noteSearchableScope: 'local' | 'global';
}; };
MetaDetailedOnly: { MetaDetailedOnly: {
features?: { features?: {
@ -6091,6 +6096,11 @@ export type operations = {
untilId?: string; untilId?: string;
/** Format: misskey:id */ /** Format: misskey:id */
userId?: string | null; userId?: string | null;
/**
* @default active
* @enum {string}
*/
status?: 'all' | 'active' | 'archived';
}; };
}; };
}; };