Compare commits

..

No commits in common. "develop" and "2025.9.0-alpha.1" have entirely different histories.

236 changed files with 3541 additions and 7241 deletions

View File

@ -105,16 +105,6 @@ port: 3000
# socket: /path/to/misskey.sock
# chmodSocket: '777'
# Proxy trust settings
#
# Changes how the server interpret the origin IP of the request.
#
# Any format supported by Fastify is accepted.
# Default: trust all proxies (i.e. trustProxy: true)
# See: https://fastify.dev/docs/latest/reference/server/#trustproxy
#
# trustProxy: 1
# ┌──────────────────────────┐
#───┘ PostgreSQL configuration └────────────────────────────────

View File

@ -1,43 +1,20 @@
## 2025.9.1
### NOTE
- pnpm 10.16.0 が必要です
## 2025.9.0
### General
- Feat: 予約投稿ができるようになりました
- デフォルトで作成可能数は1になっています。適宜ロールのポリシーで設定を行ってください。
- Enhance: 広告ごとにセンシティブフラグを設定できるようになりました
### Client
- Feat: アカウントのQRコードを表示・読み取りできるようになりました
- Feat: 動画を圧縮してアップロードできるようになりました
- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました
- Enhance: 画像編集にマスクエフェクト(塗りつぶし、ぼかし、モザイク)を追加
- Enhance: ウォーターマークにアカウントのQRコードを追加できるように
- Enhance: テーマをドラッグ&ドロップできるように
- Enhance: 絵文字ピッカーのサイズをより大きくできるように
- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
- Fix: iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正
- Fix: アクティビティウィジェットのグラフモードが動作しない問題を修正
### Server
- Enhance: ユーザーIPを確実に取得できるために設定ファイルにFastifyOptions.trustProxyを追加しました
## 2025.9.0
-
### Client
- Enhance: AiScriptAppウィジェットで構文エラーを検知してもダイアログではなくウィジェット内にエラーを表示するように
- Enhance: /flushページでサイトキャッシュをクリアできるようになりました
- Enhance: クリップ/リスト/アンテナ/ロール追加系メニュー項目において、表示件数を拡張
- Enhance: 「キャッシュを削除」ボタンでブラウザの内部キャッシュの削除も行えるように
- Enhance: CtrlキーCommandキーを押下しながらリンクをクリックすると新しいタブで開くように
- Fix: プッシュ通知を有効にできない問題を修正
- Fix: RSSティッカーウィジェットが正しく動作しない問題を修正
- Fix: プロファイルを復元後アカウントの切り替えができない問題を修正
- Fix: エラー画像が横に引き伸ばされてしまう問題に対応
### Server
- Fix: webpなどの画像に対してセンシティブなメディアの検出が適用されていなかった問題を修正
-
## 2025.8.0

View File

@ -1644,7 +1644,7 @@ _serverSettings:
reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les línies de temps reduint la càrrega de la base. Com a contrapunt, augmentarà l'ús de memòria de Redís. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat."
remoteNotesCleaning: "Neteja automàtica de notes remotes"
remoteNotesCleaning_description: "Quan activis aquesta opció, periòdicament es netejaran les notes remotes que no es consultin, això evitarà que la base de dades se"
remoteNotesCleaningMaxProcessingDuration: "Duració màxima del temps de funcionament del procés de neteja"
remoteNotesCleaningMaxProcessingDuration: "D'oració màxima del temps de funcionament del procés de neteja"
remoteNotesCleaningExpiryDaysForEachNotes: "Duració mínima de conservació de les notes"
inquiryUrl: "URL de consulta "
inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pàgina web amb el contacte d'informació."

View File

@ -3194,7 +3194,6 @@ _imageEffector:
mirror: "Mirror"
invert: "Invert Colors"
grayscale: "Grayscale"
blur: "Blur"
colorAdjust: "Color Correction"
colorClamp: "Color Compression"
colorClampAdvanced: "Color Compression (Advanced)"
@ -3210,8 +3209,6 @@ _imageEffector:
angle: "Angle"
scale: "Size"
size: "Size"
radius: "Radius"
samples: "Samples"
color: "Color"
opacity: "Opacity"
normalize: "Normalize"

View File

@ -2137,7 +2137,7 @@ _aboutMisskey:
_displayOfSensitiveMedia:
respect: "Esconder medios marcados como sensibles"
ignore: "Mostrar medios marcados como sensibles"
force: "Esconder toda la multimedia"
force: "Esconder todala multimedia"
_instanceTicker:
none: "No mostrar"
remote: "Mostrar a usuarios remotos"

288
locales/index.d.ts vendored
View File

@ -1030,10 +1030,6 @@ export interface Locale extends ILocale {
*
*/
"processing": string;
/**
*
*/
"preprocessing": string;
/**
*
*/
@ -1231,7 +1227,7 @@ export interface Locale extends ILocale {
*/
"noMoreHistory": string;
/**
*
*
*/
"startChat": string;
/**
@ -1931,7 +1927,7 @@ export interface Locale extends ILocale {
*/
"markAsReadAllUnreadNotes": string;
/**
*
*
*/
"markAsReadAllTalkMessages": string;
/**
@ -5286,10 +5282,6 @@ export interface Locale extends ILocale {
*
*/
"draft": string;
/**
* 稿
*/
"draftsAndScheduledNotes": string;
/**
*
*/
@ -5398,14 +5390,6 @@ export interface Locale extends ILocale {
*
*/
"chat": string;
/**
*
*/
"directMessage": string;
/**
*
*/
"directMessage_short": string;
/**
*
*/
@ -5517,14 +5501,6 @@ export interface Locale extends ILocale {
* <br>
*/
"defaultImageCompressionLevel_description": string;
/**
*
*/
"defaultCompressionLevel": string;
/**
* <br>
*/
"defaultCompressionLevel_description": string;
/**
*
*/
@ -5553,60 +5529,6 @@ export interface Locale extends ILocale {
*
*/
"thankYouForTestingBeta": string;
/**
*
*/
"createUserSpecifiedNote": string;
/**
* 稿
*/
"schedulePost": string;
/**
* {x}稿
*/
"scheduleToPostOnX": ParameterizedString<"x">;
/**
* {x}稿
*/
"scheduledToPostOnX": ParameterizedString<"x">;
/**
*
*/
"schedule": string;
/**
*
*/
"scheduled": string;
"_compression": {
"_quality": {
/**
*
*/
"high": string;
/**
*
*/
"medium": string;
/**
*
*/
"low": string;
};
"_size": {
/**
*
*/
"large": string;
/**
*
*/
"medium": string;
/**
*
*/
"small": string;
};
};
"_order": {
/**
*
@ -5618,10 +5540,6 @@ export interface Locale extends ILocale {
"oldest": string;
};
"_chat": {
/**
*
*/
"messages": string;
/**
*
*/
@ -5631,36 +5549,36 @@ export interface Locale extends ILocale {
*/
"newMessage": string;
/**
*
*
*/
"individualChat": string;
/**
*
*
*/
"individualChat_description": string;
/**
*
*
*/
"roomChat": string;
/**
*
*
*
*
*/
"roomChat_description": string;
/**
*
*
*/
"createRoom": string;
/**
*
*
*/
"inviteUserToChat": string;
/**
*
*
*/
"yourRooms": string;
/**
*
*
*/
"joiningRooms": string;
/**
@ -5680,7 +5598,7 @@ export interface Locale extends ILocale {
*/
"noHistory": string;
/**
*
*
*/
"noRooms": string;
/**
@ -5700,7 +5618,7 @@ export interface Locale extends ILocale {
*/
"ignore": string;
/**
* 退
* 退
*/
"leave": string;
/**
@ -5724,35 +5642,35 @@ export interface Locale extends ILocale {
*/
"newline": string;
/**
*
*
*/
"muteThisRoom": string;
/**
*
*
*/
"deleteRoom": string;
/**
*
*
*/
"chatNotAvailableForThisAccountOrServer": string;
/**
*
*
*/
"chatIsReadOnlyForThisAccountOrServer": string;
/**
* 使
* 使
*/
"chatNotAvailableInOtherAccount": string;
/**
*
*
*/
"cannotChatWithTheUser": string;
/**
* 使
* 使
*/
"cannotChatWithTheUser_description": string;
/**
*
*
*/
"youAreNotAMemberOfThisRoomButInvited": string;
/**
@ -5760,31 +5678,31 @@ export interface Locale extends ILocale {
*/
"doYouAcceptInvitation": string;
/**
*
*
*/
"chatWithThisUser": string;
/**
*
*
*/
"thisUserAllowsChatOnlyFromFollowers": string;
/**
*
*
*/
"thisUserAllowsChatOnlyFromFollowing": string;
/**
*
*
*/
"thisUserAllowsChatOnlyFromMutualFollowing": string;
/**
*
*
*/
"thisUserNotAllowedChatAnyone": string;
/**
*
*
*/
"chatAllowedUsers": string;
/**
*
*
*/
"chatAllowedUsers_note": string;
"_chatAllowedUsers": {
@ -7938,7 +7856,7 @@ export interface Locale extends ILocale {
*/
"canImportUserLists": string;
/**
*
*
*/
"chatAvailability": string;
/**
@ -7957,10 +7875,6 @@ export interface Locale extends ILocale {
*
*/
"noteDraftLimit": string;
/**
* 稿
*/
"scheduledNoteLimit": string;
/**
* 使
*/
@ -8792,7 +8706,7 @@ export interface Locale extends ILocale {
*/
"badge": string;
/**
*
*
*/
"messageBg": string;
/**
@ -8819,7 +8733,7 @@ export interface Locale extends ILocale {
*/
"reaction": string;
/**
*
*
*/
"chatMessage": string;
};
@ -9103,11 +9017,11 @@ export interface Locale extends ILocale {
*/
"write:following": string;
/**
*
*
*/
"read:messaging": string;
/**
*
*
*/
"write:messaging": string;
/**
@ -9399,11 +9313,11 @@ export interface Locale extends ILocale {
*/
"write:report-abuse": string;
/**
*
*
*/
"write:chat": string;
/**
*
*
*/
"read:chat": string;
};
@ -9629,7 +9543,7 @@ export interface Locale extends ILocale {
*/
"birthdayFollowings": string;
/**
*
*
*/
"chat": string;
};
@ -10356,14 +10270,6 @@ export interface Locale extends ILocale {
*
*/
"pollEnded": string;
/**
* 稿
*/
"scheduledNotePosted": string;
/**
* 稿
*/
"scheduledNotePostFailed": string;
/**
* 稿
*/
@ -10377,7 +10283,7 @@ export interface Locale extends ILocale {
*/
"roleAssigned": string;
/**
*
*
*/
"chatRoomInvitationReceived": string;
/**
@ -10490,7 +10396,7 @@ export interface Locale extends ILocale {
*/
"roleAssigned": string;
/**
*
*
*/
"chatRoomInvitationReceived": string;
/**
@ -10672,7 +10578,7 @@ export interface Locale extends ILocale {
*/
"roleTimeline": string;
/**
*
*
*/
"chat": string;
};
@ -11039,7 +10945,7 @@ export interface Locale extends ILocale {
*/
"deleteGalleryPost": string;
/**
*
*
*/
"deleteChatRoom": string;
/**
@ -12313,18 +12219,10 @@ export interface Locale extends ILocale {
*
*/
"text": string;
/**
*
*/
"qr": string;
/**
*
*/
"position": string;
/**
*
*/
"margin": string;
/**
*
*/
@ -12381,10 +12279,6 @@ export interface Locale extends ILocale {
*
*/
"polkadotSubDotDivisions": string;
/**
* URLになります
*/
"leaveBlankToAccountUrl": string;
};
"_imageEffector": {
/**
@ -12424,14 +12318,6 @@ export interface Locale extends ILocale {
*
*/
"grayscale": string;
/**
*
*/
"blur": string;
/**
*
*/
"pixelate": string;
/**
* 調
*/
@ -12476,10 +12362,6 @@ export interface Locale extends ILocale {
*
*/
"tearing": string;
/**
*
*/
"fill": string;
};
"_fxProps": {
/**
@ -12494,18 +12376,6 @@ export interface Locale extends ILocale {
*
*/
"size": string;
/**
*
*/
"radius": string;
/**
*
*/
"samples": string;
/**
*
*/
"offset": string;
/**
*
*/
@ -12618,10 +12488,6 @@ export interface Locale extends ILocale {
*
*/
"zoomLinesBlack": string;
/**
*
*/
"circle": string;
};
};
/**
@ -12681,80 +12547,6 @@ export interface Locale extends ILocale {
*
*/
"listDrafts": string;
/**
* 稿
*/
"schedule": string;
/**
* 稿
*/
"listScheduledNotes": string;
/**
*
*/
"cancelSchedule": string;
};
/**
*
*/
"qr": string;
"_qr": {
/**
*
*/
"showTabTitle": string;
/**
*
*/
"readTabTitle": string;
/**
* {name} {acct}
*/
"shareTitle": ParameterizedString<"name" | "acct">;
/**
* Fediverseで私をフォローしてください
*/
"shareText": string;
/**
*
*/
"chooseCamera": string;
/**
*
*/
"cannotToggleFlash": string;
/**
*
*/
"turnOnFlash": string;
/**
*
*/
"turnOffFlash": string;
/**
*
*/
"startQr": string;
/**
*
*/
"stopQr": string;
/**
* QRコードが見つかりません
*/
"noQrCodeFound": string;
/**
*
*/
"scanFile": string;
/**
*
*/
"raw": string;
/**
* MFM
*/
"mfm": string;
};
}
declare const locales: {

View File

@ -253,7 +253,6 @@ noteDeleteConfirm: "このノートを削除しますか?"
pinLimitExceeded: "これ以上ピン留めできません"
done: "完了"
processing: "処理中"
preprocessing: "準備中"
preview: "プレビュー"
default: "デフォルト"
defaultValueIs: "デフォルト: {value}"
@ -303,7 +302,7 @@ uploadNFiles: "{n}個のファイルをアップロード"
explore: "みつける"
messageRead: "既読"
noMoreHistory: "これより過去の履歴はありません"
startChat: "メッセージを送る"
startChat: "チャットを始める"
nUsersRead: "{n}人が読みました"
agreeTo: "{0}に同意"
agree: "同意する"
@ -478,7 +477,7 @@ notFoundDescription: "指定されたURLに該当するページはありませ
uploadFolder: "既定アップロード先"
markAsReadAllNotifications: "すべての通知を既読にする"
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
markAsReadAllTalkMessages: "すべてのダイレクトメッセージを既読にする"
markAsReadAllTalkMessages: "すべてのチャットを既読にする"
help: "ヘルプ"
inputMessageHere: "ここにメッセージを入力"
close: "閉じる"
@ -1317,7 +1316,6 @@ acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします
federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
draft: "下書き"
draftsAndScheduledNotes: "下書きと予約投稿"
confirmOnReact: "リアクションする際に確認する"
reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?"
@ -1345,8 +1343,6 @@ postForm: "投稿フォーム"
textCount: "文字数"
information: "情報"
chat: "チャット"
directMessage: "ダイレクトメッセージ"
directMessage_short: "メッセージ"
migrateOldSettings: "旧設定情報を移行"
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
compress: "圧縮"
@ -1374,8 +1370,6 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示"
hideAllTips: "全ての「ヒントとコツ」を非表示"
defaultImageCompressionLevel: "デフォルトの画像圧縮度"
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
defaultCompressionLevel: "デフォルトの圧縮度"
defaultCompressionLevel_description: "低くすると品質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、品質は低下します。"
inMinutes: "分"
inDays: "日"
safeModeEnabled: "セーフモードが有効です"
@ -1383,70 +1377,53 @@ pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プ
customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。"
themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。"
thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!"
createUserSpecifiedNote: "ユーザー指定ノートを作成"
schedulePost: "投稿を予約"
scheduleToPostOnX: "{x}に投稿を予約します"
scheduledToPostOnX: "{x}に投稿が予約されています"
schedule: "予約"
scheduled: "予約"
_compression:
_quality:
high: "高品質"
medium: "中品質"
low: "低品質"
_size:
large: "サイズ大"
medium: "サイズ中"
small: "サイズ小"
_order:
newest: "新しい順"
oldest: "古い順"
_chat:
messages: "メッセージ"
noMessagesYet: "まだメッセージはありません"
newMessage: "新しいメッセージ"
individualChat: "個"
individualChat_description: "特定ユーザーと個別にメッセージのやりとりができます。"
roomChat: "グループ"
roomChat_description: "複数人でメッセージのやりとりができます。\nまた、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。"
createRoom: "グループを作成"
inviteUserToChat: "ユーザーを招待してメッセージを送信しましょう"
yourRooms: "作成したグループ"
joiningRooms: "参加中のグループ"
individualChat: "個人チャット"
individualChat_description: "特定ユーザーとの一対一のチャットができます。"
roomChat: "ルームチャット"
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
createRoom: "ルームを作成"
inviteUserToChat: "ユーザーを招待してチャットを始めましょう"
yourRooms: "作成したルーム"
joiningRooms: "参加中のルーム"
invitations: "招待"
noInvitations: "招待はありません"
history: "履歴"
noHistory: "履歴はありません"
noRooms: "グループはありません"
noRooms: "ルームはありません"
inviteUser: "ユーザーを招待"
sentInvitations: "送信した招待"
join: "参加"
ignore: "無視"
leave: "グループから退出"
leave: "ルームから退出"
members: "メンバー"
searchMessages: "メッセージを検索"
home: "ホーム"
send: "送信"
newline: "改行"
muteThisRoom: "このグループをミュート"
deleteRoom: "グループを削除"
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。"
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。"
chatNotAvailableInOtherAccount: "相手のアカウントでダイレクトメッセージが使えない状態になっています。"
cannotChatWithTheUser: "このユーザーとのダイレクトメッセージを開始できません"
cannotChatWithTheUser_description: "ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。"
youAreNotAMemberOfThisRoomButInvited: "あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
muteThisRoom: "このルームをミュート"
deleteRoom: "ルームを削除"
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。"
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。"
chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。"
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
youAreNotAMemberOfThisRoomButInvited: "あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
doYouAcceptInvitation: "招待を承認しますか?"
chatWithThisUser: "ダイレクトメッセージ"
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみメッセージを受け付けています。"
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。"
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。"
thisUserNotAllowedChatAnyone: "このユーザーは誰からもメッセージを受け付けていません。"
chatAllowedUsers: "メッセージを許可する相手"
chatAllowedUsers_note: "自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。"
chatWithThisUser: "チャットする"
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。"
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。"
thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。"
chatAllowedUsers: "チャットを許可する相手"
chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。"
_chatAllowedUsers:
everyone: "誰でも"
followers: "自分のフォロワーのみ"
@ -2057,12 +2034,11 @@ _role:
canImportFollowing: "フォローのインポートを許可"
canImportMuting: "ミュートのインポートを許可"
canImportUserLists: "リストのインポートを許可"
chatAvailability: "ダイレクトメッセージを許可"
chatAvailability: "チャットを許可"
uploadableFileTypes: "アップロード可能なファイル種別"
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数"
scheduledNoteLimit: "予約投稿の同時作成可能数"
watermarkAvailable: "ウォーターマーク機能の使用可否"
_condition:
roleAssignedTo: "マニュアルロールにアサイン済み"
@ -2305,7 +2281,7 @@ _theme:
buttonHoverBg: "ボタンの背景 (ホバー)"
inputBorder: "入力ボックスの縁取り"
badge: "バッジ"
messageBg: "メッセージの背景"
messageBg: "チャットの背景"
fgHighlighted: "強調された文字"
_sfx:
@ -2313,7 +2289,7 @@ _sfx:
noteMy: "ノート(自分)"
notification: "通知"
reaction: "リアクション選択時"
chatMessage: "ダイレクトメッセージ"
chatMessage: "チャットのメッセージ"
_soundSettings:
driveFile: "ドライブの音声を使用"
@ -2393,8 +2369,8 @@ _permissions:
"write:favorites": "お気に入りを操作する"
"read:following": "フォローの情報を見る"
"write:following": "フォロー・フォロー解除する"
"read:messaging": "ダイレクトメッセージを見る"
"write:messaging": "ダイレクトメッセージを操作する"
"read:messaging": "チャットを見る"
"write:messaging": "チャットを操作する"
"read:mutes": "ミュートを見る"
"write:mutes": "ミュートを操作する"
"write:notes": "ノートを作成・削除する"
@ -2467,8 +2443,8 @@ _permissions:
"read:clip-favorite": "クリップのいいねを見る"
"read:federation": "連合に関する情報を取得する"
"write:report-abuse": "違反を報告する"
"write:chat": "ダイレクトメッセージを操作する"
"read:chat": "ダイレクトメッセージを閲覧する"
"write:chat": "チャットを操作する"
"read:chat": "チャットを閲覧する"
_auth:
shareAccessTitle: "アプリへのアクセス許可"
@ -2531,7 +2507,7 @@ _widgets:
chooseList: "リストを選択"
clicker: "クリッカー"
birthdayFollowings: "今日誕生日のユーザー"
chat: "ダイレクトメッセージ"
chat: "チャット"
_cw:
hide: "隠す"
@ -2735,12 +2711,10 @@ _notification:
youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました"
pollEnded: "アンケートの結果が出ました"
scheduledNotePosted: "予約ノートが投稿されました"
scheduledNotePostFailed: "予約ノートの投稿に失敗しました"
newNote: "新しい投稿"
unreadAntennaNote: "アンテナ {name}"
roleAssigned: "ロールが付与されました"
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待されました"
chatRoomInvitationReceived: "チャットルームへ招待されました"
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
achievementEarned: "実績を獲得"
testNotification: "通知テスト"
@ -2770,7 +2744,7 @@ _notification:
receiveFollowRequest: "フォロー申請を受け取った"
followRequestAccepted: "フォローが受理された"
roleAssigned: "ロールが付与された"
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待された"
chatRoomInvitationReceived: "チャットルームへ招待された"
achievementEarned: "実績の獲得"
exportCompleted: "エクスポートが完了した"
login: "ログイン"
@ -2820,7 +2794,7 @@ _deck:
mentions: "メンション"
direct: "指名"
roleTimeline: "ロールタイムライン"
chat: "ダイレクトメッセージ"
chat: "チャット"
_dialog:
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
@ -2923,7 +2897,7 @@ _moderationLogTypes:
deletePage: "ページを削除"
deleteFlash: "Playを削除"
deleteGalleryPost: "ギャラリーの投稿を削除"
deleteChatRoom: "ダイレクトメッセージのグループを削除"
deleteChatRoom: "チャットルームを削除"
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
_fileViewer:
@ -3297,9 +3271,7 @@ _watermarkEditor:
opacity: "不透明度"
scale: "サイズ"
text: "テキスト"
qr: "二次元コード"
position: "位置"
margin: "マージン"
type: "タイプ"
image: "画像"
advanced: "高度"
@ -3314,7 +3286,6 @@ _watermarkEditor:
polkadotSubDotOpacity: "サブドットの不透明度"
polkadotSubDotRadius: "サブドットの大きさ"
polkadotSubDotDivisions: "サブドットの数"
leaveBlankToAccountUrl: "空欄にするとアカウントのURLになります"
_imageEffector:
title: "エフェクト"
@ -3328,8 +3299,6 @@ _imageEffector:
mirror: "ミラー"
invert: "色の反転"
grayscale: "白黒"
blur: "ぼかし"
pixelate: "モザイク"
colorAdjust: "色調補正"
colorClamp: "色の圧縮"
colorClampAdvanced: "色の圧縮(高度)"
@ -3341,15 +3310,11 @@ _imageEffector:
checker: "チェッカー"
blockNoise: "ブロックノイズ"
tearing: "ティアリング"
fill: "塗りつぶし"
_fxProps:
angle: "角度"
scale: "サイズ"
size: "サイズ"
radius: "半径"
samples: "サンプル数"
offset: "位置"
color: "色"
opacity: "不透明度"
normalize: "正規化"
@ -3378,7 +3343,6 @@ _imageEffector:
zoomLinesThreshold: "集中線の幅"
zoomLinesMaskSize: "中心径"
zoomLinesBlack: "黒色にする"
circle: "円形"
drafts: "下書き"
_drafts:
@ -3395,23 +3359,3 @@ _drafts:
restoreFromDraft: "下書きから復元"
restore: "復元"
listDrafts: "下書き一覧"
schedule: "投稿予約"
listScheduledNotes: "予約投稿一覧"
cancelSchedule: "予約解除"
qr: "二次元コード"
_qr:
showTabTitle: "表示"
readTabTitle: "読み取る"
shareTitle: "{name} {acct}"
shareText: "Fediverseで私をフォローしてください"
chooseCamera: "カメラを選択"
cannotToggleFlash: "ライト選択不可"
turnOnFlash: "ライトをオンにする"
turnOffFlash: "ライトをオフにする"
startQr: "コードリーダーを再開"
stopQr: "コードリーダーを停止"
noQrCodeFound: "QRコードが見つかりません"
scanFile: "端末の画像をスキャン"
raw: "テキスト"
mfm: "MFM"

View File

@ -1215,7 +1215,6 @@ privacyPolicyUrl: "Ссылка на Политику Конфиденциаль
tosAndPrivacyPolicy: "Условия использования и политика конфиденциальности"
avatarDecorations: "Украшения для аватара"
attach: "Прикрепить"
detachAll: "Убрать всё"
angle: "Угол"
flip: "Переворот"
showAvatarDecorations: "Показать украшения для аватара"
@ -1254,7 +1253,7 @@ clipNoteLimitExceeded: "К этому клипу больше нельзя до
performance: "Производительность"
modified: "Изменено"
signinWithPasskey: "Войдите в систему, используя свой пароль"
unknownWebAuthnKey: "Неизвестный ключ"
unknownWebAuthnKey: "Не известный ключ "
passkeyVerificationFailed: "Ошибка проверка ключа доступа "
messageToFollower: "Сообщение подписчикам"
testCaptchaWarning: "Эта функция предназначена для тестирования CAPTCHA. <strong>Не использовать это в рабочей среде</strong>"
@ -1269,11 +1268,8 @@ availableRoles: "Доступные роли"
federationDisabled: "Федерация отключена для этого сервера. Вы не можете взаимодействовать с пользователями на других серверах."
draft: "Черновик"
markAsSensitiveConfirm: "Отметить контент как чувствительный?"
preferences: "Основное"
resetToDefaultValue: "Сбросить настройки до стандартных"
syncBetweenDevices: "Синхронизировать между устройствами"
postForm: "Форма отправки"
textCount: "Количество символов"
information: "Описание"
inMinutes: "мин"
inDays: "сут"
@ -1285,11 +1281,6 @@ _chat:
send: "Отправить"
_settings:
webhook: "Вебхук"
preferencesBanner: "Вы можете настроить общее поведение клиента по вашим предпочтениям"
timelineAndNote: "Лента и заметки"
_chat:
showSenderName: "Показывать имя отправителя"
sendOnEnter: "Использовать Enter для отправки"
_delivery:
stop: "Заморожено"
_type:
@ -1538,7 +1529,7 @@ _achievements:
description: "Нажато здесь"
_justPlainLucky:
title: "Чистая удача"
description: "Может достаться с вероятностью 0,005% каждые 10 секунд."
description: "Может достаться с вероятностью 0,01% каждые 10 секунд."
_setNameToSyuilo:
title: "Комплекс бога"
description: "Установлено «syuilo» в качестве имени"
@ -1566,12 +1557,6 @@ _achievements:
title: "Brain Diver"
description: "Опубликована ссылка на песню «Brain Diver»"
flavor: "Мисски-Мисски Ла-Ту-Ма"
_bubbleGameExplodingHead:
title: "🤯"
description: "Самый большой объект в Bubble game"
_bubbleGameDoubleExplodingHead:
title: "Двойной🤯"
description: "Два самых больших объекта в Bubble game одновременно!"
_role:
new: "Новая роль"
edit: "Изменить роль"

View File

@ -360,7 +360,7 @@ whenServerDisconnected: "Sunucu ile bağlantı kesildiğinde"
disconnectedFromServer: "Sunucu bağlantısı kesildi"
reload: "Yenile"
doNothing: "Yoksay"
reloadConfirm: "Panoyu yenilemek ister misin?"
reloadConfirm: "Zaman çizelgesini yenilemek ister misin?"
watch: "İzle"
unwatch: "İzlemeyi bırak"
accept: "Kabul et"
@ -573,9 +573,9 @@ objectStorageSetPublicRead: "Yükleme sırasında \"genel-okuma\" ayarını yap
s3ForcePathStyleDesc: "s3ForcePathStyle etkinleştirilirse, kova adı URL'nin ana bilgisayar adı yerine URL yoluna eklenmelidir. Kendi kendine barındırılan bir Minio örneği gibi hizmetleri kullanırken bu ayarı etkinleştirmen gerekebilir."
serverLogs: "Sunucu log kayıtları"
deleteAll: "Tümünü sil"
showFixedPostForm: "Gönderi formunu pano üstünde görüntüle"
showFixedPostFormInChannel: "Gönderi formunu pano üstünde görüntüle (Kanallar)"
withRepliesByDefaultForNewlyFollowed: "Yeni takip edilen kullanıcıların yanıtlarını varsayılan olarak panoya dahil et"
showFixedPostForm: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle"
showFixedPostFormInChannel: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle (Kanallar)"
withRepliesByDefaultForNewlyFollowed: "Yeni takip edilen kullanıcıların yanıtlarını varsayılan olarak zaman çizelgesine dahil et"
newNoteRecived: "Yeni Not'lar var"
newNote: "Yeni Not"
sounds: "Sesler"
@ -1059,7 +1059,7 @@ achievements: "Başarılar"
gotInvalidResponseError: "Geçersiz sunucu yanıtı"
gotInvalidResponseErrorDescription: "Sunucu erişilemez durumda olabilir veya bakım çalışması yapılmaktadır. Lütfen daha sonra tekrar dene."
thisPostMayBeAnnoying: "Bu not başkalarını rahatsız edebilir."
thisPostMayBeAnnoyingHome: "Ana panoya gönder"
thisPostMayBeAnnoyingHome: "Ana zaman çizelgesine gönder"
thisPostMayBeAnnoyingCancel: "İptal"
thisPostMayBeAnnoyingIgnore: "Yine de gönder"
collapseRenotes: "Daha önce görüntülenen Renote'lari kısaltılmış olarak göster"
@ -1218,8 +1218,8 @@ showRepliesToOthersInTimeline: "Pano'da diğer kişilere verilen yanıtları
hideRepliesToOthersInTimeline: "Pano'dan diğer kişilerin yanıtlarını gizle"
showRepliesToOthersInTimelineAll: "Pano'da takip ettiğin herkesin diğerlerine verdiği yanıtları göster"
hideRepliesToOthersInTimelineAll: "Pano'da takip ettiğin herkesten diğer kişilere verilen yanıtları gizle"
confirmShowRepliesAll: "Bu işlem geri alınamaz. Takip ettiğin herkesin yanıtlarını panoda diğer kullanıcılara göstermek istiyor musun?"
confirmHideRepliesAll: "Bu işlem geri alınamaz. Şu anda takip ettiğin tüm kullanıcıların yanıtlarını panoda cidden göstermeyecek misin?"
confirmShowRepliesAll: "Bu işlem geri alınamaz. Takip ettiğin herkesin yanıtlarını zaman çizelgende diğer kullanıcılara göstermek istiyor musun?"
confirmHideRepliesAll: "Bu işlem geri alınamaz. Şu anda takip ettiğin tüm kullanıcıların yanıtlarını zaman tünelinde cidden göstermeyecek misin?"
externalServices: "Dış Hizmetler"
sourceCode: "Kaynak kodu"
sourceCodeIsNotYetProvided: "Kaynak kodu henüz mevcut değildir. Bu sorunu gidermek için yöneticiyle iletişime geçin."
@ -1570,9 +1570,9 @@ _initialTutorial:
description: "Burada, Misskey'i kullanmanın temellerini ve özelliklerini öğrenebilirsin."
_note:
title: "Not nedir?"
description: "Misskey'deki gönderiler “Notlar” olarak adlandırılır. Notlar panoda kronolojik olarak düzenlenir ve gerçek zamanlı olarak güncellenir."
description: "Misskey'deki gönderiler “Notlar” olarak adlandırılır. Notlar zaman çizelgesinde kronolojik olarak düzenlenir ve gerçek zamanlı olarak güncellenir."
reply: "Bir mesaja yanıt vermek için bu düğmeye tıklayın. Yanıtlara yanıt vermek de mümkündür, böylece konuşma bir konu başlığı gibi devam eder."
renote: "Bu notu kendi panonda paylaşabilirsin. Ayrıca yorumlarınla birlikte alıntı da yapabilirsin."
renote: "Bu notu kendi zaman çizelgende paylaşabilirsiniz. Ayrıca yorumlarınızla birlikte alıntı da yapabilirsin."
reaction: "Not'a tepkiler ekleyebilirsin. Daha fazla ayrıntı bir sonraki sayfada açıklanacak."
menu: "Not ayrıntılarını görüntüleyebilir, bağlantıları kopyalayabilir ve çeşitli diğer işlemleri gerçekleştirebilirsin."
_reaction:
@ -1640,7 +1640,7 @@ _serverSettings:
shortNameDescription: "Resmi adın uzun olması durumunda görüntülenebilen, örneğin adının kısaltması."
fanoutTimelineDescription: "Etkinleştirildiğinde Pano alma performansını büyük ölçüde artırır ve veritabanı yükünü azaltır. Bunun karşılığında Redis'in bellek kullanımı artacaktır. Sunucu belleği düşükse veya sunucu kararsızsa bunu devre dışı bırakmayı düşün."
fanoutTimelineDbFallback: "Veritabanına geri dön"
fanoutTimelineDbFallbackDescription: "Etkinleştirildiğinde, Pano önbelleğe alınmamışsa ek sorgular için veritabanına geri döner. Bu özelliği devre dışı bırakmak, geri dönüş sürecini ortadan kaldırarak sunucu yükünü daha da azaltır, ancak alınabilecek panoların aralığını sınırlar."
fanoutTimelineDbFallbackDescription: "Etkinleştirildiğinde, Pano önbelleğe alınmamışsa ek sorgular için veritabanına geri döner. Bu özelliği devre dışı bırakmak, geri dönüş sürecini ortadan kaldırarak sunucu yükünü daha da azaltır, ancak alınabilecek zaman çizelgelerinin aralığını sınırlar."
reactionsBufferingDescription: "Etkinleştirildiğinde, reaksiyon oluşturma sırasında performans büyük ölçüde artacak ve veritabanı üzerindeki yük azalacaktır. Ancak, Redis bellek kullanımı artacakt."
remoteNotesCleaning: "Uzak notların otomatik olarak temizlenmesi"
remoteNotesCleaning_description: "Etkinleştirildiğinde, kullanılmayan ve güncelliğini yitirmiş uzak notlar, veritabanının şişmesini önlemek için periyodik olarak temizlenecek."
@ -1668,7 +1668,6 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "Bazı mevcut ayarlar sıfırlanacaktır."
entrancePageStyle: "Giriş sayfası stili"
showTimelineForVisitor: "Panoyu göster"
showActivitiesForVisitor: "Aktiviteleri göster"
_userGeneratedContentsVisibilityForVisitor:
all: "Her şey halka açıktır."
localOnly: "Yalnızca yerel içerik yayınlanır, uzak içerik gizli tutulur."
@ -1877,7 +1876,7 @@ _achievements:
title: "Öz Referans"
description: "Kendi notunuzu alıntı yapın"
_htl20npm:
title: "Akış Panosu"
title: "Akış Zaman Çizelgesi"
description: "Ev zaman çizelgenizin hızı 20 npm'yi (dakika başına not sayısı) aşıyor mu?"
_viewInstanceChart:
title: "Analist"
@ -1966,7 +1965,7 @@ _role:
asBadge: "Rozet olarak göster"
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
isExplorable: "Rolü keşfedilebilir hale getir"
descriptionOfIsExplorable: "Bu rolün panosu ve bu role sahip kullanıcıların listesi, etkinleştirilirse kamuya açık hale getirilecek."
descriptionOfIsExplorable: "Bu rolün zaman çizelgesi ve bu role sahip kullanıcıların listesi, etkinleştirilirse kamuya açık hale getirilecek."
displayOrder: "Pozisyon"
descriptionOfDisplayOrder: "Sayı ne kadar yüksekse, UI pozisyonu da o kadar yüksek olur."
preserveAssignmentOnMoveAccount: "Geçiş sırasında rol atamalarını koruyun"
@ -1980,7 +1979,7 @@ _role:
high: "Yüksek"
_options:
gtlAvailable: "Global Pano'yu görüntüleyebilir"
ltlAvailable: "Yerel panoyu görüntüleyebilir"
ltlAvailable: "Yerel zaman çizelgesini görüntüleyebilir"
canPublicNote: "Halka açık notlar gönderebilir"
mentionMax: "Bir notta maksimum bahsetme sayısı"
canInvite: "Sunucu davet kodları oluşturabilir"
@ -2485,7 +2484,7 @@ _visibility:
public: "Halka açık"
publicDescription: "Notunuz tüm kullanıcılar tarafından görülebilir olacaktır."
home: "Pano"
homeDescription: "Yalnızca ana panoya gönder"
homeDescription: "Yalnızca ana zaman çizelgesine gönder"
followers: "Takipçiler"
followersDescription: "Sadece takipçilerine görünür hale getir"
specified: "Doğrudan"
@ -2532,7 +2531,7 @@ _exportOrImport:
userLists: "Kullanıcı listeleri"
excludeMutingUsers: "Sessize alınan kullanıcıları hariç tut"
excludeInactiveUsers: "Etkin olmayan kullanıcıları hariç tut"
withReplies: "İçe aktarılan kullanıcıların yanıtlarını panoya dahil edin"
withReplies: "İçe aktarılan kullanıcıların yanıtlarını zaman çizelgesine dahil edin"
_charts:
federation: "Federasyon"
apRequest: "Talepler"
@ -2926,7 +2925,7 @@ _reversi:
freeMatch: "Ücretsiz Eşleştirme"
lookingForPlayer: "Rakip aranıyor..."
gameCanceled: "Oyun iptal edildi."
shareToTlTheGameWhenStart: "Oyun başlatıldığında panoda paylaş"
shareToTlTheGameWhenStart: "Oyun başlatıldığında zaman çizelgesinde paylaş"
iStartedAGame: "Oyun başladı! #MisskeyReversi"
opponentHasSettingsChanged: "Rakip ayarlarını değiştirmiş."
allowIrregularRules: "Düzensiz kurallar (tamamen ücretsiz)"
@ -3154,7 +3153,7 @@ _clientPerformanceIssueTip:
_clip:
tip: "Klip, notları gruplandırmanıza olanak tanıyan bir özelliktir."
_userLists:
tip: "Listeler, oluşturulurken belirttiğin herhangi bir kullanıcıyı içerebilir. Oluşturulan liste, yalnızca belirtilen kullanıcıları gösteren bir pano olarak görüntülenebilir."
tip: "Listeler, oluşturulurken belirttiğin herhangi bir kullanıcıyı içerebilir. Oluşturulan liste, yalnızca belirtilen kullanıcıları gösteren bir zaman çizelgesi olarak görüntülenebilir."
watermark: "Filigran"
defaultPreset: "Varsayılan Ön Ayar"
_watermarkEditor:

View File

@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2025.9.1-alpha.2",
"version": "2025.9.0-alpha.1",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@10.16.0",
"packageManager": "pnpm@10.15.0",
"workspaces": [
"packages/frontend-shared",
"packages/frontend",
@ -62,22 +62,22 @@
"js-yaml": "4.1.0",
"postcss": "8.5.6",
"tar": "7.4.3",
"terser": "5.44.0",
"terser": "5.43.1",
"typescript": "5.9.2"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/js-yaml": "4.0.9",
"@types/node": "22.18.1",
"@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.42.0",
"@types/node": "22.17.2",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"cross-env": "7.0.3",
"cypress": "14.5.4",
"eslint": "9.35.0",
"eslint": "9.34.0",
"globals": "16.3.0",
"ncp": "2.0.0",
"pnpm": "10.16.0",
"start-server-and-test": "2.1.0"
"pnpm": "10.15.0",
"start-server-and-test": "2.0.13"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.22.0"

View File

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

View File

@ -1,24 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class ScheduledPost1758677617888 {
name = 'ScheduledPost1758677617888'
/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note_draft" ADD "scheduledAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`ALTER TABLE "note_draft" ADD "isActuallyScheduled" boolean NOT NULL DEFAULT false`);
}
/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "isActuallyScheduled"`);
await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "scheduledAt"`);
}
}

View File

@ -39,17 +39,17 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.13.5",
"@swc/core-darwin-x64": "1.13.5",
"@swc/core-darwin-arm64": "1.13.4",
"@swc/core-darwin-x64": "1.13.4",
"@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.13.5",
"@swc/core-linux-arm64-gnu": "1.13.5",
"@swc/core-linux-arm64-musl": "1.13.5",
"@swc/core-linux-x64-gnu": "1.13.5",
"@swc/core-linux-x64-musl": "1.13.5",
"@swc/core-win32-arm64-msvc": "1.13.5",
"@swc/core-win32-ia32-msvc": "1.13.5",
"@swc/core-win32-x64-msvc": "1.13.5",
"@swc/core-linux-arm-gnueabihf": "1.13.4",
"@swc/core-linux-arm64-gnu": "1.13.4",
"@swc/core-linux-arm64-musl": "1.13.4",
"@swc/core-linux-x64-gnu": "1.13.4",
"@swc/core-linux-x64-musl": "1.13.4",
"@swc/core-win32-arm64-msvc": "1.13.4",
"@swc/core-win32-ia32-msvc": "1.13.4",
"@swc/core-win32-x64-msvc": "1.13.4",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9",
@ -69,20 +69,20 @@
"utf-8-validate": "6.0.5"
},
"dependencies": {
"@aws-sdk/client-s3": "3.883.0",
"@aws-sdk/lib-storage": "3.883.0",
"@aws-sdk/client-s3": "3.873.0",
"@aws-sdk/lib-storage": "3.873.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.2",
"@fastify/cookie": "11.0.2",
"@fastify/cors": "10.1.0",
"@fastify/express": "4.0.2",
"@fastify/http-proxy": "10.0.2",
"@fastify/multipart": "9.2.1",
"@fastify/multipart": "9.0.3",
"@fastify/static": "8.2.0",
"@fastify/view": "10.0.2",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.3",
"@napi-rs/canvas": "0.1.79",
"@napi-rs/canvas": "0.1.77",
"@nestjs/common": "11.1.6",
"@nestjs/core": "11.1.6",
"@nestjs/testing": "11.1.6",
@ -93,7 +93,7 @@
"@sinonjs/fake-timers": "11.3.1",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.7.8",
"@swc/core": "1.13.5",
"@swc/core": "1.13.4",
"@twemoji/parser": "16.0.0",
"@types/redis-info": "3.0.3",
"accepts": "1.3.8",
@ -103,7 +103,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
"bullmq": "5.58.5",
"bullmq": "5.58.1",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.6.0",
@ -114,13 +114,13 @@
"content-disposition": "0.5.4",
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"fastify": "5.6.0",
"fastify": "5.5.0",
"fastify-raw-body": "5.0.0",
"feed": "4.2.2",
"file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.4",
"got": "14.4.8",
"got": "14.4.7",
"happy-dom": "16.8.1",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
@ -141,7 +141,7 @@
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.202508261828",
"ms": "3.0.0-canary.1",
"nanoid": "5.1.5",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
@ -175,7 +175,7 @@
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.27.8",
"systeminformation": "5.27.7",
"tinycolor2": "1.6.0",
"tmp": "0.2.5",
"tsc-alias": "1.8.16",
@ -210,7 +210,7 @@
"@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "22.18.1",
"@types/node": "22.17.2",
"@types/nodemailer": "6.4.19",
"@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
@ -222,7 +222,7 @@
"@types/ratelimiter": "3.4.6",
"@types/rename": "1.0.7",
"@types/sanitize-html": "2.16.0",
"@types/semver": "7.7.1",
"@types/semver": "7.7.0",
"@types/simple-oauth2": "5.0.7",
"@types/sinonjs__fake-timers": "8.1.5",
"@types/supertest": "6.0.3",
@ -231,8 +231,8 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.42.0",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.32.0",

View File

@ -7,7 +7,6 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml';
import { type FastifyServerOptions } from 'fastify';
import type * as Sentry from '@sentry/node';
import type * as SentryVue from '@sentry/vue';
import type { RedisOptions } from 'ioredis';
@ -28,7 +27,6 @@ type Source = {
url?: string;
port?: number;
socket?: string;
trustProxy?: FastifyServerOptions['trustProxy'];
chmodSocket?: string;
disableHsts?: boolean;
db: {
@ -120,7 +118,6 @@ export type Config = {
url: string;
port: number;
socket: string | undefined;
trustProxy: FastifyServerOptions['trustProxy'];
chmodSocket: string | undefined;
disableHsts: boolean | undefined;
db: {
@ -269,7 +266,6 @@ export function loadConfig(): Config {
url: url.origin,
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
socket: config.socket,
trustProxy: config.trustProxy,
chmodSocket: config.chmodSocket,
disableHsts: config.disableHsts,
host,

View File

@ -29,7 +29,7 @@ export class AiService {
}
@bindThis
public async detectSensitive(source: string | Buffer): Promise<nsfw.PredictionType[] | null> {
public async detectSensitive(path: string): Promise<nsfw.PredictionType[] | null> {
try {
if (isSupportedCpu === undefined) {
isSupportedCpu = await this.computeIsSupportedCpu();
@ -51,7 +51,7 @@ export class AiService {
});
}
const buffer = source instanceof Buffer ? source : await fs.promises.readFile(source);
const buffer = await fs.promises.readFile(path);
const image = await tf.node.decodeImage(buffer, 3) as any;
try {
const predictions = await this.model.classify(image);

View File

@ -21,7 +21,6 @@ import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import type { PredictionType } from 'nsfwjs';
import { isMimeImage } from '@/misc/is-mime-image.js';
export type FileInfo = {
size: number;
@ -205,7 +204,16 @@ export class FileInfoService {
return [sensitive, porn];
}
if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) {
if ([
'image/jpeg',
'image/png',
'image/webp',
].includes(mime)) {
const result = await this.aiService.detectSensitive(source);
if (result) {
[sensitive, porn] = judgePrediction(result);
}
} else if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) {
const [outDir, disposeOutDir] = await createTempDir();
try {
const command = FFmpeg()
@ -273,23 +281,6 @@ export class FileInfoService {
} finally {
disposeOutDir();
}
} else if (isMimeImage(mime, 'sharp-convertible-image-with-bmp')) {
/*
* tfjs-node sharp PNG
* 使299x299に事前にリサイズする
*/
const png = await (await sharpBmp(source, mime))
.resize(299, 299, {
withoutEnlargement: false,
})
.rotate()
.flatten({ background: { r: 119, g: 119, b: 119 } }) // 透過部分を18%グレーで塗りつぶす
.png()
.toBuffer();
const result = await this.aiService.detectSensitive(png);
if (result) {
[sensitive, porn] = judgePrediction(result);
}
}
return [sensitive, porn];

View File

@ -13,7 +13,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
import { extractHashtags } from '@/misc/extract-hashtags.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } from '@/models/Note.js';
import type { BlockingsRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiApp } from '@/models/App.js';
import { concat } from '@/misc/prelude/array.js';
@ -56,7 +56,6 @@ import { trackPromise } from '@/misc/promise-tracker.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
import { CacheService } from '@/core/CacheService.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -193,12 +192,6 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private idService: IdService,
@ -228,167 +221,6 @@ export class NoteCreateService implements OnApplicationShutdown {
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
}
@bindThis
public async fetchAndCreate(user: {
id: MiUser['id'];
username: MiUser['username'];
host: MiUser['host'];
isBot: MiUser['isBot'];
isCat: MiUser['isCat'];
}, data: {
createdAt: Date;
replyId: MiNote['id'] | null;
renoteId: MiNote['id'] | null;
fileIds: MiDriveFile['id'][];
text: string | null;
cw: string | null;
visibility: string;
visibleUserIds: MiUser['id'][];
channelId: MiChannel['id'] | null;
localOnly: boolean;
reactionAcceptance: MiNote['reactionAcceptance'];
poll: IPoll | null;
apMentions?: MinimumUser[] | null;
apHashtags?: string[] | null;
apEmojis?: string[] | null;
}): Promise<MiNote> {
const visibleUsers = data.visibleUserIds.length > 0 ? await this.usersRepository.findBy({
id: In(data.visibleUserIds),
}) : [];
let files: MiDriveFile[] = [];
if (data.fileIds.length > 0) {
files = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
userId: user.id,
fileIds: data.fileIds,
})
.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
.setParameters({ fileIds: data.fileIds })
.getMany();
if (files.length !== data.fileIds.length) {
throw new IdentifiableError('801c046c-5bf5-4234-ad2b-e78fc20a2ac7', 'No such file');
}
}
let renote: MiNote | null = null;
if (data.renoteId != null) {
// Fetch renote to note
renote = await this.notesRepository.findOne({
where: { id: data.renoteId },
relations: ['user', 'renote', 'reply'],
});
if (renote == null) {
throw new IdentifiableError('53983c56-e163-45a6-942f-4ddc485d4290', 'No such renote target');
} else if (isRenote(renote) && !isQuote(renote)) {
throw new IdentifiableError('bde24c37-121f-4e7d-980d-cec52f599f02', 'Cannot renote pure renote');
}
// Check blocking
if (renote.userId !== user.id) {
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: renote.userId,
blockeeId: user.id,
},
});
if (blockExist) {
throw new IdentifiableError('2b4fe776-4414-4a2d-ae39-f3418b8fd4d3', 'You have been blocked by the user');
}
}
if (renote.visibility === 'followers' && renote.userId !== user.id) {
// 他人のfollowers noteはreject
throw new IdentifiableError('90b9d6f0-893a-4fef-b0f1-e9a33989f71a', 'Renote target visibility');
} else if (renote.visibility === 'specified') {
// specified / direct noteはreject
throw new IdentifiableError('48d7a997-da5c-4716-b3c3-92db3f37bf7d', 'Renote target visibility');
}
if (renote.channelId && renote.channelId !== data.channelId) {
// チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
// リートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId });
if (renoteChannel == null) {
// リノートしたいノートが書き込まれているチャンネルが無い
throw new IdentifiableError('b060f9a6-8909-4080-9e0b-94d9fa6f6a77', 'No such channel');
} else if (!renoteChannel.allowRenoteToExternal) {
// リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合
throw new IdentifiableError('7e435f4a-780d-4cfc-a15a-42519bd6fb67', 'Channel does not allow renote to external');
}
}
}
let reply: MiNote | null = null;
if (data.replyId != null) {
// Fetch reply
reply = await this.notesRepository.findOne({
where: { id: data.replyId },
relations: ['user'],
});
if (reply == null) {
throw new IdentifiableError('60142edb-1519-408e-926d-4f108d27bee0', 'No such reply target');
} else if (isRenote(reply) && !isQuote(reply)) {
throw new IdentifiableError('f089e4e2-c0e7-4f60-8a23-e5a6bf786b36', 'Cannot reply to pure renote');
} else if (!await this.noteEntityService.isVisibleForMe(reply, user.id)) {
throw new IdentifiableError('11cd37b3-a411-4f77-8633-c580ce6a8dce', 'No such reply target');
} else if (reply.visibility === 'specified' && data.visibility !== 'specified') {
throw new IdentifiableError('ced780a1-2012-4caf-bc7e-a95a291294cb', 'Cannot reply to specified note with different visibility');
}
// Check blocking
if (reply.userId !== user.id) {
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: reply.userId,
blockeeId: user.id,
},
});
if (blockExist) {
throw new IdentifiableError('b0df6025-f2e8-44b4-a26a-17ad99104612', 'You have been blocked by the user');
}
}
}
if (data.poll) {
if (data.poll.expiresAt != null) {
if (data.poll.expiresAt.getTime() < Date.now()) {
throw new IdentifiableError('0c11c11e-0c8d-48e7-822c-76ccef660068', 'Poll expiration must be future time');
}
}
}
let channel: MiChannel | null = null;
if (data.channelId != null) {
channel = await this.channelsRepository.findOneBy({ id: data.channelId, isArchived: false });
if (channel == null) {
throw new IdentifiableError('bfa3905b-25f5-4894-b430-da331a490e4b', 'No such channel');
}
}
return this.create(user, {
createdAt: data.createdAt,
files: files,
poll: data.poll,
text: data.text,
reply,
renote,
cw: data.cw,
localOnly: data.localOnly,
reactionAcceptance: data.reactionAcceptance,
visibility: data.visibility,
visibleUsers,
channel,
apMentions: data.apMentions,
apHashtags: data.apHashtags,
apEmojis: data.apEmojis,
});
}
@bindThis
public async create(user: {
id: MiUser['id'];

View File

@ -5,18 +5,32 @@
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import type { noteVisibilities, noteReactionAcceptances } from '@/types.js';
import { DI } from '@/di-symbols.js';
import type { MiNoteDraft, NoteDraftsRepository, MiNote, MiDriveFile, MiChannel, UsersRepository, DriveFilesRepository, NotesRepository, BlockingsRepository, ChannelsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { IPoll } from '@/models/Poll.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isRenote, isQuote } from '@/misc/is-renote.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { QueueService } from '@/core/QueueService.js';
export type NoteDraftOptions = Omit<MiNoteDraft, 'id' | 'userId' | 'user' | 'reply' | 'renote' | 'channel'>;
export type NoteDraftOptions = {
replyId?: MiNote['id'] | null;
renoteId?: MiNote['id'] | null;
text?: string | null;
cw?: string | null;
localOnly?: boolean | null;
reactionAcceptance?: typeof noteReactionAcceptances[number];
visibility?: typeof noteVisibilities[number];
fileIds?: MiDriveFile['id'][];
visibleUserIds?: MiUser['id'][];
hashtag?: string;
channelId?: MiChannel['id'] | null;
poll?: (IPoll & { expiredAfter?: number | null }) | null;
};
@Injectable()
export class NoteDraftService {
@ -42,7 +56,6 @@ export class NoteDraftService {
private roleService: RoleService,
private idService: IdService,
private noteEntityService: NoteEntityService,
private queueService: QueueService,
) {
}
@ -59,43 +72,36 @@ export class NoteDraftService {
@bindThis
public async create(me: MiLocalUser, data: NoteDraftOptions): Promise<MiNoteDraft> {
//#region check draft limit
const policies = await this.roleService.getUserPolicies(me.id);
const currentCount = await this.noteDraftsRepository.countBy({
userId: me.id,
});
if (currentCount >= policies.noteDraftLimit) {
if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteDraftLimit) {
throw new IdentifiableError('9ee33bbe-fde3-4c71-9b51-e50492c6b9c8', 'Too many drafts');
}
if (data.isActuallyScheduled) {
const currentScheduledCount = await this.noteDraftsRepository.countBy({
userId: me.id,
isActuallyScheduled: true,
});
if (currentScheduledCount >= policies.scheduledNoteLimit) {
throw new IdentifiableError('c3275f19-4558-4c59-83e1-4f684b5fab66', 'Too many scheduled notes');
}
}
//#endregion
await this.validate(me, data);
const draft = await this.noteDraftsRepository.insertOne({
...data,
id: this.idService.gen(),
userId: me.id,
});
if (draft.scheduledAt && draft.isActuallyScheduled) {
this.schedule(draft);
if (data.poll) {
if (typeof data.poll.expiresAt === 'number') {
if (data.poll.expiresAt < Date.now()) {
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
}
} else if (typeof data.poll.expiredAfter === 'number') {
data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter);
}
}
const appliedDraft = await this.checkAndSetDraftNoteOptions(me, this.noteDraftsRepository.create(), data);
appliedDraft.id = this.idService.gen();
appliedDraft.userId = me.id;
const draft = this.noteDraftsRepository.save(appliedDraft);
return draft;
}
@bindThis
public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: Partial<NoteDraftOptions>): Promise<MiNoteDraft> {
public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: NoteDraftOptions): Promise<MiNoteDraft> {
const draft = await this.noteDraftsRepository.findOneBy({
id: draftId,
userId: me.id,
@ -105,36 +111,19 @@ export class NoteDraftService {
throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft');
}
//#region check draft limit
const policies = await this.roleService.getUserPolicies(me.id);
if (!draft.isActuallyScheduled && data.isActuallyScheduled) {
const currentScheduledCount = await this.noteDraftsRepository.countBy({
userId: me.id,
isActuallyScheduled: true,
});
if (currentScheduledCount >= policies.scheduledNoteLimit) {
throw new IdentifiableError('bacdf856-5c51-4159-b88a-804fa5103be5', 'Too many scheduled notes');
if (data.poll) {
if (typeof data.poll.expiresAt === 'number') {
if (data.poll.expiresAt < Date.now()) {
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
}
} else if (typeof data.poll.expiredAfter === 'number') {
data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter);
}
}
//#endregion
await this.validate(me, data);
const appliedDraft = await this.checkAndSetDraftNoteOptions(me, draft, data);
const updatedDraft = await this.noteDraftsRepository.createQueryBuilder().update()
.set(data)
.where('id = :id', { id: draftId })
.returning('*')
.execute()
.then((response) => response.raw[0]);
this.clearSchedule(draftId).then(() => {
if (updatedDraft.scheduledAt != null && updatedDraft.isActuallyScheduled) {
this.schedule(updatedDraft);
}
});
return updatedDraft;
return await this.noteDraftsRepository.save(appliedDraft);
}
@bindThis
@ -149,8 +138,6 @@ export class NoteDraftService {
}
await this.noteDraftsRepository.delete(draft.id);
this.clearSchedule(draftId);
}
@bindThis
@ -167,20 +154,27 @@ export class NoteDraftService {
return draft;
}
// 関連エンティティを取得し紐づける部分を共通化する
@bindThis
public async validate(
public async checkAndSetDraftNoteOptions(
me: MiLocalUser,
data: Partial<NoteDraftOptions>,
): Promise<void> {
if (data.pollExpiresAt != null) {
if (data.pollExpiresAt.getTime() < Date.now()) {
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
}
draft: MiNoteDraft,
data: NoteDraftOptions,
): Promise<MiNoteDraft> {
data.visibility ??= 'public';
data.localOnly ??= false;
if (data.reactionAcceptance === undefined) data.reactionAcceptance = null;
if (data.channelId != null) {
data.visibility = 'public';
data.visibleUserIds = [];
data.localOnly = true;
}
let appliedDraft = draft;
//#region visibleUsers
let visibleUsers: MiUser[] = [];
if (data.visibleUserIds != null && data.visibleUserIds.length > 0) {
if (data.visibleUserIds != null) {
visibleUsers = await this.usersRepository.findBy({
id: In(data.visibleUserIds),
});
@ -190,7 +184,7 @@ export class NoteDraftService {
//#region files
let files: MiDriveFile[] = [];
const fileIds = data.fileIds ?? null;
if (fileIds != null && fileIds.length > 0) {
if (fileIds != null) {
files = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
userId: me.id,
@ -294,37 +288,27 @@ export class NoteDraftService {
}
}
//#endregion
}
@bindThis
public async schedule(draft: MiNoteDraft): Promise<void> {
if (!draft.isActuallyScheduled) return;
if (draft.scheduledAt == null) return;
if (draft.scheduledAt.getTime() <= Date.now()) return;
appliedDraft = {
...appliedDraft,
visibility: data.visibility,
cw: data.cw ?? null,
fileIds: fileIds ?? [],
replyId: data.replyId ?? null,
renoteId: data.renoteId ?? null,
channelId: data.channelId ?? null,
text: data.text ?? null,
hashtag: data.hashtag ?? null,
hasPoll: data.poll != null,
pollChoices: data.poll ? data.poll.choices : [],
pollMultiple: data.poll ? data.poll.multiple : false,
pollExpiresAt: data.poll ? data.poll.expiresAt : null,
pollExpiredAfter: data.poll ? data.poll.expiredAfter ?? null : null,
visibleUserIds: data.visibleUserIds ?? [],
localOnly: data.localOnly,
reactionAcceptance: data.reactionAcceptance,
} satisfies MiNoteDraft;
const delay = draft.scheduledAt.getTime() - Date.now();
this.queueService.postScheduledNoteQueue.add(draft.id, {
noteDraftId: draft.id,
}, {
delay,
removeOnComplete: {
age: 3600 * 24 * 7, // keep up to 7 days
count: 30,
},
removeOnFail: {
age: 3600 * 24 * 7, // keep up to 7 days
count: 100,
},
});
}
@bindThis
public async clearSchedule(draftId: MiNoteDraft['id']): Promise<void> {
const jobs = await this.queueService.postScheduledNoteQueue.getJobs(['delayed', 'waiting', 'active']);
for (const job of jobs) {
if (job.data.noteDraftId === draftId) {
await job.remove();
}
}
return appliedDraft;
}
}

View File

@ -16,13 +16,11 @@ import {
RelationshipJobData,
UserWebhookDeliverJobData,
SystemWebhookDeliverJobData,
PostScheduledNoteJobData,
} from '../queue/types.js';
import type { Provider } from '@nestjs/common';
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
export type PostScheduledNoteQueue = Bull.Queue<PostScheduledNoteJobData>;
export type DeliverQueue = Bull.Queue<DeliverJobData>;
export type InboxQueue = Bull.Queue<InboxJobData>;
export type DbQueue = Bull.Queue;
@ -43,12 +41,6 @@ const $endedPollNotification: Provider = {
inject: [DI.config],
};
const $postScheduledNote: Provider = {
provide: 'queue:postScheduledNote',
useFactory: (config: Config) => new Bull.Queue(QUEUE.POST_SCHEDULED_NOTE, baseQueueOptions(config, QUEUE.POST_SCHEDULED_NOTE)),
inject: [DI.config],
};
const $deliver: Provider = {
provide: 'queue:deliver',
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
@ -97,7 +89,6 @@ const $systemWebhookDeliver: Provider = {
providers: [
$system,
$endedPollNotification,
$postScheduledNote,
$deliver,
$inbox,
$db,
@ -109,7 +100,6 @@ const $systemWebhookDeliver: Provider = {
exports: [
$system,
$endedPollNotification,
$postScheduledNote,
$deliver,
$inbox,
$db,
@ -123,7 +113,6 @@ export class QueueModule implements OnApplicationShutdown {
constructor(
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@ -140,7 +129,6 @@ export class QueueModule implements OnApplicationShutdown {
await Promise.all([
this.systemQueue.close(),
this.endedPollNotificationQueue.close(),
this.postScheduledNoteQueue.close(),
this.deliverQueue.close(),
this.inboxQueue.close(),
this.dbQueue.close(),

View File

@ -31,7 +31,6 @@ import type {
DbQueue,
DeliverQueue,
EndedPollNotificationQueue,
PostScheduledNoteQueue,
InboxQueue,
ObjectStorageQueue,
RelationshipQueue,
@ -45,7 +44,6 @@ import type * as Bull from 'bullmq';
export const QUEUE_TYPES = [
'system',
'endedPollNotification',
'postScheduledNote',
'deliver',
'inbox',
'db',
@ -94,7 +92,6 @@ export class QueueService {
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@ -720,7 +717,6 @@ export class QueueService {
switch (type) {
case 'system': return this.systemQueue;
case 'endedPollNotification': return this.endedPollNotificationQueue;
case 'postScheduledNote': return this.postScheduledNoteQueue;
case 'deliver': return this.deliverQueue;
case 'inbox': return this.inboxQueue;
case 'db': return this.dbQueue;
@ -760,8 +756,8 @@ export class QueueService {
@bindThis
public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
const job = await queue.getJob(jobId);
if (job != null) {
const job: Bull.Job | null = await queue.getJob(jobId);
if (job) {
if (job.finishedOn != null) {
await job.retry();
} else {
@ -773,8 +769,8 @@ export class QueueService {
@bindThis
public async queueRemoveJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
const job = await queue.getJob(jobId);
if (job != null) {
const job: Bull.Job | null = await queue.getJob(jobId);
if (job) {
await job.remove();
}
}
@ -807,8 +803,8 @@ export class QueueService {
@bindThis
public async queueGetJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
const job = await queue.getJob(jobId);
if (job != null) {
const job: Bull.Job | null = await queue.getJob(jobId);
if (job) {
return this.packJobData(job);
} else {
throw new Error(`Job not found: ${jobId}`);

View File

@ -69,7 +69,6 @@ export type RolePolicies = {
chatAvailability: 'available' | 'readonly' | 'unavailable';
uploadableFileTypes: string[];
noteDraftLimit: number;
scheduledNoteLimit: number;
watermarkAvailable: boolean;
};
@ -102,22 +101,20 @@ export const DEFAULT_POLICIES: RolePolicies = {
userEachUserListsLimit: 50,
rateLimitFactor: 1,
avatarDecorationLimit: 1,
canImportAntennas: false,
canImportBlocking: false,
canImportFollowing: false,
canImportMuting: false,
canImportUserLists: false,
canImportAntennas: true,
canImportBlocking: true,
canImportFollowing: true,
canImportMuting: true,
canImportUserLists: true,
chatAvailability: 'available',
uploadableFileTypes: [
'text/plain',
'text/csv',
'application/json',
'image/*',
'video/*',
'audio/*',
],
noteDraftLimit: 10,
scheduledNoteLimit: 1,
watermarkAvailable: true,
};
@ -442,7 +439,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
return [...set];
}),
noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)),
scheduledNoteLimit: calc('scheduledNoteLimit', vs => Math.max(...vs)),
watermarkAvailable: calc('watermarkAvailable', vs => vs.some(v => v === true)),
};
}

View File

@ -117,7 +117,6 @@ export class MetaEntityService {
ratio: ad.ratio,
imageUrl: ad.imageUrl,
dayOfWeek: ad.dayOfWeek,
isSensitive: ad.isSensitive ? true : undefined,
})),
notesPerOneAd: instance.notesPerOneAd,
enableEmail: instance.enableEmail,

View File

@ -105,8 +105,6 @@ export class NoteDraftEntityService implements OnModuleInit {
const packed: Packed<'NoteDraft'> = await awaitAll({
id: noteDraft.id,
createdAt: this.idService.parse(noteDraft.id).date.toISOString(),
scheduledAt: noteDraft.scheduledAt?.getTime() ?? null,
isActuallyScheduled: noteDraft.isActuallyScheduled,
userId: noteDraft.userId,
user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me),
text: text,
@ -114,13 +112,13 @@ export class NoteDraftEntityService implements OnModuleInit {
visibility: noteDraft.visibility,
localOnly: noteDraft.localOnly,
reactionAcceptance: noteDraft.reactionAcceptance,
visibleUserIds: noteDraft.visibleUserIds,
hashtag: noteDraft.hashtag,
visibleUserIds: noteDraft.visibility === 'specified' ? noteDraft.visibleUserIds : undefined,
hashtag: noteDraft.hashtag ?? undefined,
fileIds: noteDraft.fileIds,
files: packedFiles != null ? this.packAttachedFiles(noteDraft.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(noteDraft.fileIds),
replyId: noteDraft.replyId,
renoteId: noteDraft.renoteId,
channelId: noteDraft.channelId,
channelId: noteDraft.channelId ?? undefined,
channel: channel ? {
id: channel.id,
name: channel.name,
@ -129,12 +127,6 @@ export class NoteDraftEntityService implements OnModuleInit {
allowRenoteToExternal: channel.allowRenoteToExternal,
userId: channel.userId,
} : undefined,
poll: noteDraft.hasPoll ? {
choices: noteDraft.pollChoices,
multiple: noteDraft.pollMultiple,
expiresAt: noteDraft.pollExpiresAt?.toISOString(),
expiredAfter: noteDraft.pollExpiredAfter,
} : null,
...(opts.detail ? {
reply: noteDraft.replyId ? nullIfEntityNotFound(this.noteEntityService.pack(noteDraft.replyId, me, {
@ -146,6 +138,13 @@ export class NoteDraftEntityService implements OnModuleInit {
detail: true,
skipHide: opts.skipHide,
})) : undefined,
poll: noteDraft.hasPoll ? {
choices: noteDraft.pollChoices,
multiple: noteDraft.pollMultiple,
expiresAt: noteDraft.pollExpiresAt?.toISOString(),
expiredAfter: noteDraft.pollExpiredAfter,
} : undefined,
} : {} ),
});

View File

@ -21,18 +21,7 @@ import type { OnModuleInit } from '@nestjs/common';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set([
'note',
'mention',
'reply',
'renote',
'renote:grouped',
'quote',
'reaction',
'reaction:grouped',
'pollEnded',
'scheduledNotePosted',
] as (typeof groupedNotificationTypes[number])[]);
const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]);
@Injectable()
export class NotificationEntityService implements OnModuleInit {

View File

@ -54,17 +54,10 @@ export class MiAd {
length: 8192, nullable: false,
})
public memo: string;
@Column('integer', {
default: 0, nullable: false,
})
public dayOfWeek: number;
@Column('boolean', {
default: false,
})
public isSensitive: boolean;
constructor(data: Partial<MiAd>) {
if (data == null) return;

View File

@ -126,7 +126,7 @@ export class MiNoteDraft {
@JoinColumn()
public channel: MiChannel | null;
//#region 以下、Pollについて追加
// 以下、Pollについて追加
@Column('boolean', {
default: false,
@ -151,15 +151,13 @@ export class MiNoteDraft {
})
public pollExpiredAfter: number | null;
//#endregion
// ここまで追加
@Column('timestamp with time zone', {
nullable: true,
})
public scheduledAt: Date | null;
constructor(data: Partial<MiNoteDraft>) {
if (data == null) return;
@Column('boolean', {
default: false,
})
public isActuallyScheduled: boolean;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -9,7 +9,6 @@ import { MiNote } from './Note.js';
import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js';
import { MiDriveFile } from './DriveFile.js';
import { MiNoteDraft } from './NoteDraft.js';
// misskey-js の notificationTypes と同期すべし
export type MiNotification = {
@ -61,16 +60,6 @@ export type MiNotification = {
createdAt: string;
notifierId: MiUser['id'];
noteId: MiNote['id'];
} | {
type: 'scheduledNotePosted';
id: string;
createdAt: string;
noteId: MiNote['id'];
} | {
type: 'scheduledNotePostFailed';
id: string;
createdAt: string;
noteDraftId: MiNoteDraft['id'];
} | {
type: 'receiveFollowRequest';
id: string;

View File

@ -60,10 +60,5 @@ export const packedAdSchema = {
optional: false,
nullable: false,
},
isSensitive: {
type: 'boolean',
optional: false,
nullable: false,
},
},
} as const;

View File

@ -195,10 +195,6 @@ export const packedMetaLiteSchema = {
type: 'integer',
optional: false, nullable: false,
},
isSensitive: {
type: 'boolean',
optional: true, nullable: false,
},
},
},
},

View File

@ -23,7 +23,7 @@ export const packedNoteDraftSchema = {
},
cw: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
userId: {
type: 'string',
@ -37,23 +37,27 @@ export const packedNoteDraftSchema = {
},
replyId: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
format: 'id',
example: 'xxxxxxxxxx',
},
renoteId: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
format: 'id',
example: 'xxxxxxxxxx',
},
reply: {
type: 'object',
optional: true, nullable: true,
ref: 'Note',
description: 'The reply target note contents if exists. If the reply target has been deleted since the draft was created, this will be null while replyId is not null.',
},
renote: {
type: 'object',
optional: true, nullable: true,
ref: 'Note',
description: 'The renote target note contents if exists. If the renote target has been deleted since the draft was created, this will be null while renoteId is not null.',
},
visibility: {
type: 'string',
@ -62,7 +66,7 @@ export const packedNoteDraftSchema = {
},
visibleUserIds: {
type: 'array',
optional: false, nullable: false,
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
@ -71,7 +75,7 @@ export const packedNoteDraftSchema = {
},
fileIds: {
type: 'array',
optional: false, nullable: false,
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
@ -89,11 +93,11 @@ export const packedNoteDraftSchema = {
},
hashtag: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: false,
},
poll: {
type: 'object',
optional: false, nullable: true,
optional: true, nullable: true,
properties: {
expiresAt: {
type: 'string',
@ -120,8 +124,9 @@ export const packedNoteDraftSchema = {
},
channelId: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
format: 'id',
example: 'xxxxxxxxxx',
},
channel: {
type: 'object',
@ -155,20 +160,12 @@ export const packedNoteDraftSchema = {
},
localOnly: {
type: 'boolean',
optional: false, nullable: false,
optional: true, nullable: false,
},
reactionAcceptance: {
type: 'string',
optional: false, nullable: true,
enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null],
},
scheduledAt: {
type: 'number',
optional: false, nullable: true,
},
isActuallyScheduled: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;

View File

@ -207,36 +207,6 @@ export const packedNotificationSchema = {
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['scheduledNotePosted'],
},
note: {
type: 'object',
ref: 'Note',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {
...baseSchema.properties,
type: {
type: 'string',
optional: false, nullable: false,
enum: ['scheduledNotePostFailed'],
},
noteDraft: {
type: 'object',
ref: 'NoteDraft',
optional: false, nullable: false,
},
},
}, {
type: 'object',
properties: {

View File

@ -317,10 +317,6 @@ export const packedRolePoliciesSchema = {
type: 'integer',
optional: false, nullable: false,
},
scheduledNoteLimit: {
type: 'integer',
optional: false, nullable: false,
},
watermarkAvailable: {
type: 'boolean',
optional: false, nullable: false,

View File

@ -609,8 +609,6 @@ export const packedMeDetailedOnlySchema = {
quote: { optional: true, ...notificationRecieveConfig },
reaction: { optional: true, ...notificationRecieveConfig },
pollEnded: { optional: true, ...notificationRecieveConfig },
scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig },

View File

@ -10,7 +10,6 @@ import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
@ -80,7 +79,6 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
UserWebhookDeliverProcessorService,
SystemWebhookDeliverProcessorService,
EndedPollNotificationProcessorService,
PostScheduledNoteProcessorService,
DeliverProcessorService,
InboxProcessorService,
AggregateRetentionProcessorService,

View File

@ -14,7 +14,6 @@ import { CheckModeratorsActivityProcessorService } from '@/queue/processors/Chec
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
@ -86,7 +85,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
private relationshipQueueWorker: Bull.Worker;
private objectStorageQueueWorker: Bull.Worker;
private endedPollNotificationQueueWorker: Bull.Worker;
private postScheduledNoteQueueWorker: Bull.Worker;
constructor(
@Inject(DI.config)
@ -96,7 +94,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
private postScheduledNoteProcessorService: PostScheduledNoteProcessorService,
private deliverProcessorService: DeliverProcessorService,
private inboxProcessorService: InboxProcessorService,
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
@ -523,21 +520,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
});
}
//#endregion
//#region post scheduled note
{
this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => {
if (this.config.sentryForBackend) {
return Sentry.startSpan({ name: 'Queue: PostScheduledNote' }, () => this.postScheduledNoteProcessorService.process(job));
} else {
return this.postScheduledNoteProcessorService.process(job);
}
}, {
...baseWorkerOptions(this.config, QUEUE.POST_SCHEDULED_NOTE),
autorun: false,
});
}
//#endregion
}
@bindThis
@ -552,7 +534,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker.run(),
this.objectStorageQueueWorker.run(),
this.endedPollNotificationQueueWorker.run(),
this.postScheduledNoteQueueWorker.run(),
]);
}
@ -568,7 +549,6 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker.close(),
this.objectStorageQueueWorker.close(),
this.endedPollNotificationQueueWorker.close(),
this.postScheduledNoteQueueWorker.close(),
]);
}

View File

@ -12,7 +12,6 @@ export const QUEUE = {
INBOX: 'inbox',
SYSTEM: 'system',
ENDED_POLL_NOTIFICATION: 'endedPollNotification',
POST_SCHEDULED_NOTE: 'postScheduledNote',
DB: 'db',
RELATIONSHIP: 'relationship',
OBJECT_STORAGE: 'objectStorage',

View File

@ -1,72 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { NoteDraftsRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { PostScheduledNoteJobData } from '../types.js';
@Injectable()
export class PostScheduledNoteProcessorService {
private logger: Logger;
constructor(
@Inject(DI.noteDraftsRepository)
private noteDraftsRepository: NoteDraftsRepository,
private noteCreateService: NoteCreateService,
private notificationService: NotificationService,
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('post-scheduled-note');
}
@bindThis
public async process(job: Bull.Job<PostScheduledNoteJobData>): Promise<void> {
const draft = await this.noteDraftsRepository.findOne({ where: { id: job.data.noteDraftId }, relations: ['user'] });
if (draft == null || draft.user == null || draft.scheduledAt == null || !draft.isActuallyScheduled) {
return;
}
try {
const note = await this.noteCreateService.fetchAndCreate(draft.user, {
createdAt: new Date(),
fileIds: draft.fileIds,
poll: draft.hasPoll ? {
choices: draft.pollChoices,
multiple: draft.pollMultiple,
expiresAt: draft.pollExpiredAfter ? new Date(Date.now() + draft.pollExpiredAfter) : draft.pollExpiresAt ? new Date(draft.pollExpiresAt) : null,
} : null,
text: draft.text ?? null,
replyId: draft.replyId,
renoteId: draft.renoteId,
cw: draft.cw,
localOnly: draft.localOnly,
reactionAcceptance: draft.reactionAcceptance,
visibility: draft.visibility,
visibleUserIds: draft.visibleUserIds,
channelId: draft.channelId,
});
// await不要
this.noteDraftsRepository.remove(draft);
// await不要
this.notificationService.createNotification(draft.userId, 'scheduledNotePosted', {
noteId: note.id,
});
} catch (err) {
this.notificationService.createNotification(draft.userId, 'scheduledNotePostFailed', {
noteDraftId: draft.id,
});
}
}
}

View File

@ -109,10 +109,6 @@ export type EndedPollNotificationJobData = {
noteId: MiNote['id'];
};
export type PostScheduledNoteJobData = {
noteDraftId: string;
};
export type SystemWebhookDeliverJobData<T extends SystemWebhookEventType = SystemWebhookEventType> = {
type: T;
content: SystemWebhookPayload<T>;

View File

@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
@bindThis
public async launch(): Promise<void> {
const fastify = Fastify({
trustProxy: this.config.trustProxy ?? true,
trustProxy: true,
logger: false,
});
this.#fastify = fastify;

View File

@ -176,17 +176,6 @@ export class ApiServerService {
}
});
fastify.all('/clear-browser-cache', (request, reply) => {
if (['GET', 'POST'].includes(request.method)) {
reply.header('Clear-Site-Data', '"cache", "prefetchCache", "prerenderCache", "executionContexts"');
reply.code(204);
reply.send();
} else {
reply.code(405);
reply.send();
}
});
// Make sure any unknown path under /api returns HTTP 404 Not Found,
// because otherwise ClientServerService will return the base client HTML
// page with HTTP 200.

View File

@ -34,22 +34,13 @@ export const meta = {
res: {
type: 'object',
optional: false, nullable: false,
allOf: [
{
type: 'object',
ref: 'MeDetailed',
},
{
type: 'object',
optional: false, nullable: false,
properties: {
token: {
type: 'string',
optional: false, nullable: false,
},
},
}
],
},
} as const;

View File

@ -36,7 +36,6 @@ export const paramDef = {
startsAt: { type: 'integer' },
imageUrl: { type: 'string', minLength: 1 },
dayOfWeek: { type: 'integer' },
isSensitive: { type: 'boolean' },
},
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
} as const;
@ -56,7 +55,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt),
dayOfWeek: ps.dayOfWeek,
isSensitive: ps.isSensitive,
url: ps.url,
imageUrl: ps.imageUrl,
priority: ps.priority,
@ -75,7 +73,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
isSensitive: ad.isSensitive,
url: ad.url,
imageUrl: ad.imageUrl,
priority: ad.priority,

View File

@ -63,7 +63,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
isSensitive: ad.isSensitive,
url: ad.url,
imageUrl: ad.imageUrl,
memo: ad.memo,

View File

@ -39,7 +39,6 @@ export const paramDef = {
expiresAt: { type: 'integer' },
startsAt: { type: 'integer' },
dayOfWeek: { type: 'integer' },
isSensitive: { type: 'boolean' },
},
required: ['id'],
} as const;
@ -67,7 +66,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
dayOfWeek: ps.dayOfWeek,
isSensitive: ps.isSensitive,
});
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });

View File

@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, PostScheduledNoteQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
export const meta = {
tags: ['admin'],
@ -49,7 +49,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,

View File

@ -103,8 +103,6 @@ export const meta = {
quote: { optional: true, ...notificationRecieveConfig },
reaction: { optional: true, ...notificationRecieveConfig },
pollEnded: { optional: true, ...notificationRecieveConfig },
scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig },

View File

@ -18,9 +18,9 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { ApiError } from '../../error.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['federation'],

View File

@ -209,8 +209,6 @@ export const paramDef = {
quote: notificationRecieveConfig,
reaction: notificationRecieveConfig,
pollEnded: notificationRecieveConfig,
scheduledNotePosted: notificationRecieveConfig,
scheduledNotePostFailed: notificationRecieveConfig,
receiveFollowRequest: notificationRecieveConfig,
followRequestAccepted: notificationRecieveConfig,
roleAssigned: notificationRecieveConfig,

View File

@ -6,10 +6,17 @@
import ms from 'ms';
import { In } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { MiUser } from '@/models/User.js';
import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiNote } from '@/models/Note.js';
import type { MiChannel } from '@/models/Channel.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { DI } from '@/di-symbols.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApiError } from '../../error.js';
@ -216,28 +223,168 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
private noteEntityService: NoteEntityService,
private noteCreateService: NoteCreateService,
) {
super(meta, paramDef, async (ps, me) => {
let visibleUsers: MiUser[] = [];
if (ps.visibleUserIds) {
visibleUsers = await this.usersRepository.findBy({
id: In(ps.visibleUserIds),
});
}
let files: MiDriveFile[] = [];
const fileIds = ps.fileIds ?? ps.mediaIds ?? null;
if (fileIds != null) {
files = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.userId = :userId AND file.id IN (:...fileIds)', {
userId: me.id,
fileIds,
})
.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
.setParameters({ fileIds })
.getMany();
if (files.length !== fileIds.length) {
throw new ApiError(meta.errors.noSuchFile);
}
}
let renote: MiNote | null = null;
if (ps.renoteId != null) {
// Fetch renote to note
renote = await this.notesRepository.findOne({
where: { id: ps.renoteId },
relations: ['user', 'renote', 'reply'],
});
if (renote == null) {
throw new ApiError(meta.errors.noSuchRenoteTarget);
} else if (isRenote(renote) && !isQuote(renote)) {
throw new ApiError(meta.errors.cannotReRenote);
}
// Check blocking
if (renote.userId !== me.id) {
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: renote.userId,
blockeeId: me.id,
},
});
if (blockExist) {
throw new ApiError(meta.errors.youHaveBeenBlocked);
}
}
if (renote.visibility === 'followers' && renote.userId !== me.id) {
// 他人のfollowers noteはreject
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
} else if (renote.visibility === 'specified') {
// specified / direct noteはreject
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
}
if (renote.channelId && renote.channelId !== ps.channelId) {
// チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック
// リートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する
const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId });
if (renoteChannel == null) {
// リノートしたいノートが書き込まれているチャンネルが無い
throw new ApiError(meta.errors.noSuchChannel);
} else if (!renoteChannel.allowRenoteToExternal) {
// リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合
throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel);
}
}
}
let reply: MiNote | null = null;
if (ps.replyId != null) {
// Fetch reply
reply = await this.notesRepository.findOne({
where: { id: ps.replyId },
relations: ['user'],
});
if (reply == null) {
throw new ApiError(meta.errors.noSuchReplyTarget);
} else if (isRenote(reply) && !isQuote(reply)) {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
} else if (reply.visibility === 'specified' && ps.visibility !== 'specified') {
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
}
// Check blocking
if (reply.userId !== me.id) {
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: reply.userId,
blockeeId: me.id,
},
});
if (blockExist) {
throw new ApiError(meta.errors.youHaveBeenBlocked);
}
}
}
if (ps.poll) {
if (typeof ps.poll.expiresAt === 'number') {
if (ps.poll.expiresAt < Date.now()) {
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
}
} else if (typeof ps.poll.expiredAfter === 'number') {
ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter;
}
}
let channel: MiChannel | null = null;
if (ps.channelId != null) {
channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false });
if (channel == null) {
throw new ApiError(meta.errors.noSuchChannel);
}
}
// 投稿を作成
try {
const note = await this.noteCreateService.fetchAndCreate(me, {
const note = await this.noteCreateService.create(me, {
createdAt: new Date(),
fileIds: ps.fileIds ?? ps.mediaIds ?? [],
files: files,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiredAfter ? new Date(Date.now() + ps.poll.expiredAfter) : ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : null,
text: ps.text ?? null,
replyId: ps.replyId ?? null,
renoteId: ps.renoteId ?? null,
cw: ps.cw ?? null,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : undefined,
text: ps.text ?? undefined,
reply,
renote,
cw: ps.cw,
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
visibleUserIds: ps.visibleUserIds ?? [],
channelId: ps.channelId ?? null,
visibleUsers,
channel,
apMentions: ps.noExtractMentions ? [] : undefined,
apHashtags: ps.noExtractHashtags ? [] : undefined,
apEmojis: ps.noExtractEmojis ? [] : undefined,
@ -246,46 +393,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return {
createdNote: await this.noteEntityService.pack(note, me),
};
} catch (err) {
} catch (e) {
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
if (err instanceof IdentifiableError) {
if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
if (e instanceof IdentifiableError) {
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
throw new ApiError(meta.errors.containsProhibitedWords);
} else if (err.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
throw new ApiError(meta.errors.containsTooManyMentions);
} else if (err.id === '801c046c-5bf5-4234-ad2b-e78fc20a2ac7') {
throw new ApiError(meta.errors.noSuchFile);
} else if (err.id === '53983c56-e163-45a6-942f-4ddc485d4290') {
throw new ApiError(meta.errors.noSuchRenoteTarget);
} else if (err.id === 'bde24c37-121f-4e7d-980d-cec52f599f02') {
throw new ApiError(meta.errors.cannotReRenote);
} else if (err.id === '2b4fe776-4414-4a2d-ae39-f3418b8fd4d3') {
throw new ApiError(meta.errors.youHaveBeenBlocked);
} else if (err.id === '90b9d6f0-893a-4fef-b0f1-e9a33989f71a') {
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
} else if (err.id === '48d7a997-da5c-4716-b3c3-92db3f37bf7d') {
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
} else if (err.id === 'b060f9a6-8909-4080-9e0b-94d9fa6f6a77') {
throw new ApiError(meta.errors.noSuchChannel);
} else if (err.id === '7e435f4a-780d-4cfc-a15a-42519bd6fb67') {
throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel);
} else if (err.id === '60142edb-1519-408e-926d-4f108d27bee0') {
throw new ApiError(meta.errors.noSuchReplyTarget);
} else if (err.id === 'f089e4e2-c0e7-4f60-8a23-e5a6bf786b36') {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
} else if (err.id === '11cd37b3-a411-4f77-8633-c580ce6a8dce') {
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
} else if (err.id === 'ced780a1-2012-4caf-bc7e-a95a291294cb') {
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
} else if (err.id === 'b0df6025-f2e8-44b4-a26a-17ad99104612') {
throw new ApiError(meta.errors.youHaveBeenBlocked);
} else if (err.id === '0c11c11e-0c8d-48e7-822c-76ccef660068') {
throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll);
} else if (err.id === 'bfa3905b-25f5-4894-b430-da331a490e4b') {
throw new ApiError(meta.errors.noSuchChannel);
}
}
throw err;
throw e;
}
});
}

View File

@ -124,12 +124,6 @@ export const meta = {
id: '9ee33bbe-fde3-4c71-9b51-e50492c6b9c8',
},
tooManyScheduledNotes: {
message: 'You cannot create scheduled notes any more.',
code: 'TOO_MANY_SCHEDULED_NOTES',
id: '22ae69eb-09e3-4541-a850-773cfa45e693',
},
cannotRenoteToExternal: {
message: 'Cannot Renote to External.',
code: 'CANNOT_RENOTE_TO_EXTERNAL',
@ -168,7 +162,7 @@ export const paramDef = {
fileIds: {
type: 'array',
uniqueItems: true,
minItems: 0,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
@ -189,10 +183,8 @@ export const paramDef = {
},
required: ['choices'],
},
scheduledAt: { type: 'integer', nullable: true },
isActuallyScheduled: { type: 'boolean', default: false },
},
required: ['visibility', 'visibleUserIds', 'cw', 'hashtag', 'localOnly', 'reactionAcceptance', 'replyId', 'renoteId', 'channelId', 'text', 'fileIds', 'poll', 'scheduledAt', 'isActuallyScheduled'],
required: [],
} as const;
@Injectable()
@ -204,23 +196,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
const draft = await this.noteDraftService.create(me, {
fileIds: ps.fileIds,
pollChoices: ps.poll?.choices ?? [],
pollMultiple: ps.poll?.multiple ?? false,
pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null,
pollExpiredAfter: ps.poll?.expiredAfter ?? null,
hasPoll: ps.poll != null,
text: ps.text,
replyId: ps.replyId,
renoteId: ps.renoteId,
cw: ps.cw,
hashtag: ps.hashtag,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
expiredAfter: ps.poll.expiredAfter ?? null,
} : undefined,
text: ps.text ?? null,
replyId: ps.replyId ?? undefined,
renoteId: ps.renoteId ?? undefined,
cw: ps.cw ?? null,
...(ps.hashtag ? { hashtag: ps.hashtag } : {}),
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
visibleUserIds: ps.visibleUserIds,
channelId: ps.channelId,
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
isActuallyScheduled: ps.isActuallyScheduled,
visibleUserIds: ps.visibleUserIds ?? [],
channelId: ps.channelId ?? undefined,
}).catch((err) => {
if (err instanceof IdentifiableError) {
switch (err.id) {
@ -250,8 +241,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
case '215dbc76-336c-4d2a-9605-95766ba7dab0':
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
case 'c3275f19-4558-4c59-83e1-4f684b5fab66':
throw new ApiError(meta.errors.tooManyScheduledNotes);
default:
throw err;
}

View File

@ -41,7 +41,6 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
scheduled: { type: 'boolean', nullable: true },
},
required: [],
} as const;
@ -59,12 +58,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const query = this.queryService.makePaginationQuery<MiNoteDraft>(this.noteDraftsRepository.createQueryBuilder('drafts'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('drafts.userId = :meId', { meId: me.id });
if (ps.scheduled === true) {
query.andWhere('drafts.isActuallyScheduled = true');
} else if (ps.scheduled === false) {
query.andWhere('drafts.isActuallyScheduled = false');
}
const drafts = await query
.limit(ps.limit)
.getMany();

View File

@ -159,12 +159,6 @@ export const meta = {
code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
id: '215dbc76-336c-4d2a-9605-95766ba7dab0',
},
tooManyScheduledNotes: {
message: 'You cannot create scheduled notes any more.',
code: 'TOO_MANY_SCHEDULED_NOTES',
id: '02f5df79-08ae-4a33-8524-f1503c8f6212',
},
},
limit: {
@ -177,14 +171,14 @@ export const paramDef = {
type: 'object',
properties: {
draftId: { type: 'string', nullable: false, format: 'misskey:id' },
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'] },
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 },
hashtag: { type: 'string', nullable: true, maxLength: 200 },
localOnly: { type: 'boolean' },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'] },
localOnly: { type: 'boolean', default: false },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
replyId: { type: 'string', format: 'misskey:id', nullable: true },
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
channelId: { type: 'string', format: 'misskey:id', nullable: true },
@ -200,7 +194,7 @@ export const paramDef = {
fileIds: {
type: 'array',
uniqueItems: true,
minItems: 0,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
@ -221,8 +215,6 @@ export const paramDef = {
},
required: ['choices'],
},
scheduledAt: { type: 'integer', nullable: true },
isActuallyScheduled: { type: 'boolean' },
},
required: ['draftId'],
} as const;
@ -236,22 +228,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
const draft = await this.noteDraftService.update(me, ps.draftId, {
fileIds: ps.fileIds,
pollChoices: ps.poll?.choices,
pollMultiple: ps.poll?.multiple,
pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null,
pollExpiredAfter: ps.poll?.expiredAfter,
text: ps.text,
replyId: ps.replyId,
renoteId: ps.renoteId,
cw: ps.cw,
hashtag: ps.hashtag,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
expiredAfter: ps.poll.expiredAfter ?? null,
} : undefined,
text: ps.text ?? null,
replyId: ps.replyId ?? undefined,
renoteId: ps.renoteId ?? undefined,
cw: ps.cw ?? null,
...(ps.hashtag ? { hashtag: ps.hashtag } : {}),
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
visibleUserIds: ps.visibleUserIds,
channelId: ps.channelId,
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
isActuallyScheduled: ps.isActuallyScheduled,
visibleUserIds: ps.visibleUserIds ?? [],
channelId: ps.channelId ?? undefined,
}).catch((err) => {
if (err instanceof IdentifiableError) {
switch (err.id) {
@ -293,8 +285,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.containsProhibitedWords);
case '4de0363a-3046-481b-9b0f-feff3e211025':
throw new ApiError(meta.errors.containsTooManyMentions);
case 'bacdf856-5c51-4159-b88a-804fa5103be5':
throw new ApiError(meta.errors.tooManyScheduledNotes);
default:
throw err;
}

View File

@ -29,16 +29,10 @@ export const meta = {
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
},
contentRestrictedByUser: {
message: 'Content restricted by user. Please sign in to view.',
code: 'CONTENT_RESTRICTED_BY_USER',
id: 'fbcc002d-37d9-4944-a6b0-d9e29f2d33ab',
},
contentRestrictedByServer: {
message: 'Content restricted by server settings. Please sign in to view.',
code: 'CONTENT_RESTRICTED_BY_SERVER',
id: '145f88d2-b03d-4087-8143-a78928883c4b',
signinRequired: {
message: 'Signin required.',
code: 'SIGNIN_REQUIRED',
id: '8e75455b-738c-471d-9f80-62693f33372e',
},
},
} as const;
@ -67,15 +61,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
if (note.user!.requireSigninToViewContents && me == null) {
throw new ApiError(meta.errors.contentRestrictedByUser);
throw new ApiError(meta.errors.signinRequired);
}
if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) {
throw new ApiError(meta.errors.contentRestrictedByServer);
throw new ApiError(meta.errors.signinRequired);
}
if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) {
throw new ApiError(meta.errors.contentRestrictedByServer);
throw new ApiError(meta.errors.signinRequired);
}
return await this.noteEntityService.pack(note, me, {

View File

@ -22,14 +22,7 @@ export const meta = {
res: {
type: 'object',
optional: false, nullable: false,
allOf: [
{
type: 'object',
ref: 'UserList',
},
{
type: 'object',
optional: false, nullable: false,
properties: {
likedCount: {
type: 'number',
@ -41,8 +34,6 @@ export const meta = {
},
},
},
],
},
errors: {
noSuchList: {

View File

@ -13,7 +13,7 @@
};
window.onunhandledrejection = (e) => {
console.error(e);
renderError('SOMETHING_HAPPENED_IN_PROMISE', e.reason || e);
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
};
let forceError = localStorage.getItem('forceError');

View File

@ -12,8 +12,6 @@
* quote - 稿Renoteされた
* reaction - 稿
* pollEnded -
* scheduledNotePosted - 稿
* scheduledNotePostFailed - 稿
* receiveFollowRequest -
* followRequestAccepted -
* roleAssigned -
@ -34,8 +32,6 @@ export const notificationTypes = [
'quote',
'reaction',
'pollEnded',
'scheduledNotePosted',
'scheduledNotePostFailed',
'receiveFollowRequest',
'followRequestAccepted',
'roleAssigned',

View File

@ -68,6 +68,7 @@ async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse
return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
ADMIN_CACHE.set(host, {
id: res.id,
// @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this
i: res.token,
});
return res as Misskey.entities.SignupResponse;

View File

@ -20,6 +20,6 @@
"dependencies": {
"estree-walker": "3.0.3",
"magic-string": "0.30.17",
"vite": "7.0.7"
"vite": "7.0.6"
}
}

View File

@ -46,71 +46,9 @@ export default [
allowSingleExtends: true,
}],
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
// close ... window.closeと衝突 or 紛らわしい
// open ... window.openと衝突 or 紛らわしい
// fetch ... window.fetchと衝突 or 紛らわしい
// location ... window.locationと衝突 or 紛らわしい
// document ... window.documentと衝突 or 紛らわしい
// history ... window.historyと衝突 or 紛らわしい
// scroll ... window.scrollと衝突 or 紛らわしい
// setTimeout ... window.setTimeoutと衝突 or 紛らわしい
// setInterval ... window.setIntervalと衝突 or 紛らわしい
// clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい
// clearInterval ... window.clearIntervalと衝突 or 紛らわしい
'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
'no-restricted-globals': [
'error',
{
'name': 'open',
'message': 'Use `window.open`.',
},
{
'name': 'close',
'message': 'Use `window.close`.',
},
{
'name': 'fetch',
'message': 'Use `window.fetch`.',
},
{
'name': 'location',
'message': 'Use `window.location`.',
},
{
'name': 'document',
'message': 'Use `window.document`.',
},
{
'name': 'history',
'message': 'Use `window.history`.',
},
{
'name': 'scroll',
'message': 'Use `window.scroll`.',
},
{
'name': 'setTimeout',
'message': 'Use `window.setTimeout`.',
},
{
'name': 'setInterval',
'message': 'Use `window.setInterval`.',
},
{
'name': 'clearTimeout',
'message': 'Use `window.clearTimeout`.',
},
{
'name': 'clearInterval',
'message': 'Use `window.clearInterval`.',
},
{
'name': 'name',
'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',
},
],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
'id-denylist': ['error', 'window', 'e'],
'no-shadow': ['warn'],
'vue/attributes-order': ['error', {
alphabetical: false,

View File

@ -13,10 +13,10 @@
"@discordapp/twemoji": "16.0.1",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.3.0",
"@rollup/pluginutils": "5.2.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.21",
"@vue/compiler-sfc": "3.5.19",
"astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
@ -26,16 +26,16 @@
"mfm-js": "0.25.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.50.1",
"sass": "1.92.1",
"shiki": "3.12.2",
"rollup": "4.48.0",
"sass": "1.90.0",
"shiki": "3.11.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.2",
"uuid": "11.1.0",
"vite": "7.1.5",
"vue": "3.5.21"
"vite": "7.1.3",
"vue": "3.5.19"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.3",
@ -43,14 +43,14 @@
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.9",
"@types/node": "22.18.1",
"@types/node": "22.17.2",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.42.0",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/runtime-core": "3.5.21",
"@vue/runtime-core": "3.5.19",
"acorn": "8.15.0",
"cross-env": "10.0.0",
"eslint-plugin-import": "2.32.0",
@ -59,11 +59,11 @@
"happy-dom": "18.0.1",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.11.1",
"msw": "2.10.5",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"start-server-and-test": "2.1.0",
"tsx": "4.20.5",
"start-server-and-test": "2.0.13",
"tsx": "4.20.4",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.0.6",
"vue-eslint-parser": "10.2.0",

View File

@ -33,7 +33,7 @@ import type { Theme } from '@/theme.js';
console.log('Misskey Embed');
//#region Embedパラメータの取得・パース
const params = new URLSearchParams(window.location.search);
const params = new URLSearchParams(location.search);
const embedParams = parseEmbedParams(params);
if (_DEV_) console.log(embedParams);
//#endregion
@ -81,7 +81,7 @@ storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload });
//#endregion
// サイズの制限
window.document.documentElement.style.maxWidth = '500px';
document.documentElement.style.maxWidth = '500px';
// iframeIdの設定
function setIframeIdHandler(event: MessageEvent) {
@ -114,16 +114,16 @@ app.provide(DI.embedParams, embedParams);
const rootEl = ((): HTMLElement => {
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
const currentRoot = window.document.getElementById(MISSKEY_MOUNT_DIV_ID);
const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
if (currentRoot) {
console.warn('multiple import detected');
return currentRoot;
}
const root = window.document.createElement('div');
const root = document.createElement('div');
root.id = MISSKEY_MOUNT_DIV_ID;
window.document.body.appendChild(root);
document.body.appendChild(root);
return root;
})();
@ -159,7 +159,7 @@ console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hu
//#endregion
function removeSplash() {
const splash = window.document.getElementById('splash');
const splash = document.getElementById('splash');
if (splash) {
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';

View File

@ -19,7 +19,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
// Web Worker
if (import.meta.env.MODE === 'test') {
const canvas = window.document.createElement('canvas');
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
resolve(canvas);
@ -34,7 +34,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
);
resolve(workers);
} else {
const canvas = window.document.createElement('canvas');
const canvas = document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
resolve(canvas);

View File

@ -29,7 +29,7 @@ const props = defineProps<{
// if no instance data is given, this is for the local instance
const instance = props.instance ?? {
name: serverMetadata.name,
themeColor: (window.document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content,
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content,
};
const faviconUrl = computed(() => props.instance ? mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : mediaProxy.getProxiedImageUrlNullable(serverMetadata.iconUrl, 'preview') ?? '/favicon.ico');

View File

@ -27,7 +27,7 @@ const canonical = props.host === localHost ? `@${props.username}` : `@${props.us
const url = `/${canonical}`;
const bg = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-mention'));
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-mention'));
bg.setAlpha(0.1);
const bgCss = bg.toRgbString();
</script>

View File

@ -134,7 +134,7 @@ const isBackTop = ref(false);
const empty = computed(() => items.value.size === 0);
const error = ref(false);
const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : window.document.body);
const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : document.body);
const visibility = useDocumentVisibility();
@ -353,7 +353,7 @@ watch(visibility, () => {
BACKGROUND_PAUSE_WAIT_SEC * 1000);
} else { // 'visible'
if (timerForSetPause) {
window.clearTimeout(timerForSetPause);
clearTimeout(timerForSetPause);
timerForSetPause = null;
} else {
isPausingUpdate = false;
@ -447,11 +447,11 @@ onBeforeMount(() => {
init().then(() => {
if (props.pagination.reversed) {
nextTick(() => {
window.setTimeout(toBottom, 800);
setTimeout(toBottom, 800);
// scrollToBottommoreFetching
// more = true
window.setTimeout(() => {
setTimeout(() => {
moreFetching.value = false;
}, 2000);
});
@ -461,11 +461,11 @@ onBeforeMount(() => {
onBeforeUnmount(() => {
if (timerForSetPause) {
window.clearTimeout(timerForSetPause);
clearTimeout(timerForSetPause);
timerForSetPause = null;
}
if (preventAppearFetchMoreTimer.value) {
window.clearTimeout(preventAppearFetchMoreTimer.value);
clearTimeout(preventAppearFetchMoreTimer.value);
preventAppearFetchMoreTimer.value = null;
}
scrollObserver.value?.disconnect();

View File

@ -4,7 +4,7 @@
*/
import * as Misskey from 'misskey-js';
const providedContextEl = window.document.getElementById('misskey_embedCtx');
const providedContextEl = document.getElementById('misskey_embedCtx');
export type ServerContext = {
clip?: Misskey.entities.Clip;

View File

@ -6,7 +6,7 @@
import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/misskey-api.js';
const providedMetaEl = window.document.getElementById('misskey_meta');
const providedMetaEl = document.getElementById('misskey_meta');
const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null;

View File

@ -35,15 +35,15 @@ export function assertIsTheme(theme: Record<string, unknown>): theme is Theme {
export function applyTheme(theme: Theme, persist = true) {
if (timeout) window.clearTimeout(timeout);
window.document.documentElement.classList.add('_themeChanging_');
document.documentElement.classList.add('_themeChanging_');
timeout = window.setTimeout(() => {
window.document.documentElement.classList.remove('_themeChanging_');
document.documentElement.classList.remove('_themeChanging_');
}, 1000);
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
window.document.documentElement.dataset.colorScheme = colorScheme;
document.documentElement.dataset.colorScheme = colorScheme;
// Deep copy
const _theme = JSON.parse(JSON.stringify(theme));
@ -55,7 +55,7 @@ export function applyTheme(theme: Theme, persist = true) {
const props = compile(_theme);
for (const tag of window.document.head.children) {
for (const tag of document.head.children) {
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', props['htmlThemeColor']);
break;
@ -63,7 +63,7 @@ export function applyTheme(theme: Theme, persist = true) {
}
for (const [k, v] of Object.entries(props)) {
window.document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
}
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照

View File

@ -52,8 +52,8 @@ function safeURIDecode(str: string): string {
}
}
const page = window.location.pathname.split('/')[2];
const contentId = safeURIDecode(window.location.pathname.split('/')[3]);
const page = location.pathname.split('/')[2];
const contentId = safeURIDecode(location.pathname.split('/')[3]);
if (_DEV_) console.log(page, contentId);
const embedParams = inject(DI.embedParams, defaultEmbedParams);

View File

@ -64,8 +64,6 @@ function toBase62(n: number): string {
}
export function getConfig(): UserConfig {
const localesHash = toBase62(hash(JSON.stringify(locales)));
return {
base: '/embed_vite/',
@ -150,9 +148,9 @@ export function getConfig(): UserConfig {
// dependencies of i18n.ts
'config': ['@@/js/config.js'],
},
entryFileNames: `scripts/${localesHash}-[hash:8].js`,
chunkFileNames: `scripts/${localesHash}-[hash:8].js`,
assetFileNames: `assets/${localesHash}-[hash:8][extname]`,
entryFileNames: 'scripts/[hash:8].js',
chunkFileNames: 'scripts/[hash:8].js',
assetFileNames: 'assets/[hash:8][extname]',
paths(id) {
for (const p of externalPackages) {
if (p.match.test(id)) {

View File

@ -51,71 +51,9 @@ export default [
allowSingleExtends: true,
}],
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
// close ... window.closeと衝突 or 紛らわしい
// open ... window.openと衝突 or 紛らわしい
// fetch ... window.fetchと衝突 or 紛らわしい
// location ... window.locationと衝突 or 紛らわしい
// document ... window.documentと衝突 or 紛らわしい
// history ... window.historyと衝突 or 紛らわしい
// scroll ... window.scrollと衝突 or 紛らわしい
// setTimeout ... window.setTimeoutと衝突 or 紛らわしい
// setInterval ... window.setIntervalと衝突 or 紛らわしい
// clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい
// clearInterval ... window.clearIntervalと衝突 or 紛らわしい
'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
'no-restricted-globals': [
'error',
{
'name': 'open',
'message': 'Use `window.open`.',
},
{
'name': 'close',
'message': 'Use `window.close`.',
},
{
'name': 'fetch',
'message': 'Use `window.fetch`.',
},
{
'name': 'location',
'message': 'Use `window.location`.',
},
{
'name': 'document',
'message': 'Use `window.document`.',
},
{
'name': 'history',
'message': 'Use `window.history`.',
},
{
'name': 'scroll',
'message': 'Use `window.scroll`.',
},
{
'name': 'setTimeout',
'message': 'Use `window.setTimeout`.',
},
{
'name': 'setInterval',
'message': 'Use `window.setInterval`.',
},
{
'name': 'clearTimeout',
'message': 'Use `window.clearTimeout`.',
},
{
'name': 'clearInterval',
'message': 'Use `window.clearInterval`.',
},
{
'name': 'name',
'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',
},
],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
'id-denylist': ['error', 'window', 'e'],
'no-shadow': ['warn'],
'vue/attributes-order': ['error', {
alphabetical: false,

View File

@ -4,15 +4,15 @@
*/
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const address = new URL(window.document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || window.location.href);
const siteName = window.document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
export const host = address.host;
export const hostname = address.hostname;
export const url = address.origin;
export const port = address.port;
export const apiUrl = window.location.origin + '/api';
export const wsOrigin = window.location.origin;
export const apiUrl = location.origin + '/api';
export const wsOrigin = location.origin;
export const lang = localStorage.getItem('lang') ?? 'en-US';
export const langs = _LANGS_;
export const version = _VERSION_;

View File

@ -51,7 +51,7 @@ export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknow
// - toleranceの範囲内に収まる程度の微量なスクロールが発生した
let prevTopVisible = firstTopVisible;
const onScroll = () => {
if (!window.document.body.contains(el)) return;
if (!document.body.contains(el)) return;
const topVisible = isHeadVisible(el, tolerance);
if (topVisible !== prevTopVisible) {
@ -78,7 +78,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
const containerOrWindow = container ?? window;
const onScroll = () => {
if (!window.document.body.contains(el)) return;
if (!document.body.contains(el)) return;
if (isTailVisible(el, 1, container)) {
cb();
if (once) removeListener();
@ -145,8 +145,8 @@ export function isTailVisible(el: HTMLElement, tolerance = 1, container = getScr
// https://ja.javascript.info/size-and-scroll-window#ref-932
export function getBodyScrollHeight() {
return Math.max(
window.document.body.scrollHeight, window.document.documentElement.scrollHeight,
window.document.body.offsetHeight, window.document.documentElement.offsetHeight,
window.document.body.clientHeight, window.document.documentElement.clientHeight,
document.body.scrollHeight, document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight,
);
}

View File

@ -7,18 +7,18 @@ import { onMounted, onUnmounted, ref } from 'vue';
import type { Ref } from 'vue';
export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
const visibility = ref(window.document.visibilityState);
const visibility = ref(document.visibilityState);
const onChange = (): void => {
visibility.value = window.document.visibilityState;
visibility.value = document.visibilityState;
};
onMounted(() => {
window.document.addEventListener('visibilitychange', onChange);
document.addEventListener('visibilitychange', onChange);
});
onUnmounted(() => {
window.document.removeEventListener('visibilitychange', onChange);
document.removeEventListener('visibilitychange', onChange);
});
return visibility;

View File

@ -21,9 +21,9 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.18.1",
"@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.42.0",
"@types/node": "22.17.2",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"esbuild": "0.25.9",
"eslint-plugin-vue": "10.4.0",
"nodemon": "3.1.10",
@ -35,6 +35,6 @@
],
"dependencies": {
"misskey-js": "workspace:*",
"vue": "3.5.21"
"vue": "3.5.19"
}
}

View File

@ -23,13 +23,13 @@
"@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.3.0",
"@sentry/vue": "10.10.0",
"@rollup/pluginutils": "5.2.0",
"@sentry/vue": "10.5.0",
"@syuilo/aiscript": "1.1.0",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.21",
"@vue/compiler-sfc": "3.5.19",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.19",
"astring": "1.9.0",
@ -41,7 +41,7 @@
"chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"chromatic": "13.1.4",
"chromatic": "13.1.3",
"compare-versions": "6.1.1",
"cropperjs": "2.0.1",
"date-fns": "4.1.0",
@ -57,30 +57,27 @@
"json5": "2.2.3",
"magic-string": "0.30.18",
"matter-js": "0.20.0",
"mediabunny": "1.15.1",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"punycode.js": "2.3.1",
"qr-code-styling": "1.9.2",
"qr-scanner": "1.4.2",
"rollup": "4.50.1",
"rollup": "4.48.0",
"sanitize-html": "2.17.0",
"sass": "1.92.1",
"shiki": "3.12.2",
"sass": "1.90.0",
"shiki": "3.11.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.180.0",
"three": "0.179.1",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.2",
"v-code-diff": "1.13.1",
"vite": "7.1.5",
"vue": "3.5.21",
"vite": "7.1.3",
"vue": "3.5.19",
"vuedraggable": "next",
"wanakana": "5.3.1"
},
@ -88,7 +85,7 @@
"@misskey-dev/summaly": "5.2.3",
"@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14",
"@storybook/addon-links": "9.1.5",
"@storybook/addon-links": "9.1.3",
"@storybook/addon-mdx-gfm": "8.6.14",
"@storybook/addon-storysource": "8.6.14",
"@storybook/blocks": "8.6.14",
@ -96,31 +93,31 @@
"@storybook/core-events": "8.6.14",
"@storybook/manager-api": "8.6.14",
"@storybook/preview-api": "8.6.14",
"@storybook/react": "9.1.5",
"@storybook/react-vite": "9.1.5",
"@storybook/react": "9.1.3",
"@storybook/react-vite": "9.1.3",
"@storybook/test": "8.6.14",
"@storybook/theming": "8.6.14",
"@storybook/types": "8.6.14",
"@storybook/vue3": "9.1.5",
"@storybook/vue3-vite": "9.1.5",
"@storybook/vue3": "9.1.3",
"@storybook/vue3-vite": "9.1.3",
"@tabler/icons-webfont": "3.34.1",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.8",
"@types/matter-js": "0.20.0",
"@types/micromatch": "4.0.9",
"@types/node": "22.18.1",
"@types/node": "22.17.2",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.42.0",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/compiler-core": "3.5.21",
"@vue/runtime-core": "3.5.21",
"@vue/compiler-core": "3.5.19",
"@vue/runtime-core": "3.5.19",
"acorn": "8.15.0",
"cross-env": "10.0.0",
"cypress": "14.5.4",
@ -131,17 +128,17 @@
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.0.3",
"msw": "2.11.1",
"msw": "2.10.5",
"msw-storybook-addon": "2.0.5",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"react": "19.1.1",
"react-dom": "19.1.1",
"seedrandom": "3.0.5",
"start-server-and-test": "2.1.0",
"storybook": "9.1.5",
"start-server-and-test": "2.0.13",
"storybook": "9.1.3",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.20.5",
"tsx": "4.20.4",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.4",
"vitest-fetch-mock": "0.4.5",

View File

@ -151,21 +151,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
}
//#endregion
//#region Sync dark mode
if (prefer.s.syncDeviceDarkMode) {
store.set('darkMode', isDeviceDarkmode());
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
if (prefer.s.syncDeviceDarkMode) {
store.set('darkMode', mql.matches);
}
});
//#endregion
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
// NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため)
// see: https://github.com/misskey-dev/misskey/issues/16562
watch(store.r.darkMode, (darkMode) => {
const theme = (() => {
if (darkMode) {
@ -197,6 +183,18 @@ export async function common(createVue: () => Promise<App<Element>>) {
});
}
//#region Sync dark mode
if (prefer.s.syncDeviceDarkMode) {
store.set('darkMode', isDeviceDarkmode());
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
if (prefer.s.syncDeviceDarkMode) {
store.set('darkMode', mql.matches);
}
});
//#endregion
if (!isSafeMode) {
if (prefer.s.darkTheme && store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);

View File

@ -10,7 +10,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
import isChromatic from 'chromatic/isChromatic';
import { initShaderProgram } from '@/utility/webgl.js';
const canvasEl = useTemplateRef('canvasEl');
@ -22,6 +21,47 @@ const props = withDefaults(defineProps<{
focus: 1.0,
});
function loadShader(gl: WebGLRenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (shader == null) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(
`falied to compile shader: ${gl.getShaderInfoLog(shader)}`,
);
gl.deleteShader(shader);
return null;
}
return shader;
}
function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
if (vertexShader == null || fragmentShader == null) return null;
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert(
`failed to init shader: ${gl.getProgramInfoLog(
shaderProgram,
)}`,
);
return null;
}
return shaderProgram;
}
let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
onMounted(() => {
@ -31,7 +71,7 @@ onMounted(() => {
canvas.width = width;
canvas.height = height;
const maybeGl = canvas.getContext('webgl2', { premultipliedAlpha: true });
const maybeGl = canvas.getContext('webgl', { premultipliedAlpha: true });
if (maybeGl == null) return;
const gl = maybeGl;
@ -42,16 +82,18 @@ onMounted(() => {
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const shaderProgram = initShaderProgram(gl, `#version 300 es
in vec2 position;
const shaderProgram = initShaderProgram(gl, `
attribute vec2 vertex;
uniform vec2 u_scale;
out vec2 in_uv;
varying vec2 v_pos;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
in_uv = position / u_scale;
gl_Position = vec4(vertex, 0.0, 1.0);
v_pos = vertex / u_scale;
}
`, `#version 300 es
`, `
precision mediump float;
vec3 mod289(vec3 x) {
@ -101,7 +143,6 @@ onMounted(() => {
return 130.0 * dot(m, g);
}
in vec2 in_uv;
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_spread;
@ -109,7 +150,8 @@ onMounted(() => {
uniform float u_warp;
uniform float u_focus;
uniform float u_itensity;
out vec4 out_color;
varying vec2 v_pos;
float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
float SPREAD = 0.7 * u_spread;
@ -140,13 +182,13 @@ onMounted(() => {
float ratio = u_resolution.x / u_resolution.y;
vec2 uv = vec2( in_uv.x, in_uv.y / ratio ) * 0.5 + 0.5;
vec2 uv = vec2( v_pos.x, v_pos.y / ratio ) * 0.5 + 0.5;
vec3 color = vec3( 0.0 );
float greenMix = snoise( in_uv * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
float purpleMix = snoise( in_uv * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
float orangeMix = snoise( in_uv * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
float greenMix = snoise( v_pos * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
float purpleMix = snoise( v_pos * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
float orangeMix = snoise( v_pos * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 );
float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 );
@ -156,10 +198,10 @@ onMounted(() => {
color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix );
color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix );
color *= u_itensity + 1.0 * pow( snoise( vec2( in_uv.y + u_time * 0.00013, in_uv.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
color *= u_itensity + 1.0 * pow( snoise( vec2( v_pos.y + u_time * 0.00013, v_pos.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
vec3 inverted = vec3( 1.0 ) - color;
out_color = vec4(color, max(max(color.x, color.y), color.z));
gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) );
}
`);
if (shaderProgram == null) return;
@ -181,7 +223,7 @@ onMounted(() => {
gl.uniform1f(u_itensity, 0.5);
gl.uniform2fv(u_scale, [props.scale, props.scale]);
const vertex = gl.getAttribLocation(shaderProgram, 'position');
const vertex = gl.getAttribLocation(shaderProgram, 'vertex');
gl.enableVertexAttribArray(vertex);
gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0);

View File

@ -10,11 +10,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="name">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
<MkSelect v-model="src" :items="antennaSourcesSelectDef">
<MkSelect v-model="src">
<template #label>{{ i18n.ts.antennaSource }}</template>
<option value="all">{{ i18n.ts._antennaSources.all }}</option>
<!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>-->
<option value="users">{{ i18n.ts._antennaSources.users }}</option>
<!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
<option value="users_blacklist">{{ i18n.ts._antennaSources.userBlacklist }}</option>
</MkSelect>
<MkSelect v-if="src === 'list'" v-model="userListId" :items="userListsSelectDef">
<MkSelect v-if="src === 'list'" v-model="userListId">
<template #label>{{ i18n.ts.userList }}</template>
<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
</MkSelect>
<MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users">
<template #label>{{ i18n.ts.users }}</template>
@ -46,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { watch, ref, computed } from 'vue';
import { watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import type { DeepPartial } from '@/utility/merge.js';
import MkButton from '@/components/MkButton.vue';
@ -58,7 +64,6 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { deepMerge } from '@/utility/merge.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
id?: string;
@ -94,35 +99,9 @@ const emit = defineEmits<{
(ev: 'deleted'): void,
}>();
const {
model: src,
def: antennaSourcesSelectDef,
} = useMkSelect({
items: [
{ value: 'all', label: i18n.ts._antennaSources.all },
//{ value: 'home', label: i18n.ts._antennaSources.homeTimeline },
{ value: 'users', label: i18n.ts._antennaSources.users },
//{ value: 'list', label: i18n.ts._antennaSources.userList },
{ value: 'users_blacklist', label: i18n.ts._antennaSources.userBlacklist },
],
initialValue: initialAntenna.src,
});
const {
model: userListId,
def: userListsSelectDef,
} = useMkSelect({
items: computed(() => {
if (userLists.value == null) return [];
return userLists.value.map(list => ({
value: list.id,
label: list.name,
}));
}),
initialValue: initialAntenna.userListId,
});
const name = ref<string>(initialAntenna.name);
const src = ref<Misskey.entities.AntennasCreateRequest['src']>(initialAntenna.src);
const userListId = ref<string | null>(initialAntenna.userListId);
const users = ref<string>(initialAntenna.users.join('\n'));
const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n'));
const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n'));

View File

@ -32,9 +32,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkInput>
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" :items="selectDef" @update:modelValue="onSelectUpdate">
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" @update:modelValue="onSelectUpdate">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
</MkSelect>
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton>
<div v-else-if="c.type === 'postForm'" :class="$style.postForm">
@ -73,7 +74,6 @@ import MkSelect from '@/components/MkSelect.vue';
import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
import MkFolder from '@/components/MkFolder.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import { useMkSelect } from '@/composables/use-mkselect.js';
const props = withDefaults(defineProps<{
component: AsUiComponent;
@ -130,19 +130,7 @@ function onSwitchUpdate(v: boolean) {
}
}
const {
model: valueForSelect,
def: selectDef,
} = useMkSelect({
items: computed(() => {
if (c.type !== 'select') return [];
return (c.items ?? []).map(item => ({
value: item.value,
label: item.text,
}));
}),
initialValue: (c.type === 'select' && 'default' in c && typeof c.default !== 'boolean') ? c.default ?? null : null,
});
const valueForSelect = ref('default' in c && typeof c.default !== 'boolean' ? c.default ?? null : null);
function onSelectUpdate(v) {
valueForSelect.value = v;

View File

@ -29,6 +29,6 @@ const users = ref<Misskey.entities.UserLite[]>([]);
onMounted(async () => {
users.value = await misskeyApi('users/show', {
userIds: props.userIds,
});
}) as unknown as Misskey.entities.UserLite[];
});
</script>

View File

@ -29,7 +29,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
</template>
</MkInput>
<MkSelect v-if="select" v-model="selectedValue" :items="selectDef" autofocus></MkSelect>
<MkSelect v-if="select" v-model="selectedValue" autofocus>
<template v-if="select.items">
<template v-for="item in select.items">
<optgroup v-if="'sectionTitle' in item" :label="item.sectionTitle">
<option v-for="subItem in item.items" :value="subItem.value">{{ subItem.text }}</option>
</optgroup>
<option v-else :value="item.value">{{ item.text }}</option>
</template>
</template>
</MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
@ -47,8 +56,6 @@ import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
import { useMkSelect } from '@/composables/use-mkselect.js';
import { i18n } from '@/i18n.js';
type Input = {
@ -60,9 +67,17 @@ type Input = {
maxLength?: number;
};
type SelectItem = {
value: any;
text: string;
};
type Select = {
items: MkSelectItem[];
default: OptionValue | null;
items: (SelectItem | {
sectionTitle: string;
items: SelectItem[];
})[];
default: string | null;
};
type Result = string | number | true | null;
@ -100,6 +115,7 @@ const emit = defineEmits<{
const modal = useTemplateRef('modal');
const inputValue = ref<string | number | null>(props.input?.default ?? null);
const selectedValue = ref(props.select?.default ?? null);
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
if (props.input) {
@ -118,14 +134,6 @@ const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'character
return null;
});
const {
def: selectDef,
model: selectedValue,
} = useMkSelect({
items: computed(() => props.select?.items ?? []),
initialValue: props.select?.default ?? null,
});
// overload function 使 lint
function done(canceled: true): void;
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare

View File

@ -52,8 +52,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #suffix>px</template>
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
</MkInput>
<MkSelect v-model="colorMode" :items="colorModeDef">
<MkSelect v-model="colorMode">
<template #label>{{ i18n.ts.theme }}</template>
<option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
<option value="light">{{ i18n.ts.light }}</option>
<option value="dark">{{ i18n.ts.dark }}</option>
</MkSelect>
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
@ -102,7 +105,6 @@ import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js';
@ -160,18 +162,7 @@ const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(pro
const header = ref(props.params?.header ?? true);
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? null : 500);
const {
model: colorMode,
def: colorModeDef,
} = useMkSelect({
items: [
{ value: 'auto', label: i18n.ts.syncDeviceDarkMode },
{ value: 'light', label: i18n.ts.light },
{ value: 'dark', label: i18n.ts.dark },
],
initialValue: props.params?.colorMode ?? 'auto',
});
const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
const rounded = ref(props.params?.rounded ?? true);
const border = ref(props.params?.border ?? true);

View File

@ -530,14 +530,6 @@ defineExpose({
--eachSize: 50px;
}
&.s4 {
--eachSize: 55px;
}
&.s5 {
--eachSize: 60px;
}
&.w1 {
width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
--columns: 1fr 1fr 1fr 1fr 1fr;

View File

@ -39,8 +39,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-text="v.label || k"></span>
<template v-if="v.description" #caption>{{ v.description }}</template>
</MkSwitch>
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)">
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<option v-for="option in v.enum" :key="getEnumKey(option)" :value="getEnumValue(option)">{{ getEnumLabel(option) }}</option>
</MkSelect>
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
@ -76,8 +77,7 @@ import MkRange from './MkRange.vue';
import MkButton from './MkButton.vue';
import MkRadios from './MkRadios.vue';
import XFile from './MkFormDialog.file.vue';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js';
import type { EnumItem, Form, RadioFormItem } from '@/utility/form.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
@ -120,14 +120,16 @@ function cancel() {
dialog.value?.close();
}
function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
return def.enum.map((v) => {
if (typeof v === 'string') {
return { value: v, label: v };
} else {
return { value: v.value, label: v.label };
}
});
function getEnumLabel(e: EnumItem) {
return typeof e === 'string' ? e : e.label;
}
function getEnumValue(e: EnumItem) {
return typeof e === 'string' ? e : e.value;
}
function getEnumKey(e: EnumItem) {
return typeof e === 'string' ? e : typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
}
function getRadioKey(e: RadioFormItem['options'][number]) {

View File

@ -19,12 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<div :class="$style.container">
<div :class="$style.preview">
<canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown="onImagePointerdown"></canvas>
<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
<div :class="$style.previewContainer">
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
<div class="_acrylic" :class="$style.editControls">
<button class="_button" :class="[$style.previewControlsButton, penMode != null ? $style.active : null]" @click="showPenMenu"><i class="ti ti-pencil"></i></button>
</div>
<div class="_acrylic" :class="$style.previewControls">
<button class="_button" :class="[$style.previewControlsButton, !enabled ? $style.active : null]" @click="enabled = false">Before</button>
<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
@ -215,147 +212,6 @@ watch(enabled, () => {
renderer.render();
}
});
const penMode = ref<'fill' | 'blur' | 'pixelate' | null>(null);
function showPenMenu(ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts._imageEffector._fxs.fill,
action: () => {
penMode.value = 'fill';
},
}, {
text: i18n.ts._imageEffector._fxs.blur,
action: () => {
penMode.value = 'blur';
},
}, {
text: i18n.ts._imageEffector._fxs.pixelate,
action: () => {
penMode.value = 'pixelate';
},
}], ev.currentTarget ?? ev.target);
}
function onImagePointerdown(ev: PointerEvent) {
if (canvasEl.value == null || imageBitmap == null || penMode.value == null) return;
const AW = canvasEl.value.clientWidth;
const AH = canvasEl.value.clientHeight;
const BW = imageBitmap.width;
const BH = imageBitmap.height;
let xOffset = 0;
let yOffset = 0;
if (AW / AH < BW / BH) { //
yOffset = AH - BH * (AW / BW);
} else { //
xOffset = AW - BW * (AH / BH);
}
xOffset /= 2;
yOffset /= 2;
let startX = ev.offsetX - xOffset;
let startY = ev.offsetY - yOffset;
if (AW / AH < BW / BH) { //
startX = startX / (Math.max(AW, AH) / Math.max(BH / BW, 1));
startY = startY / (Math.max(AW, AH) / Math.max(BW / BH, 1));
} else { //
startX = startX / (Math.min(AW, AH) / Math.max(BH / BW, 1));
startY = startY / (Math.min(AW, AH) / Math.max(BW / BH, 1));
}
const id = genId();
if (penMode.value === 'fill') {
layers.push({
id,
fxId: 'fill',
params: {
offsetX: 0,
offsetY: 0,
scaleX: 0.1,
scaleY: 0.1,
angle: 0,
opacity: 1,
color: [1, 1, 1],
},
});
} else if (penMode.value === 'blur') {
layers.push({
id,
fxId: 'blur',
params: {
offsetX: 0,
offsetY: 0,
scaleX: 0.1,
scaleY: 0.1,
angle: 0,
radius: 3,
},
});
} else if (penMode.value === 'pixelate') {
layers.push({
id,
fxId: 'pixelate',
params: {
offsetX: 0,
offsetY: 0,
scaleX: 0.1,
scaleY: 0.1,
angle: 0,
strength: 0.2,
},
});
}
_move(ev.offsetX, ev.offsetY);
function _move(pointerX: number, pointerY: number) {
let x = pointerX - xOffset;
let y = pointerY - yOffset;
if (AW / AH < BW / BH) { //
x = x / (Math.max(AW, AH) / Math.max(BH / BW, 1));
y = y / (Math.max(AW, AH) / Math.max(BW / BH, 1));
} else { //
x = x / (Math.min(AW, AH) / Math.max(BH / BW, 1));
y = y / (Math.min(AW, AH) / Math.max(BW / BH, 1));
}
const scaleX = Math.abs(x - startX);
const scaleY = Math.abs(y - startY);
const layerIndex = layers.findIndex((l) => l.id === id);
const layer = layerIndex !== -1 ? layers[layerIndex] : null;
if (layer != null) {
layer.params.offsetX = (x + startX) - 1;
layer.params.offsetY = (y + startY) - 1;
layer.params.scaleX = scaleX;
layer.params.scaleY = scaleY;
layers[layerIndex] = layer;
}
}
function move(ev: PointerEvent) {
_move(ev.offsetX, ev.offsetY);
}
function up() {
canvasEl.value?.removeEventListener('pointermove', move);
canvasEl.value?.removeEventListener('pointerup', up);
canvasEl.value?.removeEventListener('pointercancel', up);
canvasEl.value?.releasePointerCapture(ev.pointerId);
penMode.value = null;
}
canvasEl.value.addEventListener('pointermove', move);
canvasEl.value.addEventListener('pointerup', up);
canvasEl.value.setPointerCapture(ev.pointerId);
}
</script>
<style module>
@ -395,18 +251,6 @@ function onImagePointerdown(ev: PointerEvent) {
font-size: 85%;
}
.editControls {
position: absolute;
z-index: 100;
bottom: 8px;
left: 8px;
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 6px;
}
.previewControls {
position: absolute;
z-index: 100;
@ -439,13 +283,9 @@ function onImagePointerdown(ev: PointerEvent) {
position: absolute;
top: 0;
left: 0;
/* iOS
width: stretch;
height: stretch;
*/
width: calc(100% - 40px);
height: calc(100% - 40px);
margin: 20px;
width: 100%;
height: 100%;
padding: 20px;
box-sizing: border-box;
object-fit: contain;
}

View File

@ -9,8 +9,31 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>Chart</template>
<div :class="$style.chart">
<div class="selects">
<MkSelect v-model="chartSrc" :items="chartSrcDef" style="margin: 0; flex: 1;"></MkSelect>
<MkSelect v-model="chartSpan" :items="chartSpanDef" style="margin: 0 0 0 10px;"></MkSelect>
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
<optgroup v-if="shouldShowFederation" :label="i18n.ts.federation">
<option value="federation">{{ i18n.ts._charts.federation }}</option>
<option value="ap-request">{{ i18n.ts._charts.apRequest }}</option>
</optgroup>
<optgroup :label="i18n.ts.users">
<option value="users">{{ i18n.ts._charts.usersIncDec }}</option>
<option value="users-total">{{ i18n.ts._charts.usersTotal }}</option>
<option value="active-users">{{ i18n.ts._charts.activeUsers }}</option>
</optgroup>
<optgroup :label="i18n.ts.notes">
<option value="notes">{{ i18n.ts._charts.notesIncDec }}</option>
<option value="local-notes">{{ i18n.ts._charts.localNotesIncDec }}</option>
<option v-if="shouldShowFederation" value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option>
<option value="notes-total">{{ i18n.ts._charts.notesTotal }}</option>
</optgroup>
<optgroup :label="i18n.ts.drive">
<option value="drive-files">{{ i18n.ts._charts.filesIncDec }}</option>
<option value="drive">{{ i18n.ts._charts.storageUsageIncDec }}</option>
</optgroup>
</MkSelect>
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
<option value="hour">{{ i18n.ts.perHour }}</option>
<option value="day">{{ i18n.ts.perDay }}</option>
</MkSelect>
</div>
<div class="chart _panel">
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
@ -20,7 +43,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFoldableSection class="item">
<template #header>Active users heatmap</template>
<MkSelect v-model="heatmapSrc" :items="heatmapSrcDef" style="margin: 0 0 12px 0;"></MkSelect>
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;">
<option value="active-users">Active users</option>
<option value="notes">Notes</option>
<option v-if="shouldShowFederation" value="ap-requests-inbox-received">AP Requests: inboxReceived</option>
<option v-if="shouldShowFederation" value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option>
<option v-if="shouldShowFederation" value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
</MkSelect>
<div class="_panel" :class="$style.heatmap">
<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
</div>
@ -55,10 +84,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onMounted, computed, useTemplateRef } from 'vue';
import { onMounted, ref, computed, useTemplateRef } from 'vue';
import { Chart } from 'chart.js';
import type { HeatmapSource } from '@/components/MkHeatmap.vue';
import MkSelect from '@/components/MkSelect.vue';
import type { MkSelectItem, ItemOption } from '@/components/MkSelect.vue';
import MkChart from '@/components/MkChart.vue';
import type { ChartSrc } from '@/components/MkChart.vue';
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
@ -72,96 +101,15 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
import { initChart } from '@/utility/init-chart.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
initChart();
const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator);
const chartLimit = 500;
const {
model: chartSpan,
def: chartSpanDef,
} = useMkSelect({
items: [
{ value: 'hour', label: i18n.ts.perHour },
{ value: 'day', label: i18n.ts.perDay },
],
initialValue: 'hour',
});
const {
model: chartSrc,
def: chartSrcDef,
} = useMkSelect({
items: computed<MkSelectItem<ChartSrc>[]>(() => {
const items: MkSelectItem<ChartSrc>[] = [];
if (shouldShowFederation.value) {
items.push({
type: 'group',
label: i18n.ts.federation,
items: [
{ value: 'federation', label: i18n.ts._charts.federation },
{ value: 'ap-request', label: i18n.ts._charts.apRequest },
],
});
}
items.push({
type: 'group',
label: i18n.ts.users,
items: [
{ value: 'users', label: i18n.ts._charts.usersIncDec },
{ value: 'users-total', label: i18n.ts._charts.usersTotal },
{ value: 'active-users', label: i18n.ts._charts.activeUsers },
],
});
const notesItems: ItemOption<ChartSrc>[] = [
{ value: 'notes', label: i18n.ts._charts.notesIncDec },
{ value: 'local-notes', label: i18n.ts._charts.localNotesIncDec },
];
if (shouldShowFederation.value) notesItems.push({ value: 'remote-notes', label: i18n.ts._charts.remoteNotesIncDec });
notesItems.push(
{ value: 'notes-total', label: i18n.ts._charts.notesTotal },
);
items.push({
type: 'group',
label: i18n.ts.notes,
items: notesItems,
});
items.push({
type: 'group',
label: i18n.ts.drive,
items: [
{ value: 'drive-files', label: i18n.ts._charts.filesIncDec },
{ value: 'drive', label: i18n.ts._charts.storageUsageIncDec },
],
});
return items;
}),
initialValue: 'active-users',
});
const {
model: heatmapSrc,
def: heatmapSrcDef,
} = useMkSelect({
items: computed(() => [
{ value: 'active-users' as const, label: 'Active Users' },
{ value: 'notes' as const, label: 'Notes' },
...(shouldShowFederation.value ? [
{ value: 'ap-requests-inbox-received' as const, label: 'AP Requests: inboxReceived' },
{ value: 'ap-requests-deliver-succeeded' as const, label: 'AP Requests: deliverSucceeded' },
{ value: 'ap-requests-deliver-failed' as const, label: 'AP Requests: deliverFailed' },
] : []),
]),
initialValue: 'active-users',
});
const chartSpan = ref<'hour' | 'day'>('hour');
const chartSrc = ref<ChartSrc>('active-users');
const heatmapSrc = ref<HeatmapSource>('active-users');
const subDoughnutEl = useTemplateRef('subDoughnutEl');
const pubDoughnutEl = useTemplateRef('pubDoughnutEl');

View File

@ -15,32 +15,10 @@ SPDX-License-Identifier: AGPL-3.0-only
@esc="cancel()"
>
<template #header>
{{ i18n.ts.draftsAndScheduledNotes }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
{{ i18n.ts.drafts }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
</template>
<MkStickyContainer>
<template #header>
<MkTabs
v-model:tab="tab"
centered
:class="$style.tabs"
:tabs="[
{
key: 'drafts',
title: i18n.ts.drafts,
icon: 'ti ti-pencil-question',
},
{
key: 'scheduled',
title: i18n.ts.scheduled,
icon: 'ti ti-calendar-clock',
},
]"
/>
</template>
<div class="_spacer">
<MkPagination :key="tab" :paginator="tab === 'scheduled' ? scheduledPaginator : draftsPaginator" withControl>
<MkPagination :paginator="paginator" withControl>
<template #empty>
<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
</template>
@ -54,13 +32,6 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[$style.draft]"
>
<div :class="$style.draftBody" class="_gaps_s">
<MkInfo v-if="draft.scheduledAt != null && draft.isActuallyScheduled">
<I18n :src="i18n.ts.scheduledToPostOnX" tag="span">
<template #x>
<MkTime :time="draft.scheduledAt" :mode="'detail'" style="font-weight: bold;"/>
</template>
</I18n>
</MkInfo>
<div :class="$style.draftInfo">
<div :class="$style.draftMeta">
<div v-if="draft.reply" class="_nowrap">
@ -114,33 +85,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/>
</div>
</div>
<div :class="$style.draftActions" class="_buttons">
<template v-if="draft.scheduledAt != null && draft.isActuallyScheduled">
<MkButton
:class="$style.itemButton"
small
@click="cancelSchedule(draft)"
>
<i class="ti ti-calendar-x"></i> {{ i18n.ts._drafts.cancelSchedule }}
</MkButton>
<!-- TODO
<MkButton
:class="$style.itemButton"
small
@click="reSchedule(draft)"
>
<i class="ti ti-calendar-time"></i> {{ i18n.ts._drafts.reSchedule }}
</MkButton>
-->
</template>
<MkButton
v-else
:class="$style.itemButton"
small
@click="restoreDraft(draft)"
>
<i class="ti ti-corner-up-left"></i> {{ i18n.ts._drafts.restore }}
<i class="ti ti-corner-up-left"></i>
{{ i18n.ts._drafts.restore }}
</MkButton>
<MkButton
v-tooltip="i18n.ts._drafts.delete"
@ -148,7 +100,6 @@ SPDX-License-Identifier: AGPL-3.0-only
small
:iconOnly="true"
:class="$style.itemButton"
style="margin-left: auto;"
@click="deleteDraft(draft)"
>
<i class="ti ti-trash"></i>
@ -159,7 +110,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</MkPagination>
</div>
</MkStickyContainer>
</MkModalWindow>
</template>
@ -175,12 +125,6 @@ import * as os from '@/os.js';
import { $i } from '@/i.js';
import { misskeyApi } from '@/utility/misskey-api';
import { Paginator } from '@/utility/paginator.js';
import MkTabs from '@/components/MkTabs.vue';
import MkInfo from '@/components/MkInfo.vue';
const props = defineProps<{
scheduled?: boolean;
}>();
const emit = defineEmits<{
(ev: 'restore', draft: Misskey.entities.NoteDraft): void;
@ -188,20 +132,8 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
const tab = ref<'drafts' | 'scheduled'>(props.scheduled ? 'scheduled' : 'drafts');
const draftsPaginator = markRaw(new Paginator('notes/drafts/list', {
const paginator = markRaw(new Paginator('notes/drafts/list', {
limit: 10,
params: {
scheduled: false,
},
}));
const scheduledPaginator = markRaw(new Paginator('notes/drafts/list', {
limit: 10,
params: {
scheduled: true,
},
}));
const currentDraftsCount = ref(0);
@ -230,17 +162,7 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
if (canceled) return;
os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => {
draftsPaginator.reload();
});
}
async function cancelSchedule(draft: Misskey.entities.NoteDraft) {
os.apiWithDialog('notes/drafts/update', {
draftId: draft.id,
isActuallyScheduled: false,
scheduledAt: null,
}).then(() => {
scheduledPaginator.reload();
paginator.reload();
});
}
</script>
@ -298,11 +220,4 @@ async function cancelSchedule(draft: Misskey.entities.NoteDraft) {
padding-top: 16px;
border-top: solid 1px var(--MI_THEME-divider);
}
.tabs {
background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
border-bottom: solid 0.5px var(--MI_THEME-divider);
}
</style>

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<div :class="$style.head">
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'createToken', 'scheduledNotePosted', 'scheduledNotePostFailed'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'createToken'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
@ -23,8 +23,6 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_mention]: notification.type === 'mention',
[$style.t_quote]: notification.type === 'quote',
[$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_scheduledNotePosted]: notification.type === 'scheduledNotePosted',
[$style.t_scheduledNotePostFailed]: notification.type === 'scheduledNotePostFailed',
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
[$style.t_login]: notification.type === 'login',
@ -41,8 +39,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'scheduledNotePosted'" class="ti ti-send"></i>
<i v-else-if="notification.type === 'scheduledNotePostFailed'" class="ti ti-alert-triangle"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
@ -64,8 +60,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.tail">
<header :class="$style.header">
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
<span v-else-if="notification.type === 'scheduledNotePosted'">{{ i18n.ts._notification.scheduledNotePosted }}</span>
<span v-else-if="notification.type === 'scheduledNotePostFailed'">{{ i18n.ts._notification.scheduledNotePostFailed }}</span>
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'chatRoomInvitationReceived'">{{ i18n.ts._notification.chatRoomInvitationReceived }}</span>
@ -109,11 +103,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
<i class="ti ti-quote" :class="$style.quote"></i>
</MkA>
<MkA v-else-if="notification.type === 'scheduledNotePosted'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
<i class="ti ti-quote" :class="$style.quote"></i>
</MkA>
<div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
{{ notification.role.name }}
</div>
@ -349,16 +338,6 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
pointer-events: none;
}
.t_scheduledNotePosted {
background: var(--eventOther);
pointer-events: none;
}
.t_scheduledNotePostFailed {
background: var(--eventOther);
pointer-events: none;
}
.t_achievementEarned {
background: var(--eventAchievement);
pointer-events: none;

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root">
<div :class="$style.control">
<MkSelect v-model="order" :class="$style.order" :items="orderDef">
<MkSelect v-model="order" :class="$style.order" :items="[{ label: i18n.ts._order.newest, value: 'newest' }, { label: i18n.ts._order.oldest, value: 'oldest' }]">
<template #prefix><i class="ti ti-arrows-sort"></i></template>
</MkSelect>
<MkButton v-if="paginator.canSearch" v-tooltip="i18n.ts.search" iconOnly transparent rounded :active="searchOpened" @click="searchOpened = !searchOpened"><i class="ti ti-search"></i></MkButton>
@ -45,7 +45,6 @@ import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue';
import { formatDateTimeString } from '@/utility/format-time-string.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
const props = withDefaults(defineProps<{
paginator: T;
@ -59,16 +58,7 @@ const props = withDefaults(defineProps<{
const searchOpened = ref(false);
const filterOpened = ref(props.filterOpened);
const {
model: order,
def: orderDef,
} = useMkSelect({
items: [
{ label: i18n.ts._order.newest, value: 'newest' },
{ label: i18n.ts._order.oldest, value: 'oldest' },
],
initialValue: 'newest',
});
const order = ref<'newest' | 'oldest'>('newest');
const date = ref<number | null>(null);
const q = ref<string | null>(null);

View File

@ -4,18 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="[$style.root, accented ? $style.accented : null, revered ? $style.revered : null]"/>
<div :class="[$style.root, accented ? $style.accented : null]"></div>
</template>
<script lang="ts" setup>
const props = withDefaults(defineProps<{
accented?: boolean;
revered?: boolean;
height?: number;
}>(), {
accented: false,
revered: false,
height: 200,
});
</script>
@ -31,17 +27,14 @@ const props = withDefaults(defineProps<{
--dot-size: 2px;
--gap-size: 40px;
--offset: calc(var(--gap-size) / 2);
--height: v-bind('props.height + "px"');
height: var(--height);
height: 200px;
margin-bottom: -200px;
background-image: linear-gradient(transparent 60%, transparent 100%), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size)), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size));
background-position: 0 0, 0 0, var(--offset) var(--offset);
background-size: 100% 100%, var(--gap-size) var(--gap-size), var(--gap-size) var(--gap-size);
mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
pointer-events: none;
&.revered {
mask-image: linear-gradient(to top, black 0%, transparent 100%);
}
}
</style>

View File

@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import { useInterval } from '@@/js/use-interval.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import { sum } from '@/utility/array.js';
import { pleaseLogin } from '@/utility/please-login.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { useLowresTime } from '@/composables/use-lowres-time.js';
const props = defineProps<{
noteId: string;
@ -48,21 +48,7 @@ const props = defineProps<{
author?: Misskey.entities.UserLite;
}>();
const now = useLowresTime();
const expiresAtTime = computed(() => props.expiresAt ? new Date(props.expiresAt).getTime() : null);
const remaining = computed(() => {
if (expiresAtTime.value == null) return -1;
return Math.floor(Math.max(expiresAtTime.value - now.value, 0) / 1000);
});
const remainingWatchStop = watch(remaining, (to) => {
if (to <= 0) {
showResult.value = true;
remainingWatchStop();
}
}, { immediate: true });
const remaining = ref(-1);
const total = computed(() => sum(props.choices.map(x => x.votes)));
const closed = computed(() => remaining.value === 0);
@ -85,7 +71,22 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
url: `https://${host}/notes/${props.noteId}`,
}));
const vote = async (id: number) => {
//
if (props.expiresAt) {
const tick = () => {
remaining.value = Math.floor(Math.max(new Date(props.expiresAt!).getTime() - Date.now(), 0) / 1000);
if (remaining.value === 0) {
showResult.value = true;
}
};
useInterval(tick, 3000, {
immediate: true,
afterMounted: false,
});
}
const vote = async (id) => {
if (props.readOnly || closed.value || isVoted.value) return;
pleaseLogin({ openOnRemote: pleaseLoginContext.value });

View File

@ -22,8 +22,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="multiple">{{ i18n.ts._poll.canMultipleVote }}</MkSwitch>
<section>
<div>
<MkSelect v-model="expiration" :items="expirationDef" small>
<MkSelect v-model="expiration" small>
<template #label>{{ i18n.ts._poll.expiration }}</template>
<option value="infinite">{{ i18n.ts._poll.infinite }}</option>
<option value="at">{{ i18n.ts._poll.at }}</option>
<option value="after">{{ i18n.ts._poll.after }}</option>
</MkSelect>
<section v-if="expiration === 'at'">
<MkInput v-model="atDate" small type="date" class="input">
@ -37,7 +40,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="after" small type="number" :min="1" class="input">
<template #label>{{ i18n.ts._poll.duration }}</template>
</MkInput>
<MkSelect v-model="unit" :items="unitDef" small></MkSelect>
<MkSelect v-model="unit" small>
<option value="second">{{ i18n.ts._time.second }}</option>
<option value="minute">{{ i18n.ts._time.minute }}</option>
<option value="hour">{{ i18n.ts._time.hour }}</option>
<option value="day">{{ i18n.ts._time.day }}</option>
</MkSelect>
</section>
</div>
</section>
@ -53,7 +61,6 @@ import MkButton from './MkButton.vue';
import { formatDateTimeString } from '@/utility/format-time-string.js';
import { addTime } from '@/utility/time.js';
import { i18n } from '@/i18n.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
export type PollEditorModelValue = {
expiresAt: number | null;
@ -71,32 +78,11 @@ const emit = defineEmits<{
const choices = ref(props.modelValue.choices);
const multiple = ref(props.modelValue.multiple);
const {
model: expiration,
def: expirationDef,
} = useMkSelect({
items: [
{ label: i18n.ts._poll.infinite, value: 'infinite' },
{ label: i18n.ts._poll.at, value: 'at' },
{ label: i18n.ts._poll.after, value: 'after' },
],
initialValue: 'infinite',
});
const expiration = ref('infinite');
const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
const atTime = ref('00:00');
const after = ref(0);
const {
model: unit,
def: unitDef,
} = useMkSelect({
items: [
{ label: i18n.ts._time.second, value: 'second' },
{ label: i18n.ts._time.minute, value: 'minute' },
{ label: i18n.ts._time.hour, value: 'hour' },
{ label: i18n.ts._time.day, value: 'day' },
],
initialValue: 'second',
});
const unit = ref('second');
if (props.modelValue.expiresAt) {
expiration.value = 'at';

View File

@ -6,15 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="[$style.root]">
<div :class="$style.items">
<button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'top' ? $style.active : null]" @click="() => { x = 'left'; y = 'top'; }"><i class="ti ti-arrow-up-left"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'top' ? $style.active : null]" @click="() => { x = 'center'; y = 'top'; }"><i class="ti ti-arrow-up"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'top' ? $style.active : null]" @click="() => { x = 'right'; y = 'top'; }"><i class="ti ti-arrow-up-right"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'center' ? $style.active : null]" @click="() => { x = 'left'; y = 'center'; }"><i class="ti ti-arrow-left"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'center' ? $style.active : null]" @click="() => { x = 'center'; y = 'center'; }"><i class="ti ti-focus-2"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'center' ? $style.active : null]" @click="() => { x = 'right'; y = 'center'; }"><i class="ti ti-arrow-right"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'left' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'left'; y = 'bottom'; }"><i class="ti ti-arrow-down-left"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'center' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'center'; y = 'bottom'; }"><i class="ti ti-arrow-down"></i></button>
<button v-panel class="_button" :class="[$style.item, x === 'right' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'right'; y = 'bottom'; }"><i class="ti ti-arrow-down-right"></i></button>
<button class="_button" :class="[$style.item, x === 'left' && y === 'top' ? $style.active : null]" @click="() => { x = 'left'; y = 'top'; }"><i class="ti ti-align-box-left-top"></i></button>
<button class="_button" :class="[$style.item, x === 'center' && y === 'top' ? $style.active : null]" @click="() => { x = 'center'; y = 'top'; }"><i class="ti ti-align-box-center-top"></i></button>
<button class="_button" :class="[$style.item, x === 'right' && y === 'top' ? $style.active : null]" @click="() => { x = 'right'; y = 'top'; }"><i class="ti ti-align-box-right-top"></i></button>
<button class="_button" :class="[$style.item, x === 'left' && y === 'center' ? $style.active : null]" @click="() => { x = 'left'; y = 'center'; }"><i class="ti ti-align-box-left-middle"></i></button>
<button class="_button" :class="[$style.item, x === 'center' && y === 'center' ? $style.active : null]" @click="() => { x = 'center'; y = 'center'; }"><i class="ti ti-align-box-center-middle"></i></button>
<button class="_button" :class="[$style.item, x === 'right' && y === 'center' ? $style.active : null]" @click="() => { x = 'right'; y = 'center'; }"><i class="ti ti-align-box-right-middle"></i></button>
<button class="_button" :class="[$style.item, x === 'left' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'left'; y = 'bottom'; }"><i class="ti ti-align-box-left-bottom"></i></button>
<button class="_button" :class="[$style.item, x === 'center' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'center'; y = 'bottom'; }"><i class="ti ti-align-box-center-bottom"></i></button>
<button class="_button" :class="[$style.item, x === 'right' && y === 'bottom' ? $style.active : null]" @click="() => { x = 'right'; y = 'bottom'; }"><i class="ti ti-align-box-right-bottom"></i></button>
</div>
</div>
</template>

View File

@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.headerLeft">
<button v-if="!fixed" :class="$style.cancel" class="_button" @click="cancel"><i class="ti ti-x"></i></button>
<button v-click-anime v-tooltip="i18n.ts.switchAccount" :class="$style.account" class="_button" @click="openAccountMenu">
<img :class="$style.avatar" :src="(postAccount ?? $i).avatarUrl" style="border-radius: 100%;"/>
<MkAvatar :user="postAccount ?? $i" :class="$style.avatar"/>
</button>
<button v-if="$i.policies.noteDraftLimit > 0" v-tooltip="(postAccount != null && postAccount.id !== $i.id) ? null : i18n.ts.draftsAndScheduledNotes" class="_button" :class="$style.draftButton" :disabled="postAccount != null && postAccount.id !== $i.id" @click="showDraftMenu"><i class="ti ti-list"></i></button>
<button v-if="$i.policies.noteDraftLimit > 0" v-tooltip="(postAccount != null && postAccount.id !== $i.id) ? null : i18n.ts.draft" class="_button" :class="$style.draftButton" :disabled="postAccount != null && postAccount.id !== $i.id" @click="showDraftMenu"><i class="ti ti-pencil-minus"></i></button>
</div>
<div :class="$style.headerRight">
<template v-if="!(targetChannel != null && fixed)">
@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="posted"></template>
<template v-else-if="posting"><MkEllipsis/></template>
<template v-else>{{ submitText }}</template>
<i style="margin-left: 6px;" :class="submitIcon"></i>
<i style="margin-left: 6px;" :class="posted ? 'ti ti-check' : replyTargetNote ? 'ti ti-arrow-back-up' : renoteTargetNote ? 'ti ti-quote' : 'ti ti-send'"></i>
</div>
</button>
</div>
@ -61,13 +61,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
</div>
</div>
<MkInfo v-if="scheduledAt != null" :class="$style.scheduledAt">
<I18n :src="i18n.ts.scheduleToPostOnX" tag="span">
<template #x>
<MkTime :time="scheduledAt" :mode="'detail'" style="font-weight: bold;"/>
</template>
</I18n> - <button class="_textButton" @click="cancelSchedule()">{{ i18n.ts.cancel }}</button>
</MkInfo>
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<div v-show="useCw" :class="$style.cwOuter">
<input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd">
@ -112,7 +105,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef, onUnmounted } from 'vue';
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
@ -206,7 +199,6 @@ if (props.initialVisibleUsers) {
props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
}
const reactionAcceptance = ref(store.s.reactionAcceptance);
const scheduledAt = ref<number | null>(null);
const draghover = ref(false);
const quoteId = ref<string | null>(null);
const hasNotSpecifiedMentions = ref(false);
@ -226,10 +218,6 @@ const uploader = useUploader({
multiple: true,
});
onUnmounted(() => {
uploader.dispose();
});
uploader.events.on('itemUploaded', ctx => {
files.value.push(ctx.item.uploaded!);
uploader.removeItem(ctx.item);
@ -270,19 +258,13 @@ const placeholder = computed((): string => {
});
const submitText = computed((): string => {
return scheduledAt.value != null
? i18n.ts.schedule
: renoteTargetNote.value
return renoteTargetNote.value
? i18n.ts.quote
: replyTargetNote.value
? i18n.ts.reply
: i18n.ts.note;
});
const submitIcon = computed((): string => {
return posted.value ? 'ti ti-check' : scheduledAt.value != null ? 'ti ti-calendar-time' : replyTargetNote.value ? 'ti ti-arrow-back-up' : renoteTargetNote.value ? 'ti ti-quote' : 'ti ti-send';
});
const textLength = computed((): number => {
return (text.value + imeText.value).length;
});
@ -428,7 +410,6 @@ function watchForDraft() {
watch(localOnly, () => saveDraft());
watch(quoteId, () => saveDraft());
watch(reactionAcceptance, () => saveDraft());
watch(scheduledAt, () => saveDraft());
}
function checkMissingMention() {
@ -586,11 +567,11 @@ async function toggleReactionAcceptance() {
const select = await os.select({
title: i18n.ts.reactionAcceptance,
items: [
{ value: null, label: i18n.ts.all },
{ value: 'likeOnlyForRemote' as const, label: i18n.ts.likeOnlyForRemote },
{ value: 'nonSensitiveOnly' as const, label: i18n.ts.nonSensitiveOnly },
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, label: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
{ value: 'likeOnly' as const, label: i18n.ts.likeOnly },
{ value: null, text: i18n.ts.all },
{ value: 'likeOnlyForRemote' as const, text: i18n.ts.likeOnlyForRemote },
{ value: 'nonSensitiveOnly' as const, text: i18n.ts.nonSensitiveOnly },
{ value: 'nonSensitiveOnlyForLocalLikeOnlyForRemote' as const, text: i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote },
{ value: 'likeOnly' as const, text: i18n.ts.likeOnly },
],
default: reactionAcceptance.value,
});
@ -620,13 +601,7 @@ function showOtherSettings() {
action: () => {
toggleReactionAcceptance();
},
}, ...($i.policies.scheduledNoteLimit > 0 ? [{
icon: 'ti ti-calendar-time',
text: i18n.ts.schedulePost + '...',
action: () => {
schedule();
},
}] : []), { type: 'divider' }, {
}, { type: 'divider' }, {
type: 'switch',
icon: 'ti ti-eye',
text: i18n.ts.preview,
@ -675,7 +650,6 @@ function clear() {
files.value = [];
poll.value = null;
quoteId.value = null;
scheduledAt.value = null;
}
function onKeydown(ev: KeyboardEvent) {
@ -831,7 +805,6 @@ function saveDraft() {
...( visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}),
quoteId: quoteId.value,
reactionAcceptance: reactionAcceptance.value,
scheduledAt: scheduledAt.value,
},
};
@ -846,25 +819,29 @@ function deleteDraft() {
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
}
async function saveServerDraft(options: {
isActuallyScheduled?: boolean;
} = {}) {
async function saveServerDraft(clearLocal = false) {
return await os.apiWithDialog(serverDraftId.value == null ? 'notes/drafts/create' : 'notes/drafts/update', {
...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }),
text: text.value,
cw: useCw.value ? cw.value || null : null,
useCw: useCw.value,
cw: cw.value,
visibility: visibility.value,
localOnly: localOnly.value,
hashtag: hashtags.value,
fileIds: files.value.map(f => f.id),
...(files.value.length > 0 ? { fileIds: files.value.map(f => f.id) } : {}),
poll: poll.value,
visibleUserIds: visibleUsers.value.map(x => x.id),
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : quoteId.value ? quoteId.value : null,
replyId: replyTargetNote.value ? replyTargetNote.value.id : null,
channelId: targetChannel.value ? targetChannel.value.id : null,
...(visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}),
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : undefined,
replyId: replyTargetNote.value ? replyTargetNote.value.id : undefined,
quoteId: quoteId.value,
channelId: targetChannel.value ? targetChannel.value.id : undefined,
reactionAcceptance: reactionAcceptance.value,
scheduledAt: scheduledAt.value,
isActuallyScheduled: options.isActuallyScheduled ?? false,
}).then(() => {
if (clearLocal) {
clear();
deleteDraft();
}
}).catch((err) => {
});
}
@ -899,21 +876,6 @@ async function post(ev?: MouseEvent) {
}
}
if (scheduledAt.value != null) {
if (uploader.items.value.some(x => x.uploaded == null)) {
await uploadFiles();
//
if (uploader.items.value.some(x => x.uploaded == null)) {
return;
}
}
await postAsScheduled();
clear();
return;
}
if (props.mock) return;
if (visibility.value === 'public' && (
@ -1085,14 +1047,6 @@ async function post(ev?: MouseEvent) {
});
}
async function postAsScheduled() {
if (props.mock) return;
await saveServerDraft({
isActuallyScheduled: true,
});
}
function cancel() {
emit('cancel');
}
@ -1187,10 +1141,8 @@ function showPerUploadItemMenuViaContextmenu(item: UploaderItem, ev: MouseEvent)
}
function showDraftMenu(ev: MouseEvent) {
function showDraftsDialog(scheduled: boolean) {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNoteDraftsDialog.vue')), {
scheduled,
}, {
function showDraftsDialog() {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNoteDraftsDialog.vue')), {}, {
restore: async (draft: Misskey.entities.NoteDraft) => {
text.value = draft.text ?? '';
useCw.value = draft.cw != null;
@ -1221,7 +1173,6 @@ function showDraftMenu(ev: MouseEvent) {
renoteTargetNote.value = draft.renote;
replyTargetNote.value = draft.reply;
reactionAcceptance.value = draft.reactionAcceptance;
scheduledAt.value = draft.scheduledAt ?? null;
if (draft.channel) targetChannel.value = draft.channel as unknown as Misskey.entities.Channel;
visibleUsers.value = [];
@ -1262,32 +1213,11 @@ function showDraftMenu(ev: MouseEvent) {
text: i18n.ts._drafts.listDrafts,
icon: 'ti ti-cloud-download',
action: () => {
showDraftsDialog(false);
},
}, { type: 'divider' }, {
type: 'button',
text: i18n.ts._drafts.listScheduledNotes,
icon: 'ti ti-clock-down',
action: () => {
showDraftsDialog(true);
showDraftsDialog();
},
}], (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}
async function schedule() {
const { canceled, result } = await os.inputDatetime({
title: i18n.ts.schedulePost,
});
if (canceled) return;
if (result.getTime() <= Date.now()) return;
scheduledAt.value = result.getTime();
}
function cancelSchedule() {
scheduledAt.value = null;
}
onMounted(() => {
if (props.autofocus) {
focus();
@ -1323,7 +1253,6 @@ onMounted(() => {
}
quoteId.value = draft.data.quoteId;
reactionAcceptance.value = draft.data.reactionAcceptance;
scheduledAt.value = draft.data.scheduledAt ?? null;
}
}
@ -1373,7 +1302,6 @@ async function canClose() {
defineExpose({
clear,
abortUploader: () => uploader.abortAll(),
canClose,
});
</script>
@ -1588,10 +1516,6 @@ html[data-color-scheme=light] .preview {
margin: 0 20px 16px 20px;
}
.scheduledAt {
margin: 0 20px 16px 20px;
}
.cw,
.hashtags,
.text {

View File

@ -54,7 +54,6 @@ function onPosted() {
async function _close() {
const canClose = await form.value?.canClose();
if (!canClose) return;
form.value?.abortUploader();
modal.value?.close();
}

View File

@ -114,13 +114,14 @@ async function unsubscribe() {
if ($i && accounts.length >= 2) {
apiWithDialog('sw/unregister', {
i: $i.token,
endpoint,
}, $i.token);
});
} else {
pushSubscription.value.unsubscribe();
apiWithDialog('sw/unregister', {
endpoint,
}, null);
});
pushSubscription.value = null;
}
}
@ -133,7 +134,7 @@ function encode(buffer: ArrayBuffer | null) {
* Convert the URL safe base64 string to a Uint8Array
* @param base64String base64 string
*/
function urlBase64ToUint8Array(base64String: string): BufferSource {
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
<template v-if="forModeration">
<i v-if="'isPublic' in role && role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i>
<i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i>
<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--MI_THEME-warn)"></i>
</template>
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</span>
<span :class="$style.bodyName">{{ role.name }}</span>
<template v-if="detailed && 'target' in role && 'usersCount' in role">
<template v-if="detailed">
<span v-if="role.target === 'manual'" :class="$style.bodyUsers">{{ role.usersCount }} users</span>
<span v-else-if="role.target === 'conditional'" :class="$style.bodyUsers">? users</span>
</template>
@ -39,7 +39,7 @@ import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
role: Misskey.entities.Role | Misskey.entities.IResponse['roles'][number];
role: Misskey.entities.Role;
forModeration: boolean;
detailed?: boolean;
}>(), {

View File

@ -102,12 +102,12 @@ async function addRole() {
const items = roles.value
.filter(r => r.isPublic)
.filter(r => !selectedRoleIds.value.includes(r.id))
.map(r => ({ label: r.name, value: r.id }));
.map(r => ({ text: r.name, value: r }));
const { canceled, result: roleId } = await os.select({ items });
if (canceled || roleId == null) return;
const { canceled, result: role } = await os.select({ items });
if (canceled || role == null) return;
selectedRoleIds.value.push(roleId);
selectedRoleIds.value.push(role.id);
}
async function removeRole(roleId: string) {

View File

@ -40,41 +40,46 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
export type OptionValue = string | number | null;
export type ItemOption<T extends OptionValue = OptionValue> = {
type ItemOption = {
type?: 'option';
value: T;
value: string | number | null;
label: string;
};
export type ItemGroup<T extends OptionValue = OptionValue> = {
type ItemGroup = {
type: 'group';
label?: string;
items: ItemOption<T>[];
label: string;
items: ItemOption[];
};
export type MkSelectItem<T extends OptionValue = OptionValue> = ItemOption<T> | ItemGroup<T>;
export type MkSelectItem = ItemOption | ItemGroup;
export type GetMkSelectValueType<T extends MkSelectItem> = T extends ItemGroup
? T['items'][number]['value']
: T extends ItemOption
? T['value']
: never;
export type GetMkSelectValueTypesFromDef<T extends MkSelectItem[]> = T[number] extends MkSelectItem
? GetMkSelectValueType<T[number]>
type ValuesOfItems<T> = T extends (infer U)[]
? U extends { type: 'group'; items: infer V }
? V extends (infer W)[]
? W extends { value: infer X }
? X
: never
: never
: U extends { value: infer Y }
? Y
: never
: never;
</script>
<script lang="ts" setup generic="const ITEMS extends MkSelectItem[], MODELT extends OptionValue">
import { onMounted, nextTick, ref, watch, computed, toRefs } from 'vue';
<script lang="ts" setup generic="T extends MkSelectItem[]">
import { onMounted, nextTick, ref, watch, computed, toRefs, useSlots } from 'vue';
import { useInterval } from '@@/js/use-interval.js';
import type { VNode, VNodeChild } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
// TODO: itemsslotoption(props.items)
// see: https://github.com/misskey-dev/misskey/issues/15558
//
const props = defineProps<{
items: ITEMS;
modelValue: ValuesOfItems<T>;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
@ -83,17 +88,16 @@ const props = defineProps<{
inline?: boolean;
small?: boolean;
large?: boolean;
items?: T;
}>();
type ModelTChecked = MODELT & (
MODELT extends GetMkSelectValueTypesFromDef<ITEMS>
? unknown
: 'Error: The type of model does not match the type of items.'
);
const emit = defineEmits<{
(ev: 'update:modelValue', value: ValuesOfItems<T>): void;
}>();
const model = defineModel<ModelTChecked>({ required: true });
const slots = useSlots();
const { autofocus } = toRefs(props);
const { modelValue, autofocus } = toRefs(props);
const focused = ref(false);
const opening = ref(false);
const currentValueText = ref<string | null>(null);
@ -136,18 +140,19 @@ onMounted(() => {
});
});
watch([model, () => props.items], () => {
watch([modelValue, () => props.items], () => {
if (props.items) {
let found: ItemOption | null = null;
for (const item of props.items) {
if (item.type === 'group') {
for (const option of item.items) {
if (option.value === model.value) {
if (option.value === modelValue.value) {
found = option;
break;
}
}
} else {
if (item.value === model.value) {
if (item.value === modelValue.value) {
found = item;
break;
}
@ -156,6 +161,31 @@ watch([model, () => props.items], () => {
if (found) {
currentValueText.value = found.label;
}
return;
}
const scanOptions = (options: VNodeChild[]) => {
for (const vnode of options) {
if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
if (vnode.type === 'optgroup') {
const optgroup = vnode;
if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
} else if (Array.isArray(vnode.children)) { //
const fragment = vnode;
if (Array.isArray(fragment.children)) scanOptions(fragment.children);
} else if (vnode.props == null) { // v-if false
// nop?
} else {
const option = vnode;
if (option.props?.value === modelValue.value) {
currentValueText.value = option.children as string;
break;
}
}
}
};
scanOptions(slots.default!());
}, { immediate: true, deep: true });
function show() {
@ -166,33 +196,69 @@ function show() {
const menu: MenuItem[] = [];
if (props.items) {
for (const item of props.items) {
if (item.type === 'group') {
if (item.label != null) {
menu.push({
type: 'label',
text: item.label,
});
}
for (const option of item.items) {
menu.push({
text: option.label,
active: computed(() => model.value === option.value),
active: computed(() => modelValue.value === option.value),
action: () => {
model.value = option.value as ModelTChecked;
emit('update:modelValue', option.value);
},
});
}
} else {
menu.push({
text: item.label,
active: computed(() => model.value === item.value),
active: computed(() => modelValue.value === item.value),
action: () => {
model.value = item.value as ModelTChecked;
emit('update:modelValue', item.value);
},
});
}
}
} else {
let options = slots.default!();
const pushOption = (option: VNode) => {
menu.push({
text: option.children as string,
active: computed(() => modelValue.value === option.props?.value),
action: () => {
emit('update:modelValue', option.props?.value);
},
});
};
const scanOptions = (options: VNodeChild[]) => {
for (const vnode of options) {
if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
if (vnode.type === 'optgroup') {
const optgroup = vnode;
menu.push({
type: 'label',
text: optgroup.props?.label,
});
if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
} else if (Array.isArray(vnode.children)) { //
const fragment = vnode;
if (Array.isArray(fragment.children)) scanOptions(fragment.children);
} else if (vnode.props == null) { // v-if false
// nop?
} else {
const option = vnode;
pushOption(option);
}
}
};
scanOptions(options);
}
os.popupMenu(menu, container.value, {
width: container.value?.offsetWidth,

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